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.
data/lib/feen/dumper.rb CHANGED
@@ -1,36 +1,86 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative File.join("dumper", "games_turn")
3
4
  require_relative File.join("dumper", "piece_placement")
5
+ require_relative File.join("dumper", "pieces_in_hand")
4
6
 
5
7
  module Feen
6
- # The dumper module.
8
+ # Module responsible for converting internal data structures to FEEN notation strings.
9
+ # This implements the serialization part of the FEEN (Format for Encounter & Entertainment Notation) format.
7
10
  module Dumper
8
- # Dump position params into a FEEN string.
9
- #
10
- # @param board_shape [Array] The shape of the board.
11
- # @param side_to_move [String] Identify the active side.
12
- # @param piece_placement [Hash] The index of each piece on the board.
11
+ # Field separator used between the three main components of FEEN notation
12
+ FIELD_SEPARATOR = " "
13
+
14
+ # Error messages for validation
15
+ ERRORS = {
16
+ invalid_piece_placement_type: "Piece placement must be an Array, got %s",
17
+ invalid_games_turn_type: "Games turn must be an Array with exactly two elements, got %s",
18
+ invalid_pieces_in_hand_type: "Pieces in hand must be an Array, got %s"
19
+ }.freeze
20
+
21
+ # Converts position components to a complete FEEN string.
13
22
  #
14
- # @example Dump a classic Tsume Shogi problem
15
- # call(
16
- # "board_shape": [9, 9],
17
- # "side_to_move": "s",
18
- # "piece_placement": {
19
- # 3 => "s",
20
- # 4 => "k",
21
- # 5 => "s",
22
- # 22 => "+P",
23
- # 43 => "+B"
24
- # }
23
+ # @example Creating a FEEN string for chess initial position
24
+ # Feen::Dumper.dump(
25
+ # piece_placement: [
26
+ # ["r", "n", "b", "q", "k=", "b", "n", "r"],
27
+ # ["p", "p", "p", "p", "p", "p", "p", "p"],
28
+ # ["", "", "", "", "", "", "", ""],
29
+ # ["", "", "", "", "", "", "", ""],
30
+ # ["", "", "", "", "", "", "", ""],
31
+ # ["", "", "", "", "", "", "", ""],
32
+ # ["P", "P", "P", "P", "P", "P", "P", "P"],
33
+ # ["R", "N", "B", "Q", "K=", "B", "N", "R"]
34
+ # ],
35
+ # pieces_in_hand: [],
36
+ # games_turn: ["CHESS", "chess"]
25
37
  # )
26
- # # => "3sks3/9/4+P4/9/7+B1/9/9/9/9 s"
38
+ # # => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
27
39
  #
28
- # @return [String] The FEEN string representing the position.
29
- def self.call(board_shape:, side_to_move:, piece_placement:)
40
+ # @param piece_placement [Array] Board position data structure representing the spatial
41
+ # distribution of pieces across the board, where each cell
42
+ # is represented by a String (or empty string for empty cells)
43
+ # @param pieces_in_hand [Array<String>] Pieces available for dropping onto the board,
44
+ # each represented as a single character string
45
+ # @param games_turn [Array<String>] A two-element array where the first element is the
46
+ # active player's variant and the second is the inactive player's variant
47
+ # @return [String] Complete FEEN string representation compliant with the specification
48
+ # @raise [ArgumentError] If any input parameter is invalid
49
+ # @see https://sashite.dev/documents/feen/1.0.0/ FEEN Specification v1.0.0
50
+ def self.dump(piece_placement:, pieces_in_hand:, games_turn:)
51
+ # Validate input types
52
+ validate_inputs(piece_placement, games_turn, pieces_in_hand)
53
+
54
+ # Process each component with appropriate submodule and combine into final FEEN string
30
55
  [
31
- PiecePlacement.new(board_shape, piece_placement).to_s,
32
- side_to_move
33
- ].join(" ")
56
+ PiecePlacement.dump(piece_placement),
57
+ PiecesInHand.dump(*pieces_in_hand),
58
+ GamesTurn.dump(*games_turn)
59
+ ].join(FIELD_SEPARATOR)
60
+ end
61
+
62
+ # Validates the input parameters for type and structure
63
+ #
64
+ # @param piece_placement [Object] Piece placement parameter to validate
65
+ # @param games_turn [Object] Games turn parameter to validate
66
+ # @param pieces_in_hand [Object] Pieces in hand parameter to validate
67
+ # @raise [ArgumentError] If any parameter is invalid
68
+ # @return [void]
69
+ private_class_method def self.validate_inputs(piece_placement, games_turn, pieces_in_hand)
70
+ # Validate piece_placement is an Array
71
+ unless piece_placement.is_a?(Array)
72
+ raise ArgumentError, format(ERRORS[:invalid_piece_placement_type], piece_placement.class)
73
+ end
74
+
75
+ # Validate games_turn is an Array with exactly 2 elements
76
+ unless games_turn.is_a?(Array) && games_turn.size == 2
77
+ raise ArgumentError, format(ERRORS[:invalid_games_turn_type], games_turn.inspect)
78
+ end
79
+
80
+ # Validate pieces_in_hand is an Array
81
+ return if pieces_in_hand.is_a?(Array)
82
+
83
+ raise ArgumentError, format(ERRORS[:invalid_pieces_in_hand_type], pieces_in_hand.class)
34
84
  end
35
85
  end
36
86
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Feen
4
+ module Parser
5
+ module GamesTurn
6
+ # Error messages for games turn parsing
7
+ Errors = {
8
+ invalid_type: "Games turn must be a string, got %s",
9
+ empty_string: "Games turn string cannot be empty",
10
+ invalid_format: "Invalid games turn format. Expected format: UPPERCASE/lowercase or lowercase/UPPERCASE"
11
+ }.freeze
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Feen
4
+ module Parser
5
+ module GamesTurn
6
+ # Complete pattern matching the BNF specification with named groups
7
+ # <games-turn> ::= <game-id-uppercase> "/" <game-id-lowercase>
8
+ # | <game-id-lowercase> "/" <game-id-uppercase>
9
+ ValidGamesTurnPattern = %r{
10
+ \A # Start of string
11
+ (?: # Non-capturing group for alternatives
12
+ (?<uppercase_first>[A-Z]+) # Named group: uppercase identifier first
13
+ / # Separator
14
+ (?<lowercase_second>[a-z]+) # Named group: lowercase identifier second
15
+ | # OR
16
+ (?<lowercase_first>[a-z]+) # Named group: lowercase identifier first
17
+ / # Separator
18
+ (?<uppercase_second>[A-Z]+) # Named group: uppercase identifier second
19
+ )
20
+ \z # End of string
21
+ }x
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative File.join("games_turn", "valid_games_turn_pattern")
4
+ require_relative File.join("games_turn", "errors")
5
+
6
+ module Feen
7
+ module Parser
8
+ # Handles parsing of the games turn section of a FEEN string
9
+ module GamesTurn
10
+ # Parses the games turn section of a FEEN string
11
+ #
12
+ # @param games_turn_str [String] FEEN games turn string
13
+ # @return [Array<String>] Array containing [active_player, inactive_player]
14
+ # @raise [ArgumentError] If the input string is invalid
15
+ #
16
+ # @example Valid games turn string with uppercase first
17
+ # GamesTurn.parse("CHESS/shogi")
18
+ # # => ["CHESS", "shogi"]
19
+ #
20
+ # @example Valid games turn string with lowercase first
21
+ # GamesTurn.parse("chess/SHOGI")
22
+ # # => ["chess", "SHOGI"]
23
+ def self.parse(games_turn_str)
24
+ validate_input_type(games_turn_str)
25
+
26
+ match = ValidGamesTurnPattern.match(games_turn_str)
27
+ raise ::ArgumentError, Errors[:invalid_format] unless match
28
+
29
+ extract_game_identifiers(**match.named_captures.transform_keys(&:to_sym))
30
+ end
31
+
32
+ # Validates that the input is a non-empty string
33
+ #
34
+ # @param str [String] Input string to validate
35
+ # @raise [ArgumentError] If input is not a string or is empty
36
+ # @return [void]
37
+ private_class_method def self.validate_input_type(str)
38
+ raise ::ArgumentError, format(Errors[:invalid_type], str.class) unless str.is_a?(::String)
39
+ raise ::ArgumentError, Errors[:empty_string] if str.empty?
40
+ end
41
+
42
+ # Extracts game identifiers from regexp match captures
43
+ #
44
+ # @param uppercase_first [String, nil] Uppercase identifier if it comes first
45
+ # @param lowercase_second [String, nil] Lowercase identifier if it comes second
46
+ # @param lowercase_first [String, nil] Lowercase identifier if it comes first
47
+ # @param uppercase_second [String, nil] Uppercase identifier if it comes second
48
+ # @return [Array<String>] Array containing [active_player, inactive_player]
49
+ private_class_method def self.extract_game_identifiers(uppercase_first: nil, lowercase_second: nil, lowercase_first: nil, uppercase_second: nil)
50
+ if uppercase_first
51
+ [uppercase_first, lowercase_second]
52
+ else
53
+ [lowercase_first, uppercase_second]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end