sashite-feen 0.1.0 → 0.2.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,25 +2,76 @@
2
2
 
3
3
  module Sashite
4
4
  module Feen
5
- # Immutable aggregate for a FEEN position: placement + hands + styles
5
+ # Immutable representation of a complete board game position.
6
+ #
7
+ # Combines piece placement, pieces in hand, and style-turn information
8
+ # into a single unified position object. This represents a complete
9
+ # snapshot of the game state at a given moment.
10
+ #
11
+ # @see https://sashite.dev/specs/feen/1.0.0/
6
12
  class Position
7
- attr_reader :placement, :hands, :styles
13
+ # @return [Placement] Board piece placement configuration
14
+ attr_reader :placement
8
15
 
9
- # @param placement [Sashite::Feen::Placement]
10
- # @param hands [Sashite::Feen::Hands]
11
- # @param styles [Sashite::Feen::Styles]
12
- def initialize(placement, hands, styles)
13
- unless placement.is_a?(Placement)
14
- raise TypeError, "placement must be Sashite::Feen::Placement, got #{placement.class}"
15
- end
16
- raise TypeError, "hands must be Sashite::Feen::Hands, got #{hands.class}" unless hands.is_a?(Hands)
17
- raise TypeError, "styles must be Sashite::Feen::Styles, got #{styles.class}" unless styles.is_a?(Styles)
16
+ # @return [Hands] Pieces held in hand by each player
17
+ attr_reader :hands
18
+
19
+ # @return [Styles] Game styles and active player indicator
20
+ attr_reader :styles
18
21
 
22
+ # Create a new immutable Position object.
23
+ #
24
+ # @param placement [Placement] Board configuration
25
+ # @param hands [Hands] Captured pieces in hand
26
+ # @param styles [Styles] Style-turn information
27
+ #
28
+ # @example Create a chess starting position
29
+ # position = Position.new(placement, hands, styles)
30
+ #
31
+ # @example Parse from FEEN string
32
+ # position = Sashite::Feen.parse("+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c")
33
+ def initialize(placement, hands, styles)
19
34
  @placement = placement
20
- @hands = hands
21
- @styles = styles
35
+ @hands = hands
36
+ @styles = styles
37
+
22
38
  freeze
23
39
  end
40
+
41
+ # Convert position to its canonical FEEN string representation.
42
+ #
43
+ # Generates a deterministic FEEN string. The same position will
44
+ # always produce the same canonical string, enabling position
45
+ # equality via string comparison.
46
+ #
47
+ # @return [String] Canonical FEEN notation string
48
+ #
49
+ # @example
50
+ # position.to_s
51
+ # # => "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c"
52
+ def to_s
53
+ Dumper.dump(self)
54
+ end
55
+
56
+ # Compare two positions for equality.
57
+ #
58
+ # @param other [Position] Another position object
59
+ # @return [Boolean] True if all components are equal
60
+ def ==(other)
61
+ other.is_a?(Position) &&
62
+ placement == other.placement &&
63
+ hands == other.hands &&
64
+ styles == other.styles
65
+ end
66
+
67
+ alias eql? ==
68
+
69
+ # Generate hash code for position.
70
+ #
71
+ # @return [Integer] Hash code based on all components
72
+ def hash
73
+ [placement, hands, styles].hash
74
+ end
24
75
  end
25
76
  end
26
77
  end
@@ -1,74 +1,71 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sashite/sin"
4
-
5
3
  module Sashite
6
4
  module Feen
7
- # Immutable styles descriptor for FEEN style/turn field:
8
- # - first_family : one-letter SIN family (Symbol, :A..:Z)
9
- # - second_family : one-letter SIN family (Symbol, :A..:Z)
10
- # - turn : :first or :second (uppercase on dumper for the side to move)
5
+ # Immutable representation of game styles and active player.
6
+ #
7
+ # Stores the style identifiers (SIN) for both players, with the active
8
+ # player's style indicating whose turn it is to move. The case of each
9
+ # style identifier indicates which player uses it (uppercase = first player,
10
+ # lowercase = second player).
11
+ #
12
+ # @see https://sashite.dev/specs/feen/1.0.0/
13
+ # @see https://sashite.dev/specs/sin/1.0.0/
11
14
  class Styles
12
- attr_reader :first_family, :second_family, :turn
13
-
14
- VALID_TURNS = %i[first second].freeze
15
+ # @return [Object] Style identifier of the active player (to move)
16
+ attr_reader :active
17
+
18
+ # @return [Object] Style identifier of the inactive player (waiting)
19
+ attr_reader :inactive
20
+
21
+ # Create a new immutable Styles object.
22
+ #
23
+ # @param active [Object] SIN identifier for active player's style
24
+ # @param inactive [Object] SIN identifier for inactive player's style
25
+ #
26
+ # @example Chess game, white to move
27
+ # styles = Styles.new(sin_C, sin_c)
28
+ #
29
+ # @example Chess game, black to move
30
+ # styles = Styles.new(sin_c, sin_C)
31
+ #
32
+ # @example Cross-style game, first player to move
33
+ # styles = Styles.new(sin_C, sin_m)
34
+ def initialize(active, inactive)
35
+ @active = active
36
+ @inactive = inactive
15
37
 
16
- # @param first_family [Symbol, String, Sashite::Sin::Identifier]
17
- # @param second_family [Symbol, String, Sashite::Sin::Identifier]
18
- # @param turn [:first, :second]
19
- def initialize(first_family, second_family, turn)
20
- @first_family = _coerce_family(first_family)
21
- @second_family = _coerce_family(second_family)
22
- @turn = _coerce_turn(turn)
23
38
  freeze
24
39
  end
25
40
 
26
- # Helpers for dumper -----------------------------------------------------
27
-
28
- # Return single-letter uppercase string for first/second family
29
- def first_letter_uc
30
- _family_letter_uc(@first_family)
31
- end
32
-
33
- def second_letter_uc
34
- _family_letter_uc(@second_family)
41
+ # Convert styles to their FEEN string representation.
42
+ #
43
+ # @return [String] FEEN style-turn field
44
+ #
45
+ # @example
46
+ # styles.to_s
47
+ # # => "C/c"
48
+ def to_s
49
+ Dumper::StyleTurn.dump(self)
35
50
  end
36
51
 
37
- private
38
-
39
- def _coerce_turn(t)
40
- raise ArgumentError, "turn must be :first or :second, got #{t.inspect}" unless VALID_TURNS.include?(t)
41
-
42
- t
52
+ # Compare two styles for equality.
53
+ #
54
+ # @param other [Styles] Another styles object
55
+ # @return [Boolean] True if active and inactive styles are equal
56
+ def ==(other)
57
+ other.is_a?(Styles) &&
58
+ active == other.active &&
59
+ inactive == other.inactive
43
60
  end
44
61
 
45
- # Accepts SIN Identifier, Symbol, or String
46
- # Canonical storage is a Symbol in :A..:Z (uppercase)
47
- def _coerce_family(x)
48
- family_sym =
49
- case x
50
- when ::Sashite::Sin::Identifier
51
- x.family
52
- when Symbol
53
- x
54
- else
55
- s = String(x)
56
- raise ArgumentError, "invalid SIN family #{x.inspect}" unless s.match?(/\A[A-Za-z]\z/)
57
-
58
- s.upcase.to_sym
59
- end
60
-
61
- raise ArgumentError, "Family must be :A..:Z, got #{family_sym.inspect}" unless (:A..:Z).cover?(family_sym)
62
-
63
- # Validate via SIN once (ensures family is recognized by sashite-sin)
64
- raise Error::Style, "Unknown SIN family #{family_sym.inspect}" unless ::Sashite::Sin.valid?(family_sym.to_s)
65
-
66
- family_sym
67
- end
62
+ alias eql? ==
68
63
 
69
- def _family_letter_uc(family_sym)
70
- # Build a canonical SIN identifier to get the letter; side doesn't matter for uc
71
- ::Sashite::Sin.identifier(family_sym, :first).to_s # uppercase
64
+ # Generate hash code for styles.
65
+ #
66
+ # @return [Integer] Hash code based on active and inactive styles
67
+ def hash
68
+ [active, inactive].hash
72
69
  end
73
70
  end
74
71
  end
data/lib/sashite/feen.rb CHANGED
@@ -1,106 +1,67 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Public API for FEEN (Forsyth–Edwards Enhanced Notation)
4
- # - Pure functions, no global state
5
- # - Immutable value objects
6
- # - Delegates parsing/dumping to dedicated components
7
-
8
- require "sashite/epin"
9
- require "sashite/sin"
10
-
11
- require_relative "feen/error"
12
- require_relative "feen/position"
13
- require_relative "feen/placement"
14
- require_relative "feen/hands"
15
- require_relative "feen/styles"
16
- require_relative "feen/ordering"
17
- require_relative "feen/parser"
18
3
  require_relative "feen/dumper"
4
+ require_relative "feen/parser"
19
5
 
20
6
  module Sashite
7
+ # FEEN (Forsyth–Edwards Enhanced Notation) module provides parsing and dumping
8
+ # functionality for board game positions.
9
+ #
10
+ # FEEN is a universal, rule-agnostic notation for representing board game positions.
11
+ # It extends traditional FEN to support multiple game systems, cross-style games,
12
+ # multi-dimensional boards, and captured pieces.
13
+ #
14
+ # A FEEN string consists of three space-separated fields:
15
+ # 1. Piece placement: Board configuration using EPIN notation
16
+ # 2. Pieces in hand: Captured pieces held by each player
17
+ # 3. Style-turn: Game styles and active player
18
+ #
19
+ # @see https://sashite.dev/specs/feen/1.0.0/
21
20
  module Feen
22
- class << self
23
- # Parse a FEEN string into an immutable Position object.
24
- #
25
- # @param feen [String]
26
- # @return [Sashite::Feen::Position]
27
- def parse(feen)
28
- s = String(feen).strip
29
- raise Error::Syntax, "empty FEEN input" if s.empty?
30
-
31
- Parser.parse(s).freeze
32
- rescue ArgumentError => e
33
- # Normalise en Syntax pour surface d'erreurs plus propre
34
- raise Error::Syntax, e.message
35
- end
36
-
37
- # Validate a FEEN string.
38
- #
39
- # @param feen [String]
40
- # @return [Boolean]
41
- def valid?(feen)
42
- parse(feen)
43
- true
44
- rescue Error::Validation, Error::Syntax, Error::Piece, Error::Style, Error::Count, Error::Bounds
45
- false
46
- end
47
-
48
- # Dump a Position to its canonical FEEN string.
49
- #
50
- # @param position [Sashite::Feen::Position, String] # String => parse puis dump
51
- # @return [String] canonical FEEN
52
- def dump(position)
53
- pos = _coerce_position(position)
54
- Dumper.dump(pos).dup.freeze
55
- end
56
-
57
- # Canonicalize a FEEN string (parse → dump).
58
- #
59
- # @param feen [String]
60
- # @return [String] canonical FEEN
61
- def normalize(feen)
62
- dump(parse(feen))
63
- end
64
-
65
- # Build a Position from its three FEEN fields.
66
- #
67
- # Each argument accepts either a String (parsed by its field parser)
68
- # or the corresponding value-object (Placement/Hands/Styles).
69
- #
70
- # @param piece_placement [String, Sashite::Feen::Placement]
71
- # @param pieces_in_hand [String, Sashite::Feen::Hands]
72
- # @param style_turn [String, Sashite::Feen::Styles]
73
- # @return [Sashite::Feen::Position]
74
- def build(piece_placement:, pieces_in_hand:, style_turn:)
75
- placement = _coerce_component(Placement, Parser::PiecePlacement, piece_placement)
76
- hands = _coerce_component(Hands, Parser::PiecesInHand, pieces_in_hand)
77
- styles = _coerce_component(Styles, Parser::StyleTurn, style_turn)
78
-
79
- Position.new(placement, hands, styles).freeze
80
- end
81
-
82
- private
83
-
84
- # -- helpers -------------------------------------------------------------
85
-
86
- def _coerce_position(obj)
87
- return obj if obj.is_a?(Position)
88
- return parse(obj) if obj.is_a?(String)
89
-
90
- raise TypeError, "expected Sashite::Feen::Position or FEEN String, got #{obj.class}"
91
- end
21
+ # Dump a Position object into its canonical FEEN string representation.
22
+ #
23
+ # Generates a deterministic FEEN string from a position object. The same
24
+ # position will always produce the same canonical string, ensuring
25
+ # position equality can be tested via string comparison.
26
+ #
27
+ # @param position [Position] A position object with placement, hands, and styles
28
+ # @return [String] Canonical FEEN notation string
29
+ #
30
+ # @example Dump a position to FEEN
31
+ # feen_string = Sashite::Feen.dump(position)
32
+ # # => "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c"
33
+ #
34
+ # @example Round-trip parsing and dumping
35
+ # original = "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c"
36
+ # position = Sashite::Feen.parse(original)
37
+ # Sashite::Feen.dump(position) == original # => true
38
+ def self.dump(position)
39
+ Dumper.dump(position)
40
+ end
92
41
 
93
- def _coerce_component(klass, field_parser_mod, value)
94
- case value
95
- when klass
96
- value
97
- when String
98
- field_parser_mod.parse(value)
99
- else
100
- raise TypeError,
101
- "expected #{klass} or String for #{klass.name.split('::').last.downcase}, got #{value.class}"
102
- end
103
- end
42
+ # Parse a FEEN string into an immutable Position object.
43
+ #
44
+ # This method parses the three FEEN fields and constructs an immutable position
45
+ # object with placement, hands, and styles components.
46
+ #
47
+ # @param string [String] A FEEN notation string with three space-separated fields
48
+ # @return [Position] Immutable position object
49
+ # @raise [Error::Syntax] If the FEEN structure is malformed
50
+ # @raise [Error::Piece] If EPIN notation is invalid
51
+ # @raise [Error::Style] If SIN notation is invalid
52
+ # @raise [Error::Count] If piece counts are invalid
53
+ # @raise [Error::Validation] For other semantic violations
54
+ #
55
+ # @example Parse a chess starting position
56
+ # position = Sashite::Feen.parse("+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c")
57
+ # position.placement # => Placement object (board configuration)
58
+ # position.hands # => Hands object (pieces in hand)
59
+ # position.styles # => Styles object (style-turn information)
60
+ #
61
+ # @example Parse a shogi position with captured pieces
62
+ # position = Sashite::Feen.parse("lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL P/p S/s")
63
+ def self.parse(string)
64
+ Parser.parse(string)
104
65
  end
105
66
  end
106
67
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-feen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -62,7 +62,6 @@ files:
62
62
  - lib/sashite/feen/dumper/style_turn.rb
63
63
  - lib/sashite/feen/error.rb
64
64
  - lib/sashite/feen/hands.rb
65
- - lib/sashite/feen/ordering.rb
66
65
  - lib/sashite/feen/parser.rb
67
66
  - lib/sashite/feen/parser/piece_placement.rb
68
67
  - lib/sashite/feen/parser/pieces_in_hand.rb
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module Feen
5
- # Deterministic ordering helpers (kept minimal for now).
6
- # If you later need domain-specific sort (e.g., EPIN-aware), centralize it here.
7
- module Ordering
8
- module_function
9
-
10
- # Default lexicographic sort key for serialized EPIN tokens (String)
11
- def hand_token_key(token_str)
12
- String(token_str)
13
- end
14
- end
15
- end
16
- end