feen 5.0.0.beta1 → 5.0.0.beta3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.md +1 -1
- data/README.md +170 -44
- data/lib/feen/dumper/games_turn.rb +67 -0
- data/lib/feen/dumper/piece_placement.rb +117 -68
- data/lib/feen/dumper/pieces_in_hand/errors.rb +12 -0
- data/lib/feen/dumper/pieces_in_hand/no_pieces.rb +10 -0
- data/lib/feen/dumper/pieces_in_hand.rb +72 -0
- data/lib/feen/dumper.rb +73 -23
- data/lib/feen/parser/games_turn/errors.rb +14 -0
- data/lib/feen/parser/games_turn/valid_games_turn_pattern.rb +24 -0
- data/lib/feen/parser/games_turn.rb +58 -0
- data/lib/feen/parser/piece_placement.rb +634 -0
- data/lib/feen/parser/pieces_in_hand/errors.rb +14 -0
- data/lib/feen/parser/pieces_in_hand/no_pieces.rb +10 -0
- data/lib/feen/parser/pieces_in_hand/valid_format_pattern.rb +15 -0
- data/lib/feen/parser/pieces_in_hand.rb +84 -0
- data/lib/feen/parser.rb +74 -36
- data/lib/feen.rb +58 -43
- metadata +17 -9
- data/lib/feen/parser/board_shape.rb +0 -39
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Feen
|
4
|
+
module Parser
|
5
|
+
# Handles parsing of the pieces in hand section of a FEEN string.
|
6
|
+
# Pieces in hand represent pieces available for dropping onto the board.
|
7
|
+
module PiecesInHand
|
8
|
+
# Character used to represent no pieces in hand
|
9
|
+
NO_PIECES = "-"
|
10
|
+
|
11
|
+
# Error messages for validation
|
12
|
+
ERRORS = {
|
13
|
+
invalid_type: "Pieces in hand must be a string, got %s",
|
14
|
+
empty_string: "Pieces in hand string cannot be empty",
|
15
|
+
invalid_format: "Invalid pieces in hand format: %s",
|
16
|
+
sorting_error: "Pieces in hand must be in ASCII lexicographic order"
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
# Valid pattern for pieces in hand based on BNF:
|
20
|
+
# <pieces-in-hand> ::= "-" | <piece> <pieces-in-hand>
|
21
|
+
# <piece> ::= [a-zA-Z]
|
22
|
+
VALID_FORMAT_PATTERN = /\A(?:-|[a-zA-Z]+)\z/
|
23
|
+
|
24
|
+
# Parses the pieces in hand section of a FEEN string.
|
25
|
+
#
|
26
|
+
# @param pieces_in_hand_str [String] FEEN pieces in hand string
|
27
|
+
# @return [Array<String>] Array of single-character piece identifiers in the
|
28
|
+
# format specified in the FEEN string (no prefixes or suffixes), sorted in ASCII
|
29
|
+
# lexicographic order. Empty array if no pieces are in hand.
|
30
|
+
# @raise [ArgumentError] If the input string is invalid
|
31
|
+
#
|
32
|
+
# @example Parse no pieces in hand
|
33
|
+
# PiecesInHand.parse("-")
|
34
|
+
# # => []
|
35
|
+
#
|
36
|
+
# @example Parse multiple pieces in hand
|
37
|
+
# PiecesInHand.parse("BNPPb")
|
38
|
+
# # => ["B", "N", "P", "P", "b"]
|
39
|
+
def self.parse(pieces_in_hand_str)
|
40
|
+
validate_input_type(pieces_in_hand_str)
|
41
|
+
validate_format(pieces_in_hand_str)
|
42
|
+
|
43
|
+
return [] if pieces_in_hand_str == NO_PIECES
|
44
|
+
|
45
|
+
pieces = pieces_in_hand_str.chars
|
46
|
+
validate_pieces_order(pieces)
|
47
|
+
|
48
|
+
pieces
|
49
|
+
end
|
50
|
+
|
51
|
+
# Validates that the input is a non-empty string.
|
52
|
+
#
|
53
|
+
# @param str [String] Input string to validate
|
54
|
+
# @raise [ArgumentError] If input is not a string or is empty
|
55
|
+
# @return [void]
|
56
|
+
private_class_method def self.validate_input_type(str)
|
57
|
+
raise ::ArgumentError, format(ERRORS[:invalid_type], str.class) unless str.is_a?(::String)
|
58
|
+
raise ::ArgumentError, ERRORS[:empty_string] if str.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
# Validates that the input string matches the expected format.
|
62
|
+
#
|
63
|
+
# @param str [String] Input string to validate
|
64
|
+
# @raise [ArgumentError] If format is invalid
|
65
|
+
# @return [void]
|
66
|
+
private_class_method def self.validate_format(str)
|
67
|
+
return if str.match?(VALID_FORMAT_PATTERN)
|
68
|
+
|
69
|
+
raise ::ArgumentError, format(ERRORS[:invalid_format], str)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Validates that pieces are sorted in ASCII lexicographic order.
|
73
|
+
#
|
74
|
+
# @param pieces [Array<String>] Array of piece identifiers
|
75
|
+
# @raise [ArgumentError] If pieces are not sorted
|
76
|
+
# @return [void]
|
77
|
+
private_class_method def self.validate_pieces_order(pieces)
|
78
|
+
return if pieces == pieces.sort
|
79
|
+
|
80
|
+
raise ::ArgumentError, ERRORS[:sorting_error]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/feen/parser.rb
CHANGED
@@ -1,51 +1,89 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative File.join("parser", "
|
3
|
+
require_relative File.join("parser", "games_turn")
|
4
|
+
require_relative File.join("parser", "piece_placement")
|
5
|
+
require_relative File.join("parser", "pieces_in_hand")
|
4
6
|
|
5
7
|
module Feen
|
6
|
-
#
|
8
|
+
# Module responsible for parsing FEEN notation strings into internal data structures.
|
9
|
+
# FEEN (Format for Encounter & Entertainment Notation) is a compact, canonical, and rule-agnostic
|
10
|
+
# textual format for representing static board positions in two-player piece-placement games.
|
7
11
|
module Parser
|
8
|
-
#
|
12
|
+
# Regular expression pattern for matching a valid FEEN string structure
|
13
|
+
# Captures exactly three non-space components separated by single spaces
|
14
|
+
FEEN_PATTERN = /\A([^\s]+)\s([^\s]+)\s([^\s]+)\z/
|
15
|
+
|
16
|
+
# Error message for invalid FEEN format
|
17
|
+
INVALID_FORMAT_ERROR = "Invalid FEEN format: expected exactly 3 fields separated by single spaces"
|
18
|
+
|
19
|
+
# Parses a complete FEEN string into a structured representation
|
9
20
|
#
|
10
|
-
# @param
|
21
|
+
# @param feen_string [String] Complete FEEN notation string
|
22
|
+
# @return [Hash] Hash containing the parsed position data with the following keys:
|
23
|
+
# - :piece_placement [Array] - Hierarchical array structure representing the board
|
24
|
+
# - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board
|
25
|
+
# - :games_turn [Array<String>] - A two-element array with [active_variant, inactive_variant]
|
26
|
+
# @raise [ArgumentError] If the FEEN string is invalid or any component cannot be parsed
|
11
27
|
#
|
12
|
-
# @example
|
13
|
-
#
|
28
|
+
# @example Parsing a standard chess initial position
|
29
|
+
# feen = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
30
|
+
# result = Feen::Parser.parse(feen)
|
14
31
|
# # => {
|
15
|
-
# #
|
16
|
-
# #
|
17
|
-
# #
|
18
|
-
# #
|
19
|
-
# #
|
20
|
-
# #
|
21
|
-
# #
|
22
|
-
# #
|
23
|
-
# #
|
32
|
+
# # piece_placement: [
|
33
|
+
# # ["r", "n", "b", "q", "k=", "b", "n", "r"],
|
34
|
+
# # ["p", "p", "p", "p", "p", "p", "p", "p"],
|
35
|
+
# # ["", "", "", "", "", "", "", ""],
|
36
|
+
# # ["", "", "", "", "", "", "", ""],
|
37
|
+
# # ["", "", "", "", "", "", "", ""],
|
38
|
+
# # ["", "", "", "", "", "", "", ""],
|
39
|
+
# # ["P", "P", "P", "P", "P", "P", "P", "P"],
|
40
|
+
# # ["R", "N", "B", "Q", "K=", "B", "N", "R"]
|
41
|
+
# # ],
|
42
|
+
# # pieces_in_hand: [],
|
43
|
+
# # games_turn: ["CHESS", "chess"]
|
44
|
+
# # }
|
24
45
|
#
|
25
|
-
# @
|
26
|
-
|
27
|
-
|
46
|
+
# @example Parsing a shogi position (from a Tempo Loss Bishop Exchange opening) with pieces in hand
|
47
|
+
# feen = "lnsgk2nl/1r4gs1/p1pppp1pp/1p4p2/7P1/2P6/PP1PPPP1P/1SG4R1/LN2KGSNL Bb SHOGI/shogi"
|
48
|
+
# result = Feen::Parser.parse(feen)
|
49
|
+
# # => {
|
50
|
+
# # piece_placement: [
|
51
|
+
# # ["l", "n", "s", "g", "k", "", "", "n", "l"],
|
52
|
+
# # ["", "r", "", "", "", "", "g", "s", ""],
|
53
|
+
# # ["p", "", "p", "p", "p", "p", "", "p", "p"],
|
54
|
+
# # ["", "p", "", "", "", "", "p", "", ""],
|
55
|
+
# # ["", "", "", "", "", "", "", "P", ""],
|
56
|
+
# # ["", "", "P", "", "", "", "", "", ""],
|
57
|
+
# # ["P", "P", "", "P", "P", "P", "P", "", "P"],
|
58
|
+
# # ["", "S", "G", "", "", "", "", "R", ""],
|
59
|
+
# # ["L", "N", "", "", "K", "G", "S", "N", "L"]
|
60
|
+
# # ],
|
61
|
+
# # pieces_in_hand: ["B", "b"],
|
62
|
+
# # games_turn: ["SHOGI", "shogi"]
|
63
|
+
# # }
|
64
|
+
def self.parse(feen_string)
|
65
|
+
feen_string = String(feen_string)
|
66
|
+
|
67
|
+
# Match the FEEN string against the expected pattern
|
68
|
+
match = FEEN_PATTERN.match(feen_string)
|
69
|
+
|
70
|
+
# Raise an error if the format doesn't match the expected pattern
|
71
|
+
raise ::ArgumentError, INVALID_FORMAT_ERROR unless match
|
28
72
|
|
73
|
+
# Capture the three distinct parts
|
74
|
+
piece_placement_string, pieces_in_hand_string, games_turn_string = match.captures
|
75
|
+
|
76
|
+
# Parse each field using the appropriate submodule
|
77
|
+
piece_placement = PiecePlacement.parse(piece_placement_string)
|
78
|
+
pieces_in_hand = PiecesInHand.parse(pieces_in_hand_string)
|
79
|
+
games_turn = GamesTurn.parse(games_turn_string)
|
80
|
+
|
81
|
+
# Create a structured representation of the position
|
29
82
|
{
|
30
|
-
|
31
|
-
|
32
|
-
|
83
|
+
piece_placement:,
|
84
|
+
pieces_in_hand:,
|
85
|
+
games_turn:
|
33
86
|
}
|
34
87
|
end
|
35
|
-
|
36
|
-
def self.piece_placement(string, regex:)
|
37
|
-
hash = {}
|
38
|
-
index = 0
|
39
|
-
string.scan(/(\d+|#{regex})/) do |match|
|
40
|
-
if /\d+/.match?(match[0])
|
41
|
-
index += match[0].to_i
|
42
|
-
else
|
43
|
-
hash[index] = match[0]
|
44
|
-
index += 1
|
45
|
-
end
|
46
|
-
end
|
47
|
-
hash
|
48
|
-
end
|
49
|
-
private_class_method :piece_placement
|
50
88
|
end
|
51
89
|
end
|
data/lib/feen.rb
CHANGED
@@ -6,56 +6,71 @@ require_relative File.join("feen", "parser")
|
|
6
6
|
# This module provides a Ruby interface for data serialization and
|
7
7
|
# deserialization in FEEN format.
|
8
8
|
#
|
9
|
-
# @see https://
|
9
|
+
# @see https://sashite.dev/documents/feen/1.0.0/
|
10
10
|
module Feen
|
11
|
-
# Dumps position
|
11
|
+
# Dumps position components into a FEEN string.
|
12
12
|
#
|
13
|
-
# @
|
14
|
-
# @
|
15
|
-
# @
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
# "
|
20
|
-
# "
|
21
|
-
# "
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
13
|
+
# @see Feen::Dumper.dump for the detailed parameters documentation
|
14
|
+
# @return [String] FEEN notation string
|
15
|
+
# @raise [ArgumentError] If any parameter is invalid
|
16
|
+
# @example
|
17
|
+
# piece_placement = [
|
18
|
+
# ["r", "n", "b", "q", "k=", "b", "n", "r"],
|
19
|
+
# ["p", "p", "p", "p", "p", "p", "p", "p"],
|
20
|
+
# ["", "", "", "", "", "", "", ""],
|
21
|
+
# ["", "", "", "", "", "", "", ""],
|
22
|
+
# ["", "", "", "", "", "", "", ""],
|
23
|
+
# ["", "", "", "", "", "", "", ""],
|
24
|
+
# ["P", "P", "P", "P", "P", "P", "P", "P"],
|
25
|
+
# ["R", "N", "B", "Q", "K=", "B", "N", "R"]
|
26
|
+
# ]
|
27
|
+
# Feen.dump(
|
28
|
+
# piece_placement: piece_placement,
|
29
|
+
# pieces_in_hand: [],
|
30
|
+
# games_turn: ["CHESS", "chess"]
|
28
31
|
# )
|
29
|
-
# # => "
|
30
|
-
|
31
|
-
|
32
|
-
def self.dump(board_shape:, side_to_move:, piece_placement:)
|
33
|
-
Dumper.call(
|
34
|
-
board_shape:,
|
35
|
-
side_to_move:,
|
36
|
-
piece_placement:
|
37
|
-
)
|
32
|
+
# # => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
33
|
+
def self.dump(...)
|
34
|
+
Dumper.dump(...)
|
38
35
|
end
|
39
36
|
|
40
|
-
# Parses a FEEN string into position
|
41
|
-
#
|
42
|
-
# @param feen [String] The FEEN string representing a position.
|
37
|
+
# Parses a FEEN string into position components.
|
43
38
|
#
|
44
|
-
# @
|
45
|
-
#
|
39
|
+
# @see Feen::Parser.parse for the detailed parameters and return value documentation
|
40
|
+
# @return [Hash] Hash containing the parsed position data
|
41
|
+
# @raise [ArgumentError] If the FEEN string is invalid
|
42
|
+
# @example
|
43
|
+
# feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
44
|
+
# Feen.parse(feen_string)
|
46
45
|
# # => {
|
47
|
-
# #
|
48
|
-
# #
|
49
|
-
# #
|
50
|
-
# #
|
51
|
-
# #
|
52
|
-
# #
|
53
|
-
# #
|
54
|
-
# #
|
55
|
-
# #
|
46
|
+
# # piece_placement: [
|
47
|
+
# # ["r", "n", "b", "q", "k=", "b", "n", "r"],
|
48
|
+
# # ["p", "p", "p", "p", "p", "p", "p", "p"],
|
49
|
+
# # ["", "", "", "", "", "", "", ""],
|
50
|
+
# # ["", "", "", "", "", "", "", ""],
|
51
|
+
# # ["", "", "", "", "", "", "", ""],
|
52
|
+
# # ["", "", "", "", "", "", "", ""],
|
53
|
+
# # ["P", "P", "P", "P", "P", "P", "P", "P"],
|
54
|
+
# # ["R", "N", "B", "Q", "K=", "B", "N", "R"]
|
55
|
+
# # ],
|
56
|
+
# # pieces_in_hand: [],
|
57
|
+
# # games_turn: ["CHESS", "chess"]
|
58
|
+
# # }
|
59
|
+
def self.parse(...)
|
60
|
+
Parser.parse(...)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Validates if the given string is a valid FEEN string
|
56
64
|
#
|
57
|
-
# @
|
58
|
-
|
59
|
-
|
65
|
+
# @see Feen.parse for parameter details
|
66
|
+
# @return [Boolean] True if the string is a valid FEEN string, false otherwise
|
67
|
+
# @example
|
68
|
+
# Feen.valid?("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess") # => true
|
69
|
+
# Feen.valid?("invalid feen string") # => false
|
70
|
+
def self.valid?(...)
|
71
|
+
parse(...)
|
72
|
+
true
|
73
|
+
rescue ::ArgumentError
|
74
|
+
false
|
60
75
|
end
|
61
76
|
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: feen
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.0.0.
|
4
|
+
version: 5.0.0.beta3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies: []
|
13
12
|
description: A Ruby interface for data serialization and deserialization in FEEN format.
|
14
13
|
email: contact@cyril.email
|
@@ -20,15 +19,25 @@ files:
|
|
20
19
|
- README.md
|
21
20
|
- lib/feen.rb
|
22
21
|
- lib/feen/dumper.rb
|
22
|
+
- lib/feen/dumper/games_turn.rb
|
23
23
|
- lib/feen/dumper/piece_placement.rb
|
24
|
+
- lib/feen/dumper/pieces_in_hand.rb
|
25
|
+
- lib/feen/dumper/pieces_in_hand/errors.rb
|
26
|
+
- lib/feen/dumper/pieces_in_hand/no_pieces.rb
|
24
27
|
- lib/feen/parser.rb
|
25
|
-
- lib/feen/parser/
|
28
|
+
- lib/feen/parser/games_turn.rb
|
29
|
+
- lib/feen/parser/games_turn/errors.rb
|
30
|
+
- lib/feen/parser/games_turn/valid_games_turn_pattern.rb
|
31
|
+
- lib/feen/parser/piece_placement.rb
|
32
|
+
- lib/feen/parser/pieces_in_hand.rb
|
33
|
+
- lib/feen/parser/pieces_in_hand/errors.rb
|
34
|
+
- lib/feen/parser/pieces_in_hand/no_pieces.rb
|
35
|
+
- lib/feen/parser/pieces_in_hand/valid_format_pattern.rb
|
26
36
|
homepage: https://github.com/sashite/feen.rb
|
27
37
|
licenses:
|
28
38
|
- MIT
|
29
39
|
metadata:
|
30
40
|
rubygems_mfa_required: 'true'
|
31
|
-
post_install_message:
|
32
41
|
rdoc_options: []
|
33
42
|
require_paths:
|
34
43
|
- lib
|
@@ -39,12 +48,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
39
48
|
version: 3.2.0
|
40
49
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
50
|
requirements:
|
42
|
-
- - "
|
51
|
+
- - ">="
|
43
52
|
- !ruby/object:Gem::Version
|
44
|
-
version:
|
53
|
+
version: '0'
|
45
54
|
requirements: []
|
46
|
-
rubygems_version: 3.
|
47
|
-
signing_key:
|
55
|
+
rubygems_version: 3.6.7
|
48
56
|
specification_version: 4
|
49
57
|
summary: FEEN support for the Ruby language.
|
50
58
|
test_files: []
|
@@ -1,39 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Feen
|
4
|
-
module Parser
|
5
|
-
# The BoardShape class.
|
6
|
-
#
|
7
|
-
# @example Parse the shape of a shogiban
|
8
|
-
# BoardShape.new("3sks3/9/4+P4/9/7+B1/9/9/9/9").to_a # => [9, 9]
|
9
|
-
class BoardShape
|
10
|
-
# @param board_str [String] The flatten board.
|
11
|
-
def initialize(board_str, regex: /\+?[a-z]/i)
|
12
|
-
@board_str = board_str
|
13
|
-
@regex = regex
|
14
|
-
end
|
15
|
-
|
16
|
-
# @return [Array] The size of each dimension of the board.
|
17
|
-
def to_a
|
18
|
-
indexes(@board_str, @board_str.scan(%r{/+}).sort.fetch(-1))
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def indexes(string, separator)
|
24
|
-
if separator.empty?
|
25
|
-
last_index = string.scan(/(\d+|#{@regex})/).inject(0) do |counter, match|
|
26
|
-
sub_string = match[0]
|
27
|
-
number = sub_string.match?(/\d+/) ? Integer(sub_string) : 1
|
28
|
-
counter + number
|
29
|
-
end
|
30
|
-
|
31
|
-
return [last_index]
|
32
|
-
end
|
33
|
-
|
34
|
-
sub_strings = string.split(separator)
|
35
|
-
[sub_strings.length] + indexes(sub_strings.fetch(0), separator[1..])
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|