sashite-feen 0.1.0 → 0.3.0

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.
@@ -2,39 +2,82 @@
2
2
 
3
3
  module Sashite
4
4
  module Feen
5
- # Namespaced error types for FEEN
6
- module Error
7
- # Base FEEN error (immutable, with optional context payload)
8
- class Base < StandardError
9
- # @return [Hash, nil] optional contextual information (e.g., { rank: 3, col: 5 })
10
- attr_reader :context
5
+ # Base error class for all FEEN-related errors.
6
+ #
7
+ # All FEEN parsing and validation errors inherit from this class,
8
+ # allowing callers to rescue all FEEN errors with a single rescue clause.
9
+ #
10
+ # @see https://sashite.dev/specs/feen/1.0.0/
11
+ class Error < StandardError
12
+ # Error raised when FEEN structure is malformed.
13
+ #
14
+ # Indicates problems with the overall FEEN format, such as:
15
+ # - Missing or incorrect number of fields
16
+ # - Missing required separators
17
+ # - Empty fields where content is required
18
+ # - Invalid field structure
19
+ #
20
+ # @example Missing field separator
21
+ # raise Error::Syntax, "FEEN must have exactly 3 space-separated fields"
22
+ #
23
+ # @example Empty required field
24
+ # raise Error::Syntax, "active style cannot be empty"
25
+ class Syntax < Error; end
11
26
 
12
- # @param message [String, nil]
13
- # @param context [Hash, nil] optional structured context (will be frozen)
14
- def initialize(message = nil, context: nil)
15
- @context = context&.dup&.freeze
16
- super(message)
17
- freeze
18
- end
19
- end
27
+ # Error raised when EPIN (Extended Piece Identifier Notation) is invalid.
28
+ #
29
+ # Indicates problems with piece notation, such as:
30
+ # - Invalid EPIN format
31
+ # - Unrecognized piece characters
32
+ # - Malformed state modifiers or derivation suffixes
33
+ # - EPIN parsing failures
34
+ #
35
+ # @example Invalid EPIN format
36
+ # raise Error::Piece, "invalid EPIN notation: K#"
37
+ #
38
+ # @example Failed EPIN parsing
39
+ # raise Error::Piece, "failed to parse EPIN 'X': unknown piece type"
40
+ class Piece < Error; end
20
41
 
21
- # Raised when the FEEN text does not match the required grammar
22
- class Syntax < Base; end
42
+ # Error raised when SIN (Style Identifier Notation) is invalid.
43
+ #
44
+ # Indicates problems with style notation, such as:
45
+ # - Invalid SIN format
46
+ # - Non-letter characters in style identifier
47
+ # - Multi-character style identifiers
48
+ # - SIN parsing failures
49
+ #
50
+ # @example Invalid SIN format
51
+ # raise Error::Style, "invalid SIN notation: '1' (must be a single letter)"
52
+ #
53
+ # @example Failed SIN parsing
54
+ # raise Error::Style, "failed to parse SIN 'XY': too long"
55
+ class Style < Error; end
23
56
 
24
- # Raised for structural/semantic violations after syntactic parsing
25
- class Validation < Base; end
57
+ # Error raised when piece counts are invalid.
58
+ #
59
+ # Indicates problems with piece quantity specifications, such as:
60
+ # - Count less than 1
61
+ # - Count exceeding reasonable limits
62
+ # - Invalid count format
63
+ #
64
+ # @example Count too small
65
+ # raise Error::Count, "piece count must be at least 1, got 0"
66
+ #
67
+ # @example Count too large
68
+ # raise Error::Count, "piece count too large: 9999"
69
+ class Count < Error; end
26
70
 
27
- # Raised when an EPIN token/value is invalid in the current context
28
- class Piece < Base; end
29
-
30
- # Raised when a SIN token/value or the style/turn field is invalid
31
- class Style < Base; end
32
-
33
- # Raised when a numeric count (e.g., run-length or hand quantity) is invalid
34
- class Count < Base; end
35
-
36
- # Raised when board/grid dimensions are empty/inconsistent/out of bounds
37
- class Bounds < Base; end
71
+ # Error raised for other semantic validation failures.
72
+ #
73
+ # Indicates problems that don't fit other error categories, such as:
74
+ # - Inconsistent position state
75
+ # - Rule violations
76
+ # - Other semantic constraints
77
+ #
78
+ # @example Semantic constraint violation
79
+ # raise Error::Validation, "position violates conservation principle"
80
+ class Validation < Error; end
38
81
  end
39
82
  end
40
83
  end
@@ -2,36 +2,78 @@
2
2
 
3
3
  module Sashite
4
4
  module Feen
5
- # Immutable multiset of pieces-in-hand, keyed by EPIN value (as returned by Sashite::Epin.parse)
5
+ # Immutable representation of pieces held in hand by each player.
6
+ #
7
+ # Stores captured pieces that players hold in reserve, available for
8
+ # placement back onto the board in games that support drop mechanics
9
+ # (such as shogi, crazyhouse, etc.).
10
+ #
11
+ # @see https://sashite.dev/specs/feen/1.0.0/
6
12
  class Hands
7
- attr_reader :map
13
+ # @return [Array] Array of pieces held by first player
14
+ attr_reader :first_player
8
15
 
9
- # @param map [Hash<any, Integer>] counts per EPIN value
10
- def initialize(map)
11
- raise TypeError, "hands map must be a Hash, got #{map.class}" unless map.is_a?(Hash)
16
+ # @return [Array] Array of pieces held by second player
17
+ attr_reader :second_player
12
18
 
13
- # Coerce counts to Integer and validate
14
- coerced = {}
15
- map.each do |k, v|
16
- c = Integer(v)
17
- raise Error::Count, "hand count must be >= 0, got #{c}" if c.negative?
18
- next if c.zero? # normalize: skip zeros
19
+ # Create a new immutable Hands object.
20
+ #
21
+ # @param first_player [Array] Pieces in first player's hand
22
+ # @param second_player [Array] Pieces in second player's hand
23
+ #
24
+ # @example Empty hands
25
+ # hands = Hands.new([], [])
26
+ #
27
+ # @example First player has captured pieces
28
+ # hands = Hands.new([pawn1, pawn2], [])
29
+ #
30
+ # @example Both players have captured pieces
31
+ # hands = Hands.new([rook, bishop], [pawn1, pawn2, knight])
32
+ def initialize(first_player, second_player)
33
+ @first_player = first_player.sort_by(&:to_s).freeze
34
+ @second_player = second_player.sort_by(&:to_s).freeze
19
35
 
20
- coerced[k] = c
21
- end
22
-
23
- # Freeze shallowly (keys may already be complex frozen EPIN values)
24
- @map = coerced.each_with_object({}) { |(k, v), h| h[k] = v }.freeze
25
36
  freeze
26
37
  end
27
38
 
28
- # Convenience
39
+ # Check if both hands are empty.
40
+ #
41
+ # @return [Boolean] True if neither player has pieces in hand
42
+ #
43
+ # @example
44
+ # hands.empty? # => true
29
45
  def empty?
30
- @map.empty?
46
+ first_player.empty? && second_player.empty?
47
+ end
48
+
49
+ # Convert hands to their FEEN string representation.
50
+ #
51
+ # @return [String] FEEN pieces-in-hand field
52
+ #
53
+ # @example
54
+ # hands.to_s
55
+ # # => "2P/p"
56
+ def to_s
57
+ Dumper::PiecesInHand.dump(self)
31
58
  end
32
59
 
33
- def each(&)
34
- @map.each(&)
60
+ # Compare two hands for equality.
61
+ #
62
+ # @param other [Hands] Another hands object
63
+ # @return [Boolean] True if both players' pieces are equal
64
+ def ==(other)
65
+ other.is_a?(Hands) &&
66
+ first_player == other.first_player &&
67
+ second_player == other.second_player
68
+ end
69
+
70
+ alias eql? ==
71
+
72
+ # Generate hash code for hands.
73
+ #
74
+ # @return [Integer] Hash code based on both players' pieces
75
+ def hash
76
+ [first_player, second_player].hash
35
77
  end
36
78
  end
37
79
  end