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
|
@@ -3,56 +3,40 @@
|
|
|
3
3
|
module Sashite
|
|
4
4
|
module Feen
|
|
5
5
|
module Dumper
|
|
6
|
+
# Dumper for the style-turn field (third field of FEEN).
|
|
7
|
+
#
|
|
8
|
+
# Converts a Styles object into its FEEN string representation,
|
|
9
|
+
# encoding game styles and indicating the active player.
|
|
10
|
+
#
|
|
11
|
+
# @see https://sashite.dev/specs/feen/1.0.0/
|
|
6
12
|
module StyleTurn
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
# Separator between multiple style tokens
|
|
10
|
-
STYLES_SEPARATOR = ","
|
|
13
|
+
# Style separator in style-turn field.
|
|
14
|
+
STYLE_SEPARATOR = "/"
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# Dump the style/turn field (e.g., "w", "b;rule1,variantX")
|
|
16
|
+
# Dump a Styles object into its FEEN style-turn string.
|
|
15
17
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
+
# Formats the active and inactive player styles with the active
|
|
19
|
+
# player's style appearing first. The case of each style identifier
|
|
20
|
+
# indicates which player uses it (uppercase = first player,
|
|
21
|
+
# lowercase = second player).
|
|
18
22
|
#
|
|
19
|
-
# @param styles [
|
|
20
|
-
# @return [String]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"#{turn_str}#{TURN_STYLES_SEPARATOR}#{tokens.join(STYLES_SEPARATOR)}"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# -- internals ---------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
def _dump_turn(turn)
|
|
41
|
-
case turn
|
|
42
|
-
when :first then "w"
|
|
43
|
-
when :second then "b"
|
|
44
|
-
else
|
|
45
|
-
raise Error::Style, "invalid turn symbol #{turn.inspect}"
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
private_class_method :_dump_turn
|
|
49
|
-
|
|
50
|
-
def _coerce_styles(obj)
|
|
51
|
-
return obj if obj.is_a?(Styles)
|
|
52
|
-
|
|
53
|
-
raise TypeError, "expected Sashite::Feen::Styles, got #{obj.class}"
|
|
23
|
+
# @param styles [Styles] The styles object with active and inactive styles
|
|
24
|
+
# @return [String] FEEN style-turn field string
|
|
25
|
+
#
|
|
26
|
+
# @example Chess game, white to move
|
|
27
|
+
# dump(styles)
|
|
28
|
+
# # => "C/c"
|
|
29
|
+
#
|
|
30
|
+
# @example Chess game, black to move
|
|
31
|
+
# dump(styles)
|
|
32
|
+
# # => "c/C"
|
|
33
|
+
#
|
|
34
|
+
# @example Cross-style game, first player to move
|
|
35
|
+
# dump(styles)
|
|
36
|
+
# # => "C/m"
|
|
37
|
+
def self.dump(styles)
|
|
38
|
+
"#{styles.active}#{STYLE_SEPARATOR}#{styles.inactive}"
|
|
54
39
|
end
|
|
55
|
-
private_class_method :_coerce_styles
|
|
56
40
|
end
|
|
57
41
|
end
|
|
58
42
|
end
|
data/lib/sashite/feen/dumper.rb
CHANGED
|
@@ -1,49 +1,59 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# FEEN Dumper (entry point)
|
|
4
|
-
# -------------------------
|
|
5
|
-
# Serializes a Position object into its canonical FEEN string by delegating
|
|
6
|
-
# each field to its dedicated sub-dumper.
|
|
7
|
-
#
|
|
8
|
-
# Sub-dumpers:
|
|
9
|
-
# dumper/piece_placement.rb
|
|
10
|
-
# dumper/pieces_in_hand.rb
|
|
11
|
-
# dumper/style_turn.rb
|
|
12
|
-
|
|
13
3
|
require_relative "dumper/piece_placement"
|
|
14
4
|
require_relative "dumper/pieces_in_hand"
|
|
15
5
|
require_relative "dumper/style_turn"
|
|
16
6
|
|
|
17
7
|
module Sashite
|
|
18
8
|
module Feen
|
|
9
|
+
# Dumper for FEEN (Forsyth–Edwards Enhanced Notation) positions.
|
|
10
|
+
#
|
|
11
|
+
# Converts a Position object into its canonical FEEN string representation
|
|
12
|
+
# by delegating serialization to specialized dumpers for each component.
|
|
13
|
+
#
|
|
14
|
+
# @see https://sashite.dev/specs/feen/1.0.0/
|
|
19
15
|
module Dumper
|
|
20
|
-
#
|
|
16
|
+
# Field separator in FEEN notation.
|
|
21
17
|
FIELD_SEPARATOR = " "
|
|
22
18
|
|
|
23
|
-
|
|
19
|
+
# Number of fields in a FEEN string.
|
|
20
|
+
FIELD_COUNT = 3
|
|
24
21
|
|
|
25
|
-
# Dump a Position into
|
|
22
|
+
# Dump a Position object into its canonical FEEN string representation.
|
|
26
23
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
# Generates a deterministic FEEN string from a position object. The same
|
|
25
|
+
# position will always produce the same canonical string.
|
|
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 = Dumper.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
|
+
def self.dump(position)
|
|
34
|
+
fields = [
|
|
35
|
+
Dumper::PiecePlacement.dump(position.placement),
|
|
36
|
+
Dumper::PiecesInHand.dump(position.hands),
|
|
37
|
+
Dumper::StyleTurn.dump(position.styles)
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
join_fields(fields)
|
|
37
41
|
end
|
|
38
42
|
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
# Join the three FEEN fields into a single string.
|
|
44
|
+
#
|
|
45
|
+
# Combines the piece placement, pieces in hand, and style-turn fields
|
|
46
|
+
# with the field separator.
|
|
47
|
+
#
|
|
48
|
+
# @param fields [Array<String>] Array of three field strings
|
|
49
|
+
# @return [String] Complete FEEN string
|
|
50
|
+
#
|
|
51
|
+
# @example Join three fields
|
|
52
|
+
# join_fields(["rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR", "/", "C/c"])
|
|
53
|
+
# # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c"
|
|
54
|
+
private_class_method def self.join_fields(fields)
|
|
55
|
+
fields.join(FIELD_SEPARATOR)
|
|
45
56
|
end
|
|
46
|
-
private_class_method :_coerce_position
|
|
47
57
|
end
|
|
48
58
|
end
|
|
49
59
|
end
|
data/lib/sashite/feen/error.rb
CHANGED
|
@@ -2,39 +2,82 @@
|
|
|
2
2
|
|
|
3
3
|
module Sashite
|
|
4
4
|
module Feen
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
#
|
|
22
|
-
|
|
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
|
-
#
|
|
25
|
-
|
|
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
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
data/lib/sashite/feen/hands.rb
CHANGED
|
@@ -2,36 +2,78 @@
|
|
|
2
2
|
|
|
3
3
|
module Sashite
|
|
4
4
|
module Feen
|
|
5
|
-
# Immutable
|
|
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
|
-
|
|
13
|
+
# @return [Array] Array of pieces held by first player
|
|
14
|
+
attr_reader :first_player
|
|
8
15
|
|
|
9
|
-
# @
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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.freeze
|
|
34
|
+
@second_player = second_player.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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|