feen 5.0.0.beta3 → 5.0.0.beta5
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 +57 -29
- data/lib/feen/dumper/piece_placement.rb +1 -1
- data/lib/feen/dumper.rb +4 -4
- data/lib/feen/parser/piece_placement.rb +4 -6
- data/lib/feen/parser/pieces_in_hand/errors.rb +2 -1
- data/lib/feen/parser/pieces_in_hand/piece_count_pattern.rb +13 -0
- data/lib/feen/parser/pieces_in_hand/valid_format_pattern.rb +21 -7
- data/lib/feen/parser/pieces_in_hand.rb +69 -33
- data/lib/feen/parser.rb +28 -24
- data/lib/feen.rb +75 -22
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28b60c3d60fd3241172fee9e143c53e6dca04082a2efe7f0fa7559b85dc6eac0
|
4
|
+
data.tar.gz: a15999144de7bcd5637b6efca4ed1c4a8ae7d3757d6d583123dfaf44468d6d35
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 861669bea6101dba5eefcf6062b9e4e0b37152618e7ffa9c1ba963eb37b2f49cad804f4c1bbee31bd030bf4ddd9602563df7e54cc244384d37e1f45ec748c5ff
|
7
|
+
data.tar.gz: 6f00dcfc09fd378fde0fbe3cd36b078718eab1fa27e208d474637ef89853a7543be4027116d623d0e037c340f0deeae90ce337404db09c953faecba852f56ece
|
data/README.md
CHANGED
@@ -5,23 +5,25 @@
|
|
5
5
|

|
6
6
|
[](https://github.com/sashite/feen.rb/raw/main/LICENSE.md)
|
7
7
|
|
8
|
-
> **FEEN** (
|
8
|
+
> **FEEN** (Forsyth–Edwards Enhanced Notation) support for the Ruby language.
|
9
9
|
|
10
10
|
## What is FEEN?
|
11
11
|
|
12
|
-
FEEN (
|
12
|
+
FEEN (Forsyth–Edwards Enhanced Notation) is a compact, canonical, and rule-agnostic textual format for representing static board positions in two-player piece-placement games.
|
13
13
|
|
14
14
|
This gem implements the [FEEN Specification v1.0.0](https://sashite.dev/documents/feen/1.0.0/), providing a Ruby interface for:
|
15
|
+
|
15
16
|
- Representing positions from various games without knowledge of specific rules
|
16
17
|
- Supporting boards of arbitrary dimensions
|
17
18
|
- Encoding pieces in hand (as used in Shogi)
|
18
19
|
- Facilitating serialization and deserialization of positions
|
20
|
+
- Ensuring canonical representation for consistent data handling
|
19
21
|
|
20
22
|
## Installation
|
21
23
|
|
22
24
|
```ruby
|
23
25
|
# In your Gemfile
|
24
|
-
gem "feen", ">= 5.0.0.
|
26
|
+
gem "feen", ">= 5.0.0.beta5"
|
25
27
|
```
|
26
28
|
|
27
29
|
Or install manually:
|
@@ -47,26 +49,42 @@ Convert a FEEN string into a structured Ruby object:
|
|
47
49
|
```ruby
|
48
50
|
require "feen"
|
49
51
|
|
50
|
-
feen_string = "
|
52
|
+
feen_string = "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
|
51
53
|
position = Feen.parse(feen_string)
|
52
54
|
|
53
55
|
# Result is a hash:
|
54
56
|
# {
|
55
|
-
#
|
56
|
-
# ["r", "n", "b", "q", "k
|
57
|
+
# piece_placement: [
|
58
|
+
# ["r'", "n", "b", "q", "k", "b", "n", "r'"],
|
57
59
|
# ["p", "p", "p", "p", "p", "p", "p", "p"],
|
58
60
|
# ["", "", "", "", "", "", "", ""],
|
59
61
|
# ["", "", "", "", "", "", "", ""],
|
60
62
|
# ["", "", "", "", "", "", "", ""],
|
61
63
|
# ["", "", "", "", "", "", "", ""],
|
62
64
|
# ["P", "P", "P", "P", "P", "P", "P", "P"],
|
63
|
-
# ["R", "N", "B", "Q", "K
|
65
|
+
# ["R'", "N", "B", "Q", "K", "B", "N", "R'"]
|
64
66
|
# ],
|
65
|
-
#
|
66
|
-
# "
|
67
|
+
# pieces_in_hand: [],
|
68
|
+
# games_turn: ["CHESS", "chess"]
|
67
69
|
# }
|
68
70
|
```
|
69
71
|
|
72
|
+
### Safe Parsing
|
73
|
+
|
74
|
+
Parse a FEEN string without raising exceptions:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
require "feen"
|
78
|
+
|
79
|
+
# Valid FEEN string
|
80
|
+
result = Feen.safe_parse("r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess")
|
81
|
+
# => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
|
82
|
+
|
83
|
+
# Invalid FEEN string
|
84
|
+
result = Feen.safe_parse("invalid feen string")
|
85
|
+
# => nil
|
86
|
+
```
|
87
|
+
|
70
88
|
### Creating FEEN Strings
|
71
89
|
|
72
90
|
Convert position components to a FEEN string using named arguments:
|
@@ -76,14 +94,14 @@ require "feen"
|
|
76
94
|
|
77
95
|
# Representation of a chess board in initial position
|
78
96
|
piece_placement = [
|
79
|
-
["r", "n", "b", "q", "k
|
97
|
+
["r'", "n", "b", "q", "k", "b", "n", "r'"],
|
80
98
|
["p", "p", "p", "p", "p", "p", "p", "p"],
|
81
99
|
["", "", "", "", "", "", "", ""],
|
82
100
|
["", "", "", "", "", "", "", ""],
|
83
101
|
["", "", "", "", "", "", "", ""],
|
84
102
|
["", "", "", "", "", "", "", ""],
|
85
103
|
["P", "P", "P", "P", "P", "P", "P", "P"],
|
86
|
-
["R", "N", "B", "Q", "K
|
104
|
+
["R'", "N", "B", "Q", "K", "B", "N", "R'"]
|
87
105
|
]
|
88
106
|
|
89
107
|
result = Feen.dump(
|
@@ -91,23 +109,34 @@ result = Feen.dump(
|
|
91
109
|
games_turn: %w[CHESS chess],
|
92
110
|
pieces_in_hand: []
|
93
111
|
)
|
94
|
-
# => "
|
112
|
+
# => "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
|
95
113
|
```
|
96
114
|
|
97
115
|
### Validation
|
98
116
|
|
99
|
-
Check if a string is valid FEEN notation:
|
117
|
+
Check if a string is valid FEEN notation and in canonical form:
|
100
118
|
|
101
119
|
```ruby
|
102
120
|
require "feen"
|
103
121
|
|
104
|
-
|
122
|
+
# Canonical form
|
123
|
+
Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi")
|
105
124
|
# => true
|
106
125
|
|
126
|
+
# Invalid syntax
|
107
127
|
Feen.valid?("invalid feen string")
|
108
128
|
# => false
|
129
|
+
|
130
|
+
# Valid syntax but non-canonical form (pieces in hand not in lexicographic order)
|
131
|
+
Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gn2sl SHOGI/shogi")
|
132
|
+
# => false
|
109
133
|
```
|
110
134
|
|
135
|
+
The `valid?` method performs two levels of validation:
|
136
|
+
|
137
|
+
1. **Syntax check**: Verifies the string can be parsed as FEEN
|
138
|
+
2. **Canonicity check**: Ensures the string is in its canonical form through round-trip conversion
|
139
|
+
|
111
140
|
## Game Examples
|
112
141
|
|
113
142
|
As FEEN is rule-agnostic, it can represent positions from various board games. Here are some examples:
|
@@ -115,24 +144,26 @@ As FEEN is rule-agnostic, it can represent positions from various board games. H
|
|
115
144
|
### International Chess
|
116
145
|
|
117
146
|
```ruby
|
118
|
-
feen_string = "
|
147
|
+
feen_string = "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
|
119
148
|
```
|
120
149
|
|
121
150
|
In this initial chess position:
|
122
|
-
|
151
|
+
|
152
|
+
- The `'` suffixes on rooks indicate an intermediate state (which might represent castling rights in chess, though FEEN doesn't define this semantics)
|
123
153
|
- The third field `CHESS/chess` indicates it's the player with uppercase pieces' turn to move
|
124
154
|
|
125
155
|
### Shogi (Japanese Chess)
|
126
156
|
|
127
157
|
```ruby
|
128
|
-
feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL
|
158
|
+
feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi"
|
129
159
|
```
|
130
160
|
|
131
161
|
In this shogi position:
|
162
|
+
|
132
163
|
- The format supports promotions with the `+` prefix (e.g., `+P` for a promoted pawn)
|
133
164
|
- The notation allows for pieces in hand, indicated in the second field
|
134
165
|
- `SHOGI/shogi` indicates it's Sente's (Black's, uppercase) turn to move
|
135
|
-
- `
|
166
|
+
- `N5P2gln2s` shows the pieces in hand: Sente has a Knight (N) and 5 Pawns (5P), while Gote has 2 Golds (2g), a Lance (l), a Knight (n), and 2 Silvers (2s), all properly sorted in ASCII lexicographic order
|
136
167
|
|
137
168
|
### Makruk (Thai Chess)
|
138
169
|
|
@@ -149,6 +180,7 @@ feen_string = "rheagaehr/9/1c5c1/s1s1s1s1s/9/9/S1S1S1S1S/1C5C1/9/RHEAGAEHR - XIA
|
|
149
180
|
```
|
150
181
|
|
151
182
|
In this Xiangqi position:
|
183
|
+
|
152
184
|
- The representation uses single letters for the different pieces
|
153
185
|
- The format naturally adapts to the presence of a "river" (empty space in the middle)
|
154
186
|
|
@@ -185,19 +217,15 @@ result = Feen.dump(
|
|
185
217
|
|
186
218
|
FEEN supports prefixes and suffixes for pieces to denote various states or capabilities:
|
187
219
|
|
188
|
-
- **Prefix `+`**:
|
220
|
+
- **Prefix `+`**: Enhanced state
|
189
221
|
- Example in shogi: `+P` may represent a promoted pawn
|
190
222
|
|
191
|
-
- **
|
192
|
-
-
|
193
|
-
|
194
|
-
- **Suffix `<`**: May indicate left-side constraint
|
195
|
-
- Example in chess: `K<` may represent a king eligible for queenside castling only
|
196
|
-
- Example in chess: `P<` may represent a pawn that may be captured _en passant_ from the left
|
223
|
+
- **Prefix `-`**: Diminished state
|
224
|
+
- Could represent a piece with limited movement or other restrictions
|
197
225
|
|
198
|
-
- **Suffix
|
199
|
-
- Example in chess: `
|
200
|
-
- Example in chess: `P
|
226
|
+
- **Suffix `'`**: Intermediate state
|
227
|
+
- Example in chess: `R'` may represent a rook that has intermediate status (such as castling eligibility)
|
228
|
+
- Example in chess: `P'` may represent a pawn that may be captured _en passant_
|
201
229
|
|
202
230
|
These modifiers have no defined semantics in the FEEN specification itself but provide a flexible framework for representing piece-specific conditions while maintaining FEEN's rule-agnostic nature.
|
203
231
|
|
@@ -212,4 +240,4 @@ The [gem](https://rubygems.org/gems/feen) is available as open source under the
|
|
212
240
|
|
213
241
|
## About Sashité
|
214
242
|
|
215
|
-
This project is maintained by [Sashité](https://sashite.com/)
|
243
|
+
This project is maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
|
@@ -7,7 +7,7 @@ module Feen
|
|
7
7
|
#
|
8
8
|
# @param piece_placement [Array] Hierarchical array representing the board where:
|
9
9
|
# - Empty squares are represented by empty strings ("")
|
10
|
-
# - Pieces are represented by strings (e.g., "r", "
|
10
|
+
# - Pieces are represented by strings (e.g., "r", "R'", "+P")
|
11
11
|
# - Dimensions are represented by nested arrays
|
12
12
|
# @return [String] FEEN piece placement string
|
13
13
|
# @raise [ArgumentError] If the piece placement structure is invalid
|
data/lib/feen/dumper.rb
CHANGED
@@ -6,7 +6,7 @@ require_relative File.join("dumper", "pieces_in_hand")
|
|
6
6
|
|
7
7
|
module Feen
|
8
8
|
# Module responsible for converting internal data structures to FEEN notation strings.
|
9
|
-
# This implements the serialization part of the FEEN (
|
9
|
+
# This implements the serialization part of the FEEN (Forsyth–Edwards Enhanced Notation) format.
|
10
10
|
module Dumper
|
11
11
|
# Field separator used between the three main components of FEEN notation
|
12
12
|
FIELD_SEPARATOR = " "
|
@@ -23,19 +23,19 @@ module Feen
|
|
23
23
|
# @example Creating a FEEN string for chess initial position
|
24
24
|
# Feen::Dumper.dump(
|
25
25
|
# piece_placement: [
|
26
|
-
# ["r", "n", "b", "q", "k
|
26
|
+
# ["r'", "n", "b", "q", "k", "b", "n", "r'"],
|
27
27
|
# ["p", "p", "p", "p", "p", "p", "p", "p"],
|
28
28
|
# ["", "", "", "", "", "", "", ""],
|
29
29
|
# ["", "", "", "", "", "", "", ""],
|
30
30
|
# ["", "", "", "", "", "", "", ""],
|
31
31
|
# ["", "", "", "", "", "", "", ""],
|
32
32
|
# ["P", "P", "P", "P", "P", "P", "P", "P"],
|
33
|
-
# ["R", "N", "B", "Q", "K
|
33
|
+
# ["R'", "N", "B", "Q", "K", "B", "N", "R'"]
|
34
34
|
# ],
|
35
35
|
# pieces_in_hand: [],
|
36
36
|
# games_turn: ["CHESS", "chess"]
|
37
37
|
# )
|
38
|
-
# # => "
|
38
|
+
# # => "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
|
39
39
|
#
|
40
40
|
# @param piece_placement [Array] Board position data structure representing the spatial
|
41
41
|
# distribution of pieces across the board, where each cell
|
@@ -30,10 +30,8 @@ module Feen
|
|
30
30
|
VALID_PREFIXES = [PREFIX_PROMOTION, PREFIX_DIMINISHED].freeze
|
31
31
|
|
32
32
|
# Valid piece suffixes
|
33
|
-
|
34
|
-
|
35
|
-
SUFFIX_RIGHT = ">"
|
36
|
-
VALID_SUFFIXES = [SUFFIX_EQUALS, SUFFIX_LEFT, SUFFIX_RIGHT].freeze
|
33
|
+
SUFFIX_INTERMEDIATE = "'"
|
34
|
+
VALID_SUFFIXES = [SUFFIX_INTERMEDIATE].freeze
|
37
35
|
|
38
36
|
# Build validation pattern step by step to match BNF
|
39
37
|
# <letter> ::= <letter-lowercase> | <letter-uppercase>
|
@@ -42,8 +40,8 @@ module Feen
|
|
42
40
|
# <prefix> ::= "+" | "-"
|
43
41
|
PREFIX = "[+-]"
|
44
42
|
|
45
|
-
# <suffix> ::= "
|
46
|
-
SUFFIX = "[
|
43
|
+
# <suffix> ::= "'"
|
44
|
+
SUFFIX = "[']"
|
47
45
|
|
48
46
|
# <piece> ::= <letter> | <prefix> <letter> | <letter> <suffix> | <prefix> <letter> <suffix>
|
49
47
|
PIECE = "(?:#{PREFIX}?#{LETTER}#{SUFFIX}?)"
|
@@ -7,7 +7,8 @@ module Feen
|
|
7
7
|
Errors = {
|
8
8
|
invalid_type: "Pieces in hand must be a string, got %s",
|
9
9
|
empty_string: "Pieces in hand string cannot be empty",
|
10
|
-
invalid_format: "Invalid pieces in hand format: %s"
|
10
|
+
invalid_format: "Invalid pieces in hand format: %s",
|
11
|
+
sorting_error: "Pieces in hand must be in ASCII lexicographic order"
|
11
12
|
}.freeze
|
12
13
|
end
|
13
14
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Feen
|
4
|
+
module Parser
|
5
|
+
module PiecesInHand
|
6
|
+
# Regex to extract piece counts from pieces in hand string
|
7
|
+
# Matches either:
|
8
|
+
# - A single piece character with no count (e.g., "P")
|
9
|
+
# - A count followed by a piece character (e.g., "5P")
|
10
|
+
PieceCountPattern = /(?:([2-9]|\d{2,}))?([A-Za-z])/
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -3,13 +3,27 @@
|
|
3
3
|
module Feen
|
4
4
|
module Parser
|
5
5
|
module PiecesInHand
|
6
|
-
# Valid pattern for pieces in hand based on BNF
|
7
|
-
#
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
6
|
+
# Valid pattern for pieces in hand based on BNF specification.
|
7
|
+
#
|
8
|
+
# The FEEN format specifies these rules for numeric prefixes:
|
9
|
+
# - Cannot start with "0"
|
10
|
+
# - Cannot be exactly "1" (use the letter without prefix instead)
|
11
|
+
# - Can be 2-9 or any number with 2+ digits (10, 11, etc.)
|
12
|
+
#
|
13
|
+
# The pattern matches either:
|
14
|
+
# - A single digit from 2-9
|
15
|
+
# - OR any number with two or more digits (10, 11, 27, 103, etc.)
|
16
|
+
#
|
17
|
+
# @return [Regexp] Regular expression for validating pieces in hand format
|
18
|
+
ValidFormatPattern = /\A
|
19
|
+
(?:
|
20
|
+
-| # No pieces in hand
|
21
|
+
(?: # Or sequence of pieces
|
22
|
+
(?:(?:[2-9]|\d{2,})?[A-Z])* # Uppercase pieces (optional)
|
23
|
+
(?:(?:[2-9]|\d{2,})?[a-z])* # Lowercase pieces (optional)
|
24
|
+
)
|
25
|
+
)
|
26
|
+
\z/x
|
13
27
|
end
|
14
28
|
end
|
15
29
|
end
|
@@ -1,32 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative File.join("pieces_in_hand", "errors")
|
4
|
+
require_relative File.join("pieces_in_hand", "no_pieces")
|
5
|
+
require_relative File.join("pieces_in_hand", "piece_count_pattern")
|
6
|
+
require_relative File.join("pieces_in_hand", "valid_format_pattern")
|
7
|
+
|
3
8
|
module Feen
|
4
9
|
module Parser
|
5
10
|
# Handles parsing of the pieces in hand section of a FEEN string.
|
6
11
|
# Pieces in hand represent pieces available for dropping onto the board.
|
7
12
|
module PiecesInHand
|
8
|
-
# Character used to represent no pieces in hand
|
9
|
-
NO_PIECES = "-"
|
10
|
-
|
11
|
-
# Error messages for validation
|
12
|
-
ERRORS = {
|
13
|
-
invalid_type: "Pieces in hand must be a string, got %s",
|
14
|
-
empty_string: "Pieces in hand string cannot be empty",
|
15
|
-
invalid_format: "Invalid pieces in hand format: %s",
|
16
|
-
sorting_error: "Pieces in hand must be in ASCII lexicographic order"
|
17
|
-
}.freeze
|
18
|
-
|
19
|
-
# Valid pattern for pieces in hand based on BNF:
|
20
|
-
# <pieces-in-hand> ::= "-" | <piece> <pieces-in-hand>
|
21
|
-
# <piece> ::= [a-zA-Z]
|
22
|
-
VALID_FORMAT_PATTERN = /\A(?:-|[a-zA-Z]+)\z/
|
23
|
-
|
24
13
|
# Parses the pieces in hand section of a FEEN string.
|
25
14
|
#
|
26
15
|
# @param pieces_in_hand_str [String] FEEN pieces in hand string
|
27
16
|
# @return [Array<String>] Array of single-character piece identifiers in the
|
28
|
-
# format specified in the FEEN string (no prefixes or suffixes),
|
29
|
-
#
|
17
|
+
# format specified in the FEEN string (no prefixes or suffixes), expanded
|
18
|
+
# based on their counts and sorted in ASCII lexicographic order.
|
19
|
+
# Empty array if no pieces are in hand.
|
30
20
|
# @raise [ArgumentError] If the input string is invalid
|
31
21
|
#
|
32
22
|
# @example Parse no pieces in hand
|
@@ -34,18 +24,26 @@ module Feen
|
|
34
24
|
# # => []
|
35
25
|
#
|
36
26
|
# @example Parse multiple pieces in hand
|
37
|
-
# PiecesInHand.parse("
|
27
|
+
# PiecesInHand.parse("BN2Pb")
|
38
28
|
# # => ["B", "N", "P", "P", "b"]
|
29
|
+
#
|
30
|
+
# @example Parse pieces with counts
|
31
|
+
# PiecesInHand.parse("N5P2b")
|
32
|
+
# # => ["N", "P", "P", "P", "P", "P", "b", "b"]
|
39
33
|
def self.parse(pieces_in_hand_str)
|
34
|
+
# Validate input
|
40
35
|
validate_input_type(pieces_in_hand_str)
|
41
36
|
validate_format(pieces_in_hand_str)
|
42
37
|
|
43
|
-
|
38
|
+
# Handle the no-pieces case early
|
39
|
+
return [] if pieces_in_hand_str == NoPieces
|
44
40
|
|
45
|
-
pieces
|
46
|
-
|
41
|
+
# Extract pieces with their counts and validate the order
|
42
|
+
pieces_with_counts = extract_pieces_with_counts(pieces_in_hand_str)
|
43
|
+
validate_lexicographic_order(pieces_with_counts)
|
47
44
|
|
48
|
-
pieces
|
45
|
+
# Expand the pieces into an array and maintain lexicographic ordering
|
46
|
+
expand_pieces(pieces_with_counts)
|
49
47
|
end
|
50
48
|
|
51
49
|
# Validates that the input is a non-empty string.
|
@@ -54,30 +52,68 @@ module Feen
|
|
54
52
|
# @raise [ArgumentError] If input is not a string or is empty
|
55
53
|
# @return [void]
|
56
54
|
private_class_method def self.validate_input_type(str)
|
57
|
-
raise ::ArgumentError, format(
|
58
|
-
raise ::ArgumentError,
|
55
|
+
raise ::ArgumentError, format(Errors[:invalid_type], str.class) unless str.is_a?(::String)
|
56
|
+
raise ::ArgumentError, Errors[:empty_string] if str.empty?
|
59
57
|
end
|
60
58
|
|
61
|
-
# Validates that the input string matches the expected format.
|
59
|
+
# Validates that the input string matches the expected format according to FEEN specification.
|
62
60
|
#
|
63
61
|
# @param str [String] Input string to validate
|
64
62
|
# @raise [ArgumentError] If format is invalid
|
65
63
|
# @return [void]
|
66
64
|
private_class_method def self.validate_format(str)
|
67
|
-
return if str.match?(
|
65
|
+
return if str == NoPieces || str.match?(ValidFormatPattern)
|
68
66
|
|
69
|
-
raise ::ArgumentError, format(
|
67
|
+
raise ::ArgumentError, format(Errors[:invalid_format], str)
|
70
68
|
end
|
71
69
|
|
72
|
-
#
|
70
|
+
# Extracts pieces with their counts from the FEEN string.
|
73
71
|
#
|
74
|
-
# @param
|
75
|
-
# @
|
72
|
+
# @param str [String] FEEN pieces in hand string
|
73
|
+
# @return [Array<Hash>] Array of hashes with :piece and :count keys
|
74
|
+
private_class_method def self.extract_pieces_with_counts(str)
|
75
|
+
result = []
|
76
|
+
position = 0
|
77
|
+
|
78
|
+
while position < str.length
|
79
|
+
match = str[position..].match(PieceCountPattern)
|
80
|
+
break unless match
|
81
|
+
|
82
|
+
count_str, piece = match.captures
|
83
|
+
count = count_str ? count_str.to_i : 1
|
84
|
+
|
85
|
+
# Add to our result with piece type and count
|
86
|
+
result << { piece: piece, count: count }
|
87
|
+
|
88
|
+
# Move position forward
|
89
|
+
position += match[0].length
|
90
|
+
end
|
91
|
+
|
92
|
+
result
|
93
|
+
end
|
94
|
+
|
95
|
+
# Validates that pieces are in lexicographic ASCII order.
|
96
|
+
#
|
97
|
+
# @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
|
98
|
+
# @raise [ArgumentError] If pieces are not in lexicographic order
|
76
99
|
# @return [void]
|
77
|
-
private_class_method def self.
|
100
|
+
private_class_method def self.validate_lexicographic_order(pieces_with_counts)
|
101
|
+
pieces = pieces_with_counts.map { |item| item[:piece] }
|
102
|
+
|
103
|
+
# Verify the array is sorted lexicographically
|
78
104
|
return if pieces == pieces.sort
|
79
105
|
|
80
|
-
raise ::ArgumentError,
|
106
|
+
raise ::ArgumentError, Errors[:sorting_error]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Expands the pieces based on their counts into an array.
|
110
|
+
#
|
111
|
+
# @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
|
112
|
+
# @return [Array<String>] Array of expanded pieces
|
113
|
+
private_class_method def self.expand_pieces(pieces_with_counts)
|
114
|
+
pieces_with_counts.flat_map do |item|
|
115
|
+
Array.new(item[:count], item[:piece])
|
116
|
+
end
|
81
117
|
end
|
82
118
|
end
|
83
119
|
end
|
data/lib/feen/parser.rb
CHANGED
@@ -6,7 +6,7 @@ require_relative File.join("parser", "pieces_in_hand")
|
|
6
6
|
|
7
7
|
module Feen
|
8
8
|
# Module responsible for parsing FEEN notation strings into internal data structures.
|
9
|
-
# FEEN (
|
9
|
+
# FEEN (Forsyth–Edwards Enhanced Notation) is a compact, canonical, and rule-agnostic
|
10
10
|
# textual format for representing static board positions in two-player piece-placement games.
|
11
11
|
module Parser
|
12
12
|
# Regular expression pattern for matching a valid FEEN string structure
|
@@ -23,44 +23,25 @@ module Feen
|
|
23
23
|
# - :piece_placement [Array] - Hierarchical array structure representing the board
|
24
24
|
# - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board
|
25
25
|
# - :games_turn [Array<String>] - A two-element array with [active_variant, inactive_variant]
|
26
|
-
# @raise [ArgumentError] If the FEEN string is invalid
|
26
|
+
# @raise [ArgumentError] If the FEEN string is invalid
|
27
27
|
#
|
28
28
|
# @example Parsing a standard chess initial position
|
29
|
-
# feen = "
|
29
|
+
# feen = "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
|
30
30
|
# result = Feen::Parser.parse(feen)
|
31
31
|
# # => {
|
32
32
|
# # piece_placement: [
|
33
|
-
# # ["r", "n", "b", "q", "k
|
33
|
+
# # ["r'", "n", "b", "q", "k", "b", "n", "r'"],
|
34
34
|
# # ["p", "p", "p", "p", "p", "p", "p", "p"],
|
35
35
|
# # ["", "", "", "", "", "", "", ""],
|
36
36
|
# # ["", "", "", "", "", "", "", ""],
|
37
37
|
# # ["", "", "", "", "", "", "", ""],
|
38
38
|
# # ["", "", "", "", "", "", "", ""],
|
39
39
|
# # ["P", "P", "P", "P", "P", "P", "P", "P"],
|
40
|
-
# # ["R", "N", "B", "Q", "K
|
40
|
+
# # ["R'", "N", "B", "Q", "K", "B", "N", "R'"]
|
41
41
|
# # ],
|
42
42
|
# # pieces_in_hand: [],
|
43
43
|
# # games_turn: ["CHESS", "chess"]
|
44
44
|
# # }
|
45
|
-
#
|
46
|
-
# @example Parsing a shogi position (from a Tempo Loss Bishop Exchange opening) with pieces in hand
|
47
|
-
# feen = "lnsgk2nl/1r4gs1/p1pppp1pp/1p4p2/7P1/2P6/PP1PPPP1P/1SG4R1/LN2KGSNL Bb SHOGI/shogi"
|
48
|
-
# result = Feen::Parser.parse(feen)
|
49
|
-
# # => {
|
50
|
-
# # piece_placement: [
|
51
|
-
# # ["l", "n", "s", "g", "k", "", "", "n", "l"],
|
52
|
-
# # ["", "r", "", "", "", "", "g", "s", ""],
|
53
|
-
# # ["p", "", "p", "p", "p", "p", "", "p", "p"],
|
54
|
-
# # ["", "p", "", "", "", "", "p", "", ""],
|
55
|
-
# # ["", "", "", "", "", "", "", "P", ""],
|
56
|
-
# # ["", "", "P", "", "", "", "", "", ""],
|
57
|
-
# # ["P", "P", "", "P", "P", "P", "P", "", "P"],
|
58
|
-
# # ["", "S", "G", "", "", "", "", "R", ""],
|
59
|
-
# # ["L", "N", "", "", "K", "G", "S", "N", "L"]
|
60
|
-
# # ],
|
61
|
-
# # pieces_in_hand: ["B", "b"],
|
62
|
-
# # games_turn: ["SHOGI", "shogi"]
|
63
|
-
# # }
|
64
45
|
def self.parse(feen_string)
|
65
46
|
feen_string = String(feen_string)
|
66
47
|
|
@@ -85,5 +66,28 @@ module Feen
|
|
85
66
|
games_turn:
|
86
67
|
}
|
87
68
|
end
|
69
|
+
|
70
|
+
# Safely parses a complete FEEN string into a structured representation without raising exceptions
|
71
|
+
#
|
72
|
+
# This method works like `parse` but returns nil instead of raising an exception
|
73
|
+
# if the FEEN string is invalid.
|
74
|
+
#
|
75
|
+
# @param feen_string [String] Complete FEEN notation string
|
76
|
+
# @return [Hash, nil] Hash containing the parsed position data or nil if parsing fails
|
77
|
+
#
|
78
|
+
# @example Parsing a valid FEEN string
|
79
|
+
# feen = "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
|
80
|
+
# result = Feen::Parser.safe_parse(feen)
|
81
|
+
# # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
|
82
|
+
#
|
83
|
+
# @example Parsing an invalid FEEN string
|
84
|
+
# feen = "invalid feen string"
|
85
|
+
# result = Feen::Parser.safe_parse(feen)
|
86
|
+
# # => nil
|
87
|
+
def self.safe_parse(feen_string)
|
88
|
+
parse(feen_string)
|
89
|
+
rescue ::ArgumentError
|
90
|
+
nil
|
91
|
+
end
|
88
92
|
end
|
89
93
|
end
|
data/lib/feen.rb
CHANGED
@@ -6,71 +6,124 @@ require_relative File.join("feen", "parser")
|
|
6
6
|
# This module provides a Ruby interface for data serialization and
|
7
7
|
# deserialization in FEEN format.
|
8
8
|
#
|
9
|
+
# FEEN (Forsyth–Edwards Enhanced Notation) is a compact, canonical, and
|
10
|
+
# rule-agnostic textual format for representing static board positions
|
11
|
+
# in two-player piece-placement games.
|
12
|
+
#
|
9
13
|
# @see https://sashite.dev/documents/feen/1.0.0/
|
10
14
|
module Feen
|
11
15
|
# Dumps position components into a FEEN string.
|
12
16
|
#
|
13
|
-
# @
|
17
|
+
# @param piece_placement [Array] Board position data structure representing the spatial
|
18
|
+
# distribution of pieces across the board
|
19
|
+
# @param pieces_in_hand [Array<String>] Pieces available for dropping onto the board
|
20
|
+
# @param games_turn [Array<String>] A two-element array where the first element is the
|
21
|
+
# active player's variant and the second is the inactive player's variant
|
14
22
|
# @return [String] FEEN notation string
|
15
23
|
# @raise [ArgumentError] If any parameter is invalid
|
16
24
|
# @example
|
17
25
|
# piece_placement = [
|
18
|
-
# ["r", "n", "b", "q", "k
|
26
|
+
# ["r'", "n", "b", "q", "k", "b", "n", "r'"],
|
19
27
|
# ["p", "p", "p", "p", "p", "p", "p", "p"],
|
20
28
|
# ["", "", "", "", "", "", "", ""],
|
21
29
|
# ["", "", "", "", "", "", "", ""],
|
22
30
|
# ["", "", "", "", "", "", "", ""],
|
23
31
|
# ["", "", "", "", "", "", "", ""],
|
24
32
|
# ["P", "P", "P", "P", "P", "P", "P", "P"],
|
25
|
-
# ["R", "N", "B", "Q", "K
|
33
|
+
# ["R'", "N", "B", "Q", "K", "B", "N", "R'"]
|
26
34
|
# ]
|
27
35
|
# Feen.dump(
|
28
36
|
# piece_placement: piece_placement,
|
29
37
|
# pieces_in_hand: [],
|
30
38
|
# games_turn: ["CHESS", "chess"]
|
31
39
|
# )
|
32
|
-
# # => "
|
33
|
-
def self.dump(
|
34
|
-
Dumper.dump(
|
40
|
+
# # => "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
|
41
|
+
def self.dump(piece_placement:, pieces_in_hand:, games_turn:)
|
42
|
+
Dumper.dump(piece_placement:, pieces_in_hand:, games_turn:)
|
35
43
|
end
|
36
44
|
|
37
45
|
# Parses a FEEN string into position components.
|
38
46
|
#
|
39
|
-
# @
|
40
|
-
# @return [Hash] Hash containing the parsed position data
|
47
|
+
# @param feen_string [String] Complete FEEN notation string
|
48
|
+
# @return [Hash] Hash containing the parsed position data with the following keys:
|
49
|
+
# - :piece_placement [Array] - Hierarchical array structure representing the board
|
50
|
+
# - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board
|
51
|
+
# - :games_turn [Array<String>] - A two-element array with [active_variant, inactive_variant]
|
41
52
|
# @raise [ArgumentError] If the FEEN string is invalid
|
42
53
|
# @example
|
43
|
-
# feen_string = "
|
54
|
+
# feen_string = "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
|
44
55
|
# Feen.parse(feen_string)
|
45
56
|
# # => {
|
46
57
|
# # piece_placement: [
|
47
|
-
# # ["r", "n", "b", "q", "k
|
58
|
+
# # ["r'", "n", "b", "q", "k", "b", "n", "r'"],
|
48
59
|
# # ["p", "p", "p", "p", "p", "p", "p", "p"],
|
49
60
|
# # ["", "", "", "", "", "", "", ""],
|
50
61
|
# # ["", "", "", "", "", "", "", ""],
|
51
62
|
# # ["", "", "", "", "", "", "", ""],
|
52
63
|
# # ["", "", "", "", "", "", "", ""],
|
53
64
|
# # ["P", "P", "P", "P", "P", "P", "P", "P"],
|
54
|
-
# # ["R", "N", "B", "Q", "K
|
65
|
+
# # ["R'", "N", "B", "Q", "K", "B", "N", "R'"]
|
55
66
|
# # ],
|
56
67
|
# # pieces_in_hand: [],
|
57
68
|
# # games_turn: ["CHESS", "chess"]
|
58
69
|
# # }
|
59
|
-
def self.parse(
|
60
|
-
Parser.parse(
|
70
|
+
def self.parse(feen_string)
|
71
|
+
Parser.parse(feen_string)
|
61
72
|
end
|
62
73
|
|
63
|
-
#
|
74
|
+
# Safely parses a FEEN string into position components without raising exceptions.
|
75
|
+
#
|
76
|
+
# This method works like `parse` but returns nil instead of raising an exception
|
77
|
+
# if the FEEN string is invalid.
|
64
78
|
#
|
65
|
-
# @
|
66
|
-
# @return [
|
79
|
+
# @param feen_string [String] Complete FEEN notation string
|
80
|
+
# @return [Hash, nil] Hash containing the parsed position data or nil if parsing fails
|
67
81
|
# @example
|
68
|
-
#
|
82
|
+
# # Valid FEEN string
|
83
|
+
# Feen.safe_parse("r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess")
|
84
|
+
# # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
|
85
|
+
#
|
86
|
+
# # Invalid FEEN string
|
87
|
+
# Feen.safe_parse("invalid feen string")
|
88
|
+
# # => nil
|
89
|
+
def self.safe_parse(feen_string)
|
90
|
+
Parser.safe_parse(feen_string)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Validates if the given string is a valid and canonical FEEN string
|
94
|
+
#
|
95
|
+
# This method performs a complete validation in two steps:
|
96
|
+
# 1. Syntax check: Verifies the string can be parsed as FEEN
|
97
|
+
# 2. Canonicity check: Ensures the string is in canonical form by comparing
|
98
|
+
# it with a freshly generated FEEN string created from its parsed components
|
99
|
+
#
|
100
|
+
# This approach guarantees that the string not only follows FEEN syntax
|
101
|
+
# but is also in its most compact, canonical representation.
|
102
|
+
#
|
103
|
+
# @param feen_string [String] FEEN string to validate
|
104
|
+
# @return [Boolean] True if the string is a valid and canonical FEEN string
|
105
|
+
# @example
|
106
|
+
# # Canonical form
|
107
|
+
# Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi") # => true
|
108
|
+
#
|
109
|
+
# # Invalid syntax
|
69
110
|
# Feen.valid?("invalid feen string") # => false
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
111
|
+
#
|
112
|
+
# # Valid syntax but non-canonical form (pieces in hand not in lexicographic order)
|
113
|
+
# Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gn2sl SHOGI/shogi") # => false
|
114
|
+
def self.valid?(feen_string)
|
115
|
+
# First check: Basic syntax validation
|
116
|
+
begin
|
117
|
+
parsed_data = parse(feen_string)
|
118
|
+
rescue ::ArgumentError
|
119
|
+
return false
|
120
|
+
end
|
121
|
+
|
122
|
+
# Second check: Canonicity validation through round-trip conversion
|
123
|
+
# Generate a fresh FEEN string from the parsed data
|
124
|
+
canonical_feen = dump(**parsed_data)
|
125
|
+
|
126
|
+
# Compare the original string with the canonical form
|
127
|
+
feen_string == canonical_feen
|
75
128
|
end
|
76
129
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: feen
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.0.0.
|
4
|
+
version: 5.0.0.beta5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -10,6 +10,9 @@ cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
12
|
description: A Ruby interface for data serialization and deserialization in FEEN format.
|
13
|
+
FEEN is a compact, canonical, and rule-agnostic textual format for representing
|
14
|
+
static board positions in two-player piece-placement games like Chess, Shogi, Xiangqi,
|
15
|
+
and others.
|
13
16
|
email: contact@cyril.email
|
14
17
|
executables: []
|
15
18
|
extensions: []
|
@@ -32,11 +35,17 @@ files:
|
|
32
35
|
- lib/feen/parser/pieces_in_hand.rb
|
33
36
|
- lib/feen/parser/pieces_in_hand/errors.rb
|
34
37
|
- lib/feen/parser/pieces_in_hand/no_pieces.rb
|
38
|
+
- lib/feen/parser/pieces_in_hand/piece_count_pattern.rb
|
35
39
|
- lib/feen/parser/pieces_in_hand/valid_format_pattern.rb
|
36
40
|
homepage: https://github.com/sashite/feen.rb
|
37
41
|
licenses:
|
38
42
|
- MIT
|
39
43
|
metadata:
|
44
|
+
bug_tracker_uri: https://github.com/sashite/feen.rb/issues
|
45
|
+
documentation_uri: https://rubydoc.info/github/sashite/feen.rb/main
|
46
|
+
homepage_uri: https://github.com/sashite/feen.rb
|
47
|
+
source_code_uri: https://github.com/sashite/feen.rb
|
48
|
+
specification_uri: https://sashite.dev/documents/feen/1.0.0/
|
40
49
|
rubygems_mfa_required: 'true'
|
41
50
|
rdoc_options: []
|
42
51
|
require_paths:
|
@@ -54,5 +63,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
63
|
requirements: []
|
55
64
|
rubygems_version: 3.6.7
|
56
65
|
specification_version: 4
|
57
|
-
summary: FEEN support for the Ruby language.
|
66
|
+
summary: FEEN (Forsyth–Edwards Enhanced Notation) support for the Ruby language.
|
58
67
|
test_files: []
|