feen 5.0.0.beta1 → 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/LICENSE.md +1 -1
- data/README.md +170 -44
- data/lib/feen/dumper/games_turn.rb +67 -0
- data/lib/feen/dumper/piece_placement.rb +117 -68
- 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 +72 -0
- data/lib/feen/dumper.rb +73 -23
- 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 +58 -0
- data/lib/feen/parser/piece_placement.rb +634 -0
- 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 +84 -0
- data/lib/feen/parser.rb +74 -36
- data/lib/feen.rb +58 -43
- metadata +17 -9
- data/lib/feen/parser/board_shape.rb +0 -39
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/LICENSE.md
CHANGED
data/README.md
CHANGED
@@ -1,89 +1,215 @@
|
|
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
|
-
>
|
8
|
+
> **FEEN** (Format for Encounter & Entertainment Notation) support for the Ruby language.
|
10
9
|
|
11
|
-
##
|
10
|
+
## What is FEEN?
|
12
11
|
|
13
|
-
|
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:
|
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
|
16
19
|
|
17
|
-
|
20
|
+
## Installation
|
18
21
|
|
19
22
|
```ruby
|
20
|
-
|
23
|
+
# In your Gemfile
|
24
|
+
gem "feen", ">= 5.0.0.beta3"
|
21
25
|
```
|
22
26
|
|
23
|
-
|
27
|
+
Or install manually:
|
24
28
|
|
25
29
|
```sh
|
26
|
-
|
30
|
+
gem install feen --pre
|
27
31
|
```
|
28
32
|
|
29
|
-
|
33
|
+
## FEEN Format
|
30
34
|
|
31
|
-
|
32
|
-
|
35
|
+
A FEEN record consists of three space-separated fields:
|
36
|
+
|
37
|
+
```
|
38
|
+
<PIECE-PLACEMENT> <PIECES-IN-HAND> <GAMES-TURN>
|
33
39
|
```
|
34
40
|
|
35
|
-
## Usage
|
41
|
+
## Basic Usage
|
36
42
|
|
37
|
-
###
|
43
|
+
### Parsing FEEN Strings
|
38
44
|
|
39
|
-
|
45
|
+
Convert a FEEN string into a structured Ruby object:
|
40
46
|
|
41
|
-
|
42
|
-
|
43
|
-
|
47
|
+
```ruby
|
48
|
+
require "feen"
|
49
|
+
|
50
|
+
feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
51
|
+
position = Feen.parse(feen_string)
|
52
|
+
|
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
|
+
# }
|
68
|
+
```
|
44
69
|
|
45
|
-
|
70
|
+
### Creating FEEN Strings
|
46
71
|
|
47
|
-
|
72
|
+
Convert position components to a FEEN string using named arguments:
|
48
73
|
|
49
74
|
```ruby
|
50
75
|
require "feen"
|
51
76
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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],
|
92
|
+
pieces_in_hand: []
|
62
93
|
)
|
63
|
-
# => "
|
94
|
+
# => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
95
|
+
```
|
96
|
+
|
97
|
+
### Validation
|
98
|
+
|
99
|
+
Check if a string is valid FEEN notation:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
require "feen"
|
103
|
+
|
104
|
+
Feen.valid?("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess")
|
105
|
+
# => true
|
106
|
+
|
107
|
+
Feen.valid?("invalid feen string")
|
108
|
+
# => false
|
109
|
+
```
|
110
|
+
|
111
|
+
## Game Examples
|
112
|
+
|
113
|
+
As FEEN is rule-agnostic, it can represent positions from various board games. Here are some examples:
|
114
|
+
|
115
|
+
### International Chess
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
119
|
+
```
|
120
|
+
|
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"
|
129
|
+
```
|
130
|
+
|
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)
|
138
|
+
|
139
|
+
```ruby
|
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)
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
feen_string = "rheagaehr/9/1c5c1/s1s1s1s1s/9/9/S1S1S1S1S/1C5C1/9/RHEAGAEHR - XIANGQI/xiangqi"
|
64
149
|
```
|
65
150
|
|
66
|
-
|
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)
|
154
|
+
|
155
|
+
## Advanced Features
|
67
156
|
|
68
|
-
|
157
|
+
### Multi-dimensional Boards
|
69
158
|
|
70
|
-
|
159
|
+
FEEN supports arbitrary-dimensional board configurations:
|
71
160
|
|
72
161
|
```ruby
|
73
162
|
require "feen"
|
74
163
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
164
|
+
# 3D board
|
165
|
+
piece_placement = [
|
166
|
+
[
|
167
|
+
%w[r n b],
|
168
|
+
%w[q k p]
|
169
|
+
],
|
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],
|
179
|
+
pieces_in_hand: []
|
180
|
+
)
|
181
|
+
# => "rnb/qkp//PR1/1KQ - FOO/bar"
|
79
182
|
```
|
80
183
|
|
184
|
+
### Piece Modifiers
|
185
|
+
|
186
|
+
FEEN supports prefixes and suffixes for pieces to denote various states or capabilities:
|
187
|
+
|
188
|
+
- **Prefix `+`**: May indicate promotion or special state
|
189
|
+
- Example in shogi: `+P` may represent a promoted pawn
|
190
|
+
|
191
|
+
- **Suffix `=`**: May indicate dual-option status
|
192
|
+
- Example in chess: `K=` may represent a king eligible for both kingside and queenside castling
|
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
|
197
|
+
|
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
|
201
|
+
|
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.
|
203
|
+
|
204
|
+
## Documentation
|
205
|
+
|
206
|
+
- [Official FEEN Specification](https://sashite.dev/documents/feen/1.0.0/)
|
207
|
+
- [API Documentation](https://rubydoc.info/github/sashite/feen.rb/main)
|
208
|
+
|
81
209
|
## License
|
82
210
|
|
83
|
-
The
|
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).
|
84
212
|
|
85
213
|
## About Sashité
|
86
214
|
|
87
|
-
This
|
88
|
-
|
89
|
-
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.
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Feen
|
4
|
+
module Dumper
|
5
|
+
# Handles conversion of games turn data to FEEN notation string
|
6
|
+
module GamesTurn
|
7
|
+
ERRORS = {
|
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
|
+
}.freeze
|
14
|
+
|
15
|
+
# Converts the active and inactive variant identifiers to a FEEN-formatted games turn string
|
16
|
+
#
|
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
|
19
|
+
# @return [String] FEEN-formatted games turn string
|
20
|
+
def self.dump(active_variant, inactive_variant)
|
21
|
+
validate_variants(active_variant, inactive_variant)
|
22
|
+
"#{active_variant}/#{inactive_variant}"
|
23
|
+
end
|
24
|
+
|
25
|
+
# Validates the game variant identifiers
|
26
|
+
#
|
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
|
30
|
+
# @return [void]
|
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/)
|
37
|
+
end
|
38
|
+
|
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
|
42
|
+
|
43
|
+
# If both have the same casing (both uppercase or both lowercase), raise error
|
44
|
+
raise ArgumentError, ERRORS[:casing] if active_uppercase == inactive_uppercase
|
45
|
+
|
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)
|
49
|
+
end
|
50
|
+
|
51
|
+
if inactive_uppercase && inactive != inactive.upcase
|
52
|
+
raise ArgumentError, format(ERRORS[:mixed], "Inactive variant", inactive)
|
53
|
+
end
|
54
|
+
|
55
|
+
if !active_uppercase && active != active.downcase
|
56
|
+
raise ArgumentError, format(ERRORS[:mixed], "Active variant", active)
|
57
|
+
end
|
58
|
+
|
59
|
+
if !inactive_uppercase && inactive != inactive.downcase
|
60
|
+
raise ArgumentError, format(ERRORS[:mixed], "Inactive variant", inactive)
|
61
|
+
end
|
62
|
+
|
63
|
+
true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -2,84 +2,133 @@
|
|
2
2
|
|
3
3
|
module Feen
|
4
4
|
module Dumper
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
class PiecePlacement
|
49
|
-
# @param indexes [Array] The shape of the board.
|
50
|
-
# @param piece_placement [Hash] The index of each piece on the board.
|
51
|
-
def initialize(indexes, piece_placement = {})
|
52
|
-
@indexes = indexes
|
53
|
-
@squares = ::Array.new(length) { |i| piece_placement.fetch(i, nil) }
|
5
|
+
module PiecePlacement
|
6
|
+
# Converts a piece placement structure to a FEEN-compliant string
|
7
|
+
#
|
8
|
+
# @param piece_placement [Array] Hierarchical array representing the board where:
|
9
|
+
# - Empty squares are represented by empty strings ("")
|
10
|
+
# - Pieces are represented by strings (e.g., "r", "K=", "+P")
|
11
|
+
# - Dimensions are represented by nested arrays
|
12
|
+
# @return [String] FEEN piece placement string
|
13
|
+
# @raise [ArgumentError] If the piece placement structure is invalid
|
14
|
+
def self.dump(piece_placement)
|
15
|
+
# Détecter la forme du tableau directement à partir de la structure
|
16
|
+
detect_shape(piece_placement)
|
17
|
+
|
18
|
+
# Formater directement la structure en chaîne FEEN
|
19
|
+
format_placement(piece_placement)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Detects the shape of the board based on the piece_placement structure
|
23
|
+
#
|
24
|
+
# @param piece_placement [Array] Hierarchical array structure representing the board
|
25
|
+
# @return [Array<Integer>] Array of dimension sizes
|
26
|
+
# @raise [ArgumentError] If the piece_placement structure is invalid
|
27
|
+
def self.detect_shape(piece_placement)
|
28
|
+
return [] if piece_placement.empty?
|
29
|
+
|
30
|
+
dimensions = []
|
31
|
+
current = piece_placement
|
32
|
+
|
33
|
+
# Traverse the structure to determine shape
|
34
|
+
while current.is_a?(Array) && !current.empty?
|
35
|
+
dimensions << current.size
|
36
|
+
|
37
|
+
# Check if all elements at this level have the same structure
|
38
|
+
validate_dimension_uniformity(current)
|
39
|
+
|
40
|
+
# Check if we've reached the leaf level (array of strings)
|
41
|
+
break if current.first.is_a?(String) ||
|
42
|
+
(current.first.is_a?(Array) && current.first.empty?)
|
43
|
+
|
44
|
+
current = current.first
|
45
|
+
end
|
46
|
+
|
47
|
+
dimensions
|
54
48
|
end
|
55
49
|
|
56
|
-
#
|
57
|
-
|
58
|
-
|
50
|
+
# Validates that all elements in a dimension have the same structure
|
51
|
+
#
|
52
|
+
# @param dimension [Array] Array of elements at a particular dimension level
|
53
|
+
# @raise [ArgumentError] If elements have inconsistent structure
|
54
|
+
def self.validate_dimension_uniformity(dimension)
|
55
|
+
return if dimension.empty?
|
56
|
+
|
57
|
+
first_type = dimension.first.class
|
58
|
+
first_size = dimension.first.is_a?(Array) ? dimension.first.size : nil
|
59
|
+
|
60
|
+
dimension.each do |element|
|
61
|
+
unless element.class == first_type
|
62
|
+
raise ArgumentError, "Inconsistent element types in dimension: #{first_type} vs #{element.class}"
|
63
|
+
end
|
64
|
+
|
65
|
+
if element.is_a?(Array) && element.size != first_size
|
66
|
+
raise ArgumentError, "Inconsistent dimension sizes: expected #{first_size}, got #{element.size}"
|
67
|
+
end
|
68
|
+
end
|
59
69
|
end
|
60
70
|
|
61
|
-
|
71
|
+
# Formats the piece placement structure into a FEEN string
|
72
|
+
#
|
73
|
+
# @param placement [Array] Piece placement structure
|
74
|
+
# @return [String] FEEN piece placement string
|
75
|
+
def self.format_placement(placement)
|
76
|
+
# For 1D arrays (ranks), format directly
|
77
|
+
if !placement.is_a?(Array) ||
|
78
|
+
(placement.is_a?(Array) && (placement.empty? || !placement.first.is_a?(Array)))
|
79
|
+
return format_rank(placement)
|
80
|
+
end
|
62
81
|
|
63
|
-
|
64
|
-
|
82
|
+
# For 2D+ arrays, format each sub-element and join with appropriate separator
|
83
|
+
depth = calculate_depth(placement) - 1
|
84
|
+
separator = "/" * depth
|
85
|
+
|
86
|
+
# Important: Ne pas inverser le tableau - nous voulons maintenir l'ordre original
|
87
|
+
elements = placement
|
88
|
+
elements.map { |element| format_placement(element) }.join(separator)
|
65
89
|
end
|
66
90
|
|
67
|
-
|
68
|
-
|
91
|
+
# Formats a rank (1D array of cells)
|
92
|
+
#
|
93
|
+
# @param rank [Array] 1D array of cells
|
94
|
+
# @return [String] FEEN rank string
|
95
|
+
def self.format_rank(rank)
|
96
|
+
return "" if !rank.is_a?(Array) || rank.empty?
|
97
|
+
|
98
|
+
result = ""
|
99
|
+
empty_count = 0
|
69
100
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
101
|
+
rank.each do |cell|
|
102
|
+
if cell.empty?
|
103
|
+
empty_count += 1
|
104
|
+
else
|
105
|
+
# Add accumulated empty squares
|
106
|
+
result += empty_count.to_s if empty_count > 0
|
107
|
+
empty_count = 0
|
108
|
+
|
109
|
+
# Add the piece
|
110
|
+
result += cell
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Add any trailing empty squares
|
115
|
+
result += empty_count.to_s if empty_count > 0
|
116
|
+
|
117
|
+
result
|
75
118
|
end
|
76
119
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
120
|
+
# Calculates the depth of a nested structure
|
121
|
+
#
|
122
|
+
# @param structure [Array] Structure to analyze
|
123
|
+
# @return [Integer] Depth of the structure
|
124
|
+
def self.calculate_depth(structure)
|
125
|
+
return 0 unless structure.is_a?(Array) && !structure.empty?
|
126
|
+
|
127
|
+
if structure.first.is_a?(Array)
|
128
|
+
1 + calculate_depth(structure.first)
|
129
|
+
else
|
130
|
+
1
|
131
|
+
end
|
83
132
|
end
|
84
133
|
end
|
85
134
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Feen
|
4
|
+
module Dumper
|
5
|
+
module PiecesInHand
|
6
|
+
Errors = {
|
7
|
+
invalid_type: "Piece at index: %<index>s must be a String, got type: %<type>s",
|
8
|
+
invalid_format: "Piece at index: %<index>s has an invalid format: '%<value>s'"
|
9
|
+
}.freeze
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative File.join("pieces_in_hand", "no_pieces")
|
4
|
+
require_relative File.join("pieces_in_hand", "errors")
|
5
|
+
|
6
|
+
module Feen
|
7
|
+
module Dumper
|
8
|
+
# Handles conversion of pieces in hand data to FEEN notation string
|
9
|
+
module PiecesInHand
|
10
|
+
# Converts an array of piece identifiers to a FEEN-formatted pieces in hand string
|
11
|
+
#
|
12
|
+
# @param piece_chars [Array<String>] Array of single-character piece identifiers
|
13
|
+
# @return [String] FEEN-formatted pieces in hand string
|
14
|
+
# @raise [ArgumentError] If any piece identifier is invalid
|
15
|
+
# @example
|
16
|
+
# PiecesInHand.dump("P", "p", "B")
|
17
|
+
# # => "BPp"
|
18
|
+
#
|
19
|
+
# PiecesInHand.dump
|
20
|
+
# # => "-"
|
21
|
+
def self.dump(*piece_chars)
|
22
|
+
# If no pieces in hand, return the standardized empty indicator
|
23
|
+
return NoPieces if piece_chars.empty?
|
24
|
+
|
25
|
+
# Validate each piece character according to the FEEN specification
|
26
|
+
validated_chars = validate_piece_chars(piece_chars)
|
27
|
+
|
28
|
+
# Sort pieces in ASCII lexicographic order and join them
|
29
|
+
validated_chars.sort.join
|
30
|
+
end
|
31
|
+
|
32
|
+
# Validates all piece characters according to FEEN specification
|
33
|
+
#
|
34
|
+
# @param piece_chars [Array<Object>] Array of piece character candidates
|
35
|
+
# @return [Array<String>] Array of validated piece characters
|
36
|
+
# @raise [ArgumentError] If any piece character is invalid
|
37
|
+
private_class_method def self.validate_piece_chars(piece_chars)
|
38
|
+
piece_chars.each_with_index.map do |char, index|
|
39
|
+
validate_piece_char(char, index)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Validates a single piece character according to FEEN specification
|
44
|
+
#
|
45
|
+
# @param char [Object] Piece character candidate
|
46
|
+
# @param index [Integer] Index of the character in the original array
|
47
|
+
# @return [String] Validated piece character
|
48
|
+
# @raise [ArgumentError] If the piece character is invalid
|
49
|
+
private_class_method def self.validate_piece_char(char, index)
|
50
|
+
# Validate type
|
51
|
+
unless char.is_a?(::String)
|
52
|
+
raise ::ArgumentError, format(
|
53
|
+
Errors[:invalid_type],
|
54
|
+
index: index,
|
55
|
+
type: char.class
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Validate format (single alphabetic character)
|
60
|
+
unless char.match?(/\A[a-zA-Z]\z/)
|
61
|
+
raise ::ArgumentError, format(
|
62
|
+
Errors[:invalid_format],
|
63
|
+
index: index,
|
64
|
+
value: char
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
char
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|