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.
@@ -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", "board_shape")
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
- # The parser module.
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
- # Parse a FEEN string into position params.
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 feen [String] The FEEN string representing a position.
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 Parse a classic Tsume Shogi problem
13
- # call("3sks3/9/4+P4/9/7+B1/9/9/9/9 s")
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
- # # "board_shape": [9, 9],
16
- # # "side_to_move": "s",
17
- # # "piece_placement": {
18
- # # 3 => "s",
19
- # # 4 => "k",
20
- # # 5 => "s",
21
- # # 22 => "+P",
22
- # # 43 => "+B"
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
- # @return [Hash] The position params representing the position.
26
- def self.call(feen, regex: /\+?[a-z]/i)
27
- piece_placement_str, side_to_move_str = feen.split
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
- board_shape: BoardShape.new(piece_placement_str, regex:).to_a,
31
- piece_placement: piece_placement(piece_placement_str, regex:),
32
- side_to_move: side_to_move_str
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://github.com/sashite/specs/blob/main/forsyth-edwards-expanded-notation.md
9
+ # @see https://sashite.dev/documents/feen/1.0.0/
10
10
  module Feen
11
- # Dumps position params into a FEEN string.
11
+ # Dumps position components into a FEEN string.
12
12
  #
13
- # @param board_shape [Array] The shape of the board.
14
- # @param side_to_move [String] The identifier of the player who must play.
15
- # @param piece_placement [Hash] The index of each piece on the board.
16
- #
17
- # @example Dump a classic Tsume Shogi problem
18
- # dump(
19
- # "board_shape": [9, 9],
20
- # "side_to_move": "s",
21
- # "piece_placement": {
22
- # 3 => "s",
23
- # 4 => "k",
24
- # 5 => "s",
25
- # 22 => "+P",
26
- # 43 => "+B"
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
- # # => "3sks3/9/4+P4/9/7+B1/9/9/9/9 s"
30
- #
31
- # @return [String] The FEEN string representing the position.
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 params.
41
- #
42
- # @param feen [String] The FEEN string representing a position.
37
+ # Parses a FEEN string into position components.
43
38
  #
44
- # @example Parse a classic Tsume Shogi problem
45
- # parse("3sks3/9/4+P4/9/7+B1/9/9/9/9 s")
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
- # # "board_shape": [9, 9],
48
- # # "side_to_move": "s",
49
- # # "piece_placement": {
50
- # # 3 => "s",
51
- # # 4 => "k",
52
- # # 5 => "s",
53
- # # 22 => "+P",
54
- # # 43 => "+B"
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
- # @return [Hash] The position params representing the position.
58
- def self.parse(feen, regex: /\+?[a-z]/i)
59
- Parser.call(feen, regex:)
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.beta1
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: 2023-04-27 00:00:00.000000000 Z
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/board_shape.rb
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: 1.3.1
53
+ version: '0'
45
54
  requirements: []
46
- rubygems_version: 3.4.6
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