feen 5.0.0.beta2 → 5.0.0.beta3
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 +107 -86
- data/lib/feen/dumper/games_turn.rb +40 -65
- data/lib/feen/dumper/piece_placement.rb +102 -89
- data/lib/feen/dumper/pieces_in_hand/errors.rb +12 -0
- data/lib/feen/dumper/pieces_in_hand/no_pieces.rb +10 -0
- data/lib/feen/dumper/pieces_in_hand.rb +57 -41
- data/lib/feen/dumper.rb +63 -32
- data/lib/feen/parser/games_turn/errors.rb +14 -0
- data/lib/feen/parser/games_turn/valid_games_turn_pattern.rb +24 -0
- data/lib/feen/parser/games_turn.rb +32 -110
- data/lib/feen/parser/piece_placement.rb +490 -77
- data/lib/feen/parser/pieces_in_hand/errors.rb +14 -0
- data/lib/feen/parser/pieces_in_hand/no_pieces.rb +10 -0
- data/lib/feen/parser/pieces_in_hand/valid_format_pattern.rb +15 -0
- data/lib/feen/parser/pieces_in_hand.rb +53 -44
- data/lib/feen/parser.rb +67 -30
- data/lib/feen.rb +42 -76
- metadata +9 -6
- data/lib/feen/converter/from_fen.rb +0 -170
- data/lib/feen/converter/to_fen.rb +0 -153
- data/lib/feen/converter.rb +0 -16
- data/lib/feen/sanitizer.rb +0 -119
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ad6a4426ae68ac5344888465a23eea36cf28b3e0137b50af8483b98288012969
|
4
|
+
data.tar.gz: f9f42840240c5b75629cdb5459f492f966d3fc2706a354c18abc9841a9f17e21
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 31141ae8b866a11ffec6eed44308b475d03e75174ec0bc12b14ff0dc60bfe8b21ad60af3d32e1747d7dce94fe0cff0bbd5add502f26c26aeffac97104fbeeaca
|
7
|
+
data.tar.gz: 82c82de3cf0024e596d9f318e612a8077069a9b519e6b4e753dc808bbee91045f72328489a26307f0877194a4e99ffc8502dd3d3d1610900215ab697fa479ce4
|
data/README.md
CHANGED
@@ -1,28 +1,27 @@
|
|
1
1
|
# Feen.rb
|
2
2
|
|
3
|
-
[](https://github.com/sashite/feen.rb/
|
3
|
+
[](https://github.com/sashite/feen.rb/tags)
|
4
4
|
[](https://rubydoc.info/github/sashite/feen.rb/main)
|
5
|
-
|
6
|
-
[](https://github.com/sashite/feen.rb/actions?query=workflow%3Arubocop+branch%3Amain)
|
5
|
+

|
7
6
|
[](https://github.com/sashite/feen.rb/raw/main/LICENSE.md)
|
8
7
|
|
9
|
-
> **FEEN** (
|
8
|
+
> **FEEN** (Format for Encounter & Entertainment Notation) support for the Ruby language.
|
10
9
|
|
11
10
|
## What is FEEN?
|
12
11
|
|
13
|
-
FEEN (
|
12
|
+
FEEN (Format for Encounter & Entertainment Notation) is a compact, canonical, and rule-agnostic textual format for representing static board positions in two-player piece-placement games.
|
14
13
|
|
15
14
|
This gem implements the [FEEN Specification v1.0.0](https://sashite.dev/documents/feen/1.0.0/), providing a Ruby interface for:
|
16
|
-
-
|
17
|
-
-
|
18
|
-
-
|
19
|
-
-
|
15
|
+
- Representing positions from various games without knowledge of specific rules
|
16
|
+
- Supporting boards of arbitrary dimensions
|
17
|
+
- Encoding pieces in hand (as used in Shogi)
|
18
|
+
- Facilitating serialization and deserialization of positions
|
20
19
|
|
21
20
|
## Installation
|
22
21
|
|
23
22
|
```ruby
|
24
23
|
# In your Gemfile
|
25
|
-
gem "feen", ">= 5.0.0.
|
24
|
+
gem "feen", ">= 5.0.0.beta3"
|
26
25
|
```
|
27
26
|
|
28
27
|
Or install manually:
|
@@ -36,7 +35,7 @@ gem install feen --pre
|
|
36
35
|
A FEEN record consists of three space-separated fields:
|
37
36
|
|
38
37
|
```
|
39
|
-
<PIECE-PLACEMENT> <
|
38
|
+
<PIECE-PLACEMENT> <PIECES-IN-HAND> <GAMES-TURN>
|
40
39
|
```
|
41
40
|
|
42
41
|
## Basic Usage
|
@@ -48,42 +47,51 @@ Convert a FEEN string into a structured Ruby object:
|
|
48
47
|
```ruby
|
49
48
|
require "feen"
|
50
49
|
|
51
|
-
feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR CHESS/chess
|
50
|
+
feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
52
51
|
position = Feen.parse(feen_string)
|
53
52
|
|
54
|
-
# Result is a hash
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
53
|
+
# Result is a hash:
|
54
|
+
# {
|
55
|
+
# "piece_placement" => [
|
56
|
+
# ["r", "n", "b", "q", "k=", "b", "n", "r"],
|
57
|
+
# ["p", "p", "p", "p", "p", "p", "p", "p"],
|
58
|
+
# ["", "", "", "", "", "", "", ""],
|
59
|
+
# ["", "", "", "", "", "", "", ""],
|
60
|
+
# ["", "", "", "", "", "", "", ""],
|
61
|
+
# ["", "", "", "", "", "", "", ""],
|
62
|
+
# ["P", "P", "P", "P", "P", "P", "P", "P"],
|
63
|
+
# ["R", "N", "B", "Q", "K=", "B", "N", "R"]
|
64
|
+
# ],
|
65
|
+
# "games_turn" => ["CHESS", "chess"],
|
66
|
+
# "pieces_in_hand" => []
|
67
|
+
# }
|
58
68
|
```
|
59
69
|
|
60
70
|
### Creating FEEN Strings
|
61
71
|
|
62
|
-
Convert
|
72
|
+
Convert position components to a FEEN string using named arguments:
|
63
73
|
|
64
74
|
```ruby
|
65
75
|
require "feen"
|
66
76
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
77
|
+
# Representation of a chess board in initial position
|
78
|
+
piece_placement = [
|
79
|
+
["r", "n", "b", "q", "k=", "b", "n", "r"],
|
80
|
+
["p", "p", "p", "p", "p", "p", "p", "p"],
|
81
|
+
["", "", "", "", "", "", "", ""],
|
82
|
+
["", "", "", "", "", "", "", ""],
|
83
|
+
["", "", "", "", "", "", "", ""],
|
84
|
+
["", "", "", "", "", "", "", ""],
|
85
|
+
["P", "P", "P", "P", "P", "P", "P", "P"],
|
86
|
+
["R", "N", "B", "Q", "K=", "B", "N", "R"]
|
87
|
+
]
|
88
|
+
|
89
|
+
result = Feen.dump(
|
90
|
+
piece_placement: piece_placement,
|
91
|
+
games_turn: %w[CHESS chess],
|
82
92
|
pieces_in_hand: []
|
83
|
-
|
84
|
-
|
85
|
-
Feen.dump(position)
|
86
|
-
# => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR CHESS/chess -"
|
93
|
+
)
|
94
|
+
# => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
87
95
|
```
|
88
96
|
|
89
97
|
### Validation
|
@@ -93,36 +101,56 @@ Check if a string is valid FEEN notation:
|
|
93
101
|
```ruby
|
94
102
|
require "feen"
|
95
103
|
|
96
|
-
Feen.valid?("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR CHESS/chess
|
104
|
+
Feen.valid?("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess")
|
97
105
|
# => true
|
98
106
|
|
99
107
|
Feen.valid?("invalid feen string")
|
100
108
|
# => false
|
101
109
|
```
|
102
110
|
|
103
|
-
##
|
111
|
+
## Game Examples
|
112
|
+
|
113
|
+
As FEEN is rule-agnostic, it can represent positions from various board games. Here are some examples:
|
104
114
|
|
105
|
-
###
|
115
|
+
### International Chess
|
106
116
|
|
107
117
|
```ruby
|
108
|
-
|
118
|
+
feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
119
|
+
```
|
109
120
|
|
110
|
-
|
111
|
-
|
112
|
-
|
121
|
+
In this initial chess position:
|
122
|
+
- The `=` suffixes on kings indicate castling rights on both sides (though FEEN doesn't define this semantics)
|
123
|
+
- The third field `CHESS/chess` indicates it's the player with uppercase pieces' turn to move
|
124
|
+
|
125
|
+
### Shogi (Japanese Chess)
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2g2snl SHOGI/shogi"
|
113
129
|
```
|
114
130
|
|
115
|
-
|
131
|
+
In this shogi position:
|
132
|
+
- The format supports promotions with the `+` prefix (e.g., `+P` for a promoted pawn)
|
133
|
+
- The notation allows for pieces in hand, indicated in the second field
|
134
|
+
- `SHOGI/shogi` indicates it's Sente's (Black's, uppercase) turn to move
|
135
|
+
- `N5P2g2snl` shows the pieces in hand: Sente has a Knight (N) and 5 Pawns (P), while Gote has 2 Golds (g), 2 Silvers (s), a Knight (n), and a Lance (l)
|
136
|
+
|
137
|
+
### Makruk (Thai Chess)
|
116
138
|
|
117
139
|
```ruby
|
118
|
-
|
140
|
+
feen_string = "rnbqkbnr/8/pppppppp/8/8/PPPPPPPP/8/RNBQKBNR - MAKRUK/makruk"
|
141
|
+
```
|
142
|
+
|
143
|
+
This initial Makruk position is easily represented in FEEN without needing to know the specific rules of the game.
|
144
|
+
|
145
|
+
### Xiangqi (Chinese Chess)
|
119
146
|
|
120
|
-
|
121
|
-
|
122
|
-
# => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
147
|
+
```ruby
|
148
|
+
feen_string = "rheagaehr/9/1c5c1/s1s1s1s1s/9/9/S1S1S1S1S/1C5C1/9/RHEAGAEHR - XIANGQI/xiangqi"
|
123
149
|
```
|
124
150
|
|
125
|
-
|
151
|
+
In this Xiangqi position:
|
152
|
+
- The representation uses single letters for the different pieces
|
153
|
+
- The format naturally adapts to the presence of a "river" (empty space in the middle)
|
126
154
|
|
127
155
|
## Advanced Features
|
128
156
|
|
@@ -134,61 +162,54 @@ FEEN supports arbitrary-dimensional board configurations:
|
|
134
162
|
require "feen"
|
135
163
|
|
136
164
|
# 3D board
|
137
|
-
|
138
|
-
|
139
|
-
[
|
140
|
-
|
141
|
-
[{ id: "q" }, { id: "k" }, { id: "p" }]
|
142
|
-
],
|
143
|
-
[
|
144
|
-
[{ id: "P" }, { id: "R" }, nil],
|
145
|
-
[nil, { id: "K" }, { id: "Q" }]
|
146
|
-
]
|
165
|
+
piece_placement = [
|
166
|
+
[
|
167
|
+
%w[r n b],
|
168
|
+
%w[q k p]
|
147
169
|
],
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
170
|
+
[
|
171
|
+
["P", "R", ""],
|
172
|
+
["", "K", "Q"]
|
173
|
+
]
|
174
|
+
]
|
175
|
+
|
176
|
+
result = Feen.dump(
|
177
|
+
piece_placement: piece_placement,
|
178
|
+
games_turn: %w[FOO bar],
|
152
179
|
pieces_in_hand: []
|
153
|
-
|
154
|
-
|
155
|
-
Feen.dump(position)
|
156
|
-
# => "rnb/qkp//PR1/1KQ CHESS/chess -"
|
180
|
+
)
|
181
|
+
# => "rnb/qkp//PR1/1KQ - FOO/bar"
|
157
182
|
```
|
158
183
|
|
159
184
|
### Piece Modifiers
|
160
185
|
|
161
|
-
FEEN supports prefixes and suffixes for pieces:
|
186
|
+
FEEN supports prefixes and suffixes for pieces to denote various states or capabilities:
|
162
187
|
|
163
|
-
- Prefix
|
164
|
-
-
|
165
|
-
- Suffix `<`: Left-side constraint (e.g., `K<` for queenside castling only)
|
166
|
-
- Suffix `>`: Right-side constraint (e.g., `K>` for kingside castling only)
|
188
|
+
- **Prefix `+`**: May indicate promotion or special state
|
189
|
+
- Example in shogi: `+P` may represent a promoted pawn
|
167
190
|
|
168
|
-
|
191
|
+
- **Suffix `=`**: May indicate dual-option status
|
192
|
+
- Example in chess: `K=` may represent a king eligible for both kingside and queenside castling
|
169
193
|
|
170
|
-
|
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
|
171
197
|
|
172
|
-
|
173
|
-
|
198
|
+
- **Suffix `>`**: May indicate right-side constraint
|
199
|
+
- Example in chess: `K>` may represent a king eligible for kingside castling only
|
200
|
+
- Example in chess: `P>` may represent a pawn that may be captured en passant from the right
|
174
201
|
|
175
|
-
|
176
|
-
fen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQ1BNR w KQkq - 0 1"
|
177
|
-
cleaned_fen = Feen::Sanitizer.clean_fen(fen_string)
|
178
|
-
# => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQ1BNR w kq - 0 1"
|
179
|
-
```
|
202
|
+
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.
|
180
203
|
|
181
204
|
## Documentation
|
182
205
|
|
183
|
-
- [Official FEEN Specification
|
206
|
+
- [Official FEEN Specification](https://sashite.dev/documents/feen/1.0.0/)
|
184
207
|
- [API Documentation](https://rubydoc.info/github/sashite/feen.rb/main)
|
185
208
|
|
186
209
|
## License
|
187
210
|
|
188
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
211
|
+
The [gem](https://rubygems.org/gems/feen) is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
189
212
|
|
190
213
|
## About Sashité
|
191
214
|
|
192
|
-
This
|
193
|
-
|
194
|
-
With some [lines of code](https://github.com/sashite/), let's share the beauty of Chinese, Japanese and Western cultures through the game of chess!
|
215
|
+
This project is maintained by [Sashité](https://sashite.com/) - a project dedicated to promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
|
@@ -2,90 +2,65 @@
|
|
2
2
|
|
3
3
|
module Feen
|
4
4
|
module Dumper
|
5
|
-
# Handles conversion of games turn data
|
5
|
+
# Handles conversion of games turn data to FEEN notation string
|
6
6
|
module GamesTurn
|
7
7
|
ERRORS = {
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
type: "%s must be a String, got %s",
|
9
|
+
empty: "%s cannot be empty",
|
10
|
+
mixed: "%s has mixed case: %s",
|
11
|
+
casing: "One variant must be uppercase and the other lowercase",
|
12
|
+
chars: "Variant identifiers must contain only alphabetic characters (a-z, A-Z)"
|
13
13
|
}.freeze
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
# Converts the internal games turn representation to a FEEN string
|
15
|
+
# Converts the active and inactive variant identifiers to a FEEN-formatted games turn string
|
18
16
|
#
|
19
|
-
# @param
|
17
|
+
# @param active_variant [String] Identifier for the player to move and their game variant
|
18
|
+
# @param inactive_variant [String] Identifier for the opponent and their game variant
|
20
19
|
# @return [String] FEEN-formatted games turn string
|
21
|
-
def self.dump(
|
22
|
-
|
23
|
-
|
24
|
-
# Format is <active_player>/<inactive_player>
|
25
|
-
"#{games_turn[:active_player]}/#{games_turn[:inactive_player]}"
|
26
|
-
end
|
27
|
-
|
28
|
-
# Validates the games turn data structure
|
29
|
-
#
|
30
|
-
# @param games_turn [Hash] The games turn data to validate
|
31
|
-
# @raise [ArgumentError] If the games turn data is invalid
|
32
|
-
# @return [Boolean] true if the validation passes
|
33
|
-
def self.validate_games_turn(games_turn)
|
34
|
-
validate_structure(games_turn)
|
35
|
-
validate_casing(games_turn)
|
36
|
-
validate_character_set(games_turn)
|
37
|
-
|
38
|
-
true
|
20
|
+
def self.dump(active_variant, inactive_variant)
|
21
|
+
validate_variants(active_variant, inactive_variant)
|
22
|
+
"#{active_variant}/#{inactive_variant}"
|
39
23
|
end
|
40
24
|
|
41
|
-
# Validates the
|
25
|
+
# Validates the game variant identifiers
|
42
26
|
#
|
43
|
-
# @param
|
44
|
-
# @
|
27
|
+
# @param active [String] The active player's variant identifier
|
28
|
+
# @param inactive [String] The inactive player's variant identifier
|
29
|
+
# @raise [ArgumentError] If the variant identifiers are invalid
|
45
30
|
# @return [void]
|
46
|
-
private_class_method def self.
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end
|
53
|
-
|
54
|
-
raise ArgumentError, format(ERRORS[:empty_string], key) if games_turn[key].empty?
|
31
|
+
private_class_method def self.validate_variants(active, inactive)
|
32
|
+
# Validate basic type and presence
|
33
|
+
[["Active variant", active], ["Inactive variant", inactive]].each do |name, variant|
|
34
|
+
raise ArgumentError, format(ERRORS[:type], name, variant.class) unless variant.is_a?(String)
|
35
|
+
raise ArgumentError, format(ERRORS[:empty], name) if variant.empty?
|
36
|
+
raise ArgumentError, ERRORS[:chars] unless variant.match?(/\A[a-zA-Z]+\z/)
|
55
37
|
end
|
56
|
-
end
|
57
38
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
# @raise [ArgumentError] If the casing requirement is not met
|
62
|
-
# @return [void]
|
63
|
-
private_class_method def self.validate_casing(games_turn)
|
64
|
-
active_has_uppercase = games_turn[:active_player].match?(/[A-Z]/)
|
65
|
-
inactive_has_uppercase = games_turn[:inactive_player].match?(/[A-Z]/)
|
39
|
+
# Validate casing (one must be uppercase, one must be lowercase)
|
40
|
+
active_uppercase = active == active.upcase && active != active.downcase
|
41
|
+
inactive_uppercase = inactive == inactive.upcase && inactive != inactive.downcase
|
66
42
|
|
67
|
-
#
|
68
|
-
raise ArgumentError, ERRORS[:
|
43
|
+
# If both have the same casing (both uppercase or both lowercase), raise error
|
44
|
+
raise ArgumentError, ERRORS[:casing] if active_uppercase == inactive_uppercase
|
69
45
|
|
70
|
-
# Check
|
71
|
-
if
|
72
|
-
raise ArgumentError, "Active
|
46
|
+
# Check for mixed case (must be all uppercase or all lowercase)
|
47
|
+
if active_uppercase && active != active.upcase
|
48
|
+
raise ArgumentError, format(ERRORS[:mixed], "Active variant", active)
|
73
49
|
end
|
74
50
|
|
75
|
-
|
51
|
+
if inactive_uppercase && inactive != inactive.upcase
|
52
|
+
raise ArgumentError, format(ERRORS[:mixed], "Inactive variant", inactive)
|
53
|
+
end
|
76
54
|
|
77
|
-
|
78
|
-
|
55
|
+
if !active_uppercase && active != active.downcase
|
56
|
+
raise ArgumentError, format(ERRORS[:mixed], "Active variant", active)
|
57
|
+
end
|
79
58
|
|
80
|
-
|
81
|
-
|
82
|
-
# @param games_turn [Hash] The games turn data to validate
|
83
|
-
# @raise [ArgumentError] If invalid characters are present
|
84
|
-
# @return [void]
|
85
|
-
private_class_method def self.validate_character_set(games_turn)
|
86
|
-
REQUIRED_KEYS.each do |key|
|
87
|
-
raise ArgumentError, ERRORS[:invalid_chars] unless games_turn[key].match?(/\A[a-zA-Z]+\z/)
|
59
|
+
if !inactive_uppercase && inactive != inactive.downcase
|
60
|
+
raise ArgumentError, format(ERRORS[:mixed], "Inactive variant", inactive)
|
88
61
|
end
|
62
|
+
|
63
|
+
true
|
89
64
|
end
|
90
65
|
end
|
91
66
|
end
|