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.
- checksums.yaml +4 -4
- data/README.md +64 -183
- data/lib/sashite/feen/dumper/piece_placement.rb +86 -63
- data/lib/sashite/feen/dumper/pieces_in_hand.rb +124 -35
- data/lib/sashite/feen/dumper/style_turn.rb +29 -45
- data/lib/sashite/feen/dumper.rb +40 -30
- data/lib/sashite/feen/error.rb +72 -29
- data/lib/sashite/feen/hands.rb +62 -20
- data/lib/sashite/feen/parser/piece_placement.rb +302 -110
- data/lib/sashite/feen/parser/pieces_in_hand.rb +216 -44
- data/lib/sashite/feen/parser/style_turn.rb +81 -41
- data/lib/sashite/feen/parser.rb +52 -16
- data/lib/sashite/feen/placement.rb +67 -21
- data/lib/sashite/feen/position.rb +64 -13
- data/lib/sashite/feen/styles.rb +54 -57
- data/lib/sashite/feen.rb +57 -96
- metadata +1 -2
- data/lib/sashite/feen/ordering.rb +0 -16
|
@@ -2,25 +2,76 @@
|
|
|
2
2
|
|
|
3
3
|
module Sashite
|
|
4
4
|
module Feen
|
|
5
|
-
# Immutable
|
|
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
|
-
|
|
13
|
+
# @return [Placement] Board piece placement configuration
|
|
14
|
+
attr_reader :placement
|
|
8
15
|
|
|
9
|
-
# @
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
21
|
-
@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
|
data/lib/sashite/feen/styles.rb
CHANGED
|
@@ -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
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
#
|
|
27
|
-
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
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
|