feen 5.0.0.beta3 → 5.0.0.beta4
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 +37 -10
- data/lib/feen/dumper.rb +1 -1
- 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 +25 -21
- data/lib/feen.rb +69 -16
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: de87bfc9b071e9f1ff14b6f7378ba0a6f9f78cdb9a9bfe32ca00f2494a1be2cd
|
4
|
+
data.tar.gz: fd4d19e8a3b7fb845f081b127a859ff0791c8c11c05e1efb4846152044f67c18
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e9fe95297b8577a3bf171a865deca0dc9c2eed91a7d039c04f97077d53d3fa912ca553f28f448f0dd987087926d3ca5225624d7f2d8d08a9275a16a11efe26e0
|
7
|
+
data.tar.gz: b82b7e00f5ece6f5e5859750226be3c74db23e694c8e53def8c8c656fe2fb3ecba7f0f4166cf3d9a4f6c7b4ab7af3cf6544da87a2684e438a389d96e04b3dc75
|
data/README.md
CHANGED
@@ -5,23 +5,24 @@
|
|
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
|
- Representing positions from various games without knowledge of specific rules
|
16
16
|
- Supporting boards of arbitrary dimensions
|
17
17
|
- Encoding pieces in hand (as used in Shogi)
|
18
18
|
- Facilitating serialization and deserialization of positions
|
19
|
+
- Ensuring canonical representation for consistent data handling
|
19
20
|
|
20
21
|
## Installation
|
21
22
|
|
22
23
|
```ruby
|
23
24
|
# In your Gemfile
|
24
|
-
gem "feen", ">= 5.0.0.
|
25
|
+
gem "feen", ">= 5.0.0.beta4"
|
25
26
|
```
|
26
27
|
|
27
28
|
Or install manually:
|
@@ -52,7 +53,7 @@ position = Feen.parse(feen_string)
|
|
52
53
|
|
53
54
|
# Result is a hash:
|
54
55
|
# {
|
55
|
-
#
|
56
|
+
# piece_placement: [
|
56
57
|
# ["r", "n", "b", "q", "k=", "b", "n", "r"],
|
57
58
|
# ["p", "p", "p", "p", "p", "p", "p", "p"],
|
58
59
|
# ["", "", "", "", "", "", "", ""],
|
@@ -62,11 +63,27 @@ position = Feen.parse(feen_string)
|
|
62
63
|
# ["P", "P", "P", "P", "P", "P", "P", "P"],
|
63
64
|
# ["R", "N", "B", "Q", "K=", "B", "N", "R"]
|
64
65
|
# ],
|
65
|
-
#
|
66
|
-
# "
|
66
|
+
# pieces_in_hand: [],
|
67
|
+
# games_turn: ["CHESS", "chess"]
|
67
68
|
# }
|
68
69
|
```
|
69
70
|
|
71
|
+
### Safe Parsing
|
72
|
+
|
73
|
+
Parse a FEEN string without raising exceptions:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
require "feen"
|
77
|
+
|
78
|
+
# Valid FEEN string
|
79
|
+
result = Feen.safe_parse("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess")
|
80
|
+
# => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
|
81
|
+
|
82
|
+
# Invalid FEEN string
|
83
|
+
result = Feen.safe_parse("invalid feen string")
|
84
|
+
# => nil
|
85
|
+
```
|
86
|
+
|
70
87
|
### Creating FEEN Strings
|
71
88
|
|
72
89
|
Convert position components to a FEEN string using named arguments:
|
@@ -96,18 +113,28 @@ result = Feen.dump(
|
|
96
113
|
|
97
114
|
### Validation
|
98
115
|
|
99
|
-
Check if a string is valid FEEN notation:
|
116
|
+
Check if a string is valid FEEN notation and in canonical form:
|
100
117
|
|
101
118
|
```ruby
|
102
119
|
require "feen"
|
103
120
|
|
104
|
-
|
121
|
+
# Canonical form
|
122
|
+
Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi")
|
105
123
|
# => true
|
106
124
|
|
125
|
+
# Invalid syntax
|
107
126
|
Feen.valid?("invalid feen string")
|
108
127
|
# => false
|
128
|
+
|
129
|
+
# Valid syntax but non-canonical form (pieces in hand not in lexicographic order)
|
130
|
+
Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gn2sl SHOGI/shogi")
|
131
|
+
# => false
|
109
132
|
```
|
110
133
|
|
134
|
+
The `valid?` method performs two levels of validation:
|
135
|
+
1. **Syntax check**: Verifies the string can be parsed as FEEN
|
136
|
+
2. **Canonicity check**: Ensures the string is in its canonical form through round-trip conversion
|
137
|
+
|
111
138
|
## Game Examples
|
112
139
|
|
113
140
|
As FEEN is rule-agnostic, it can represent positions from various board games. Here are some examples:
|
@@ -125,14 +152,14 @@ In this initial chess position:
|
|
125
152
|
### Shogi (Japanese Chess)
|
126
153
|
|
127
154
|
```ruby
|
128
|
-
feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL
|
155
|
+
feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi"
|
129
156
|
```
|
130
157
|
|
131
158
|
In this shogi position:
|
132
159
|
- The format supports promotions with the `+` prefix (e.g., `+P` for a promoted pawn)
|
133
160
|
- The notation allows for pieces in hand, indicated in the second field
|
134
161
|
- `SHOGI/shogi` indicates it's Sente's (Black's, uppercase) turn to move
|
135
|
-
- `
|
162
|
+
- `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
163
|
|
137
164
|
### Makruk (Thai Chess)
|
138
165
|
|
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 = " "
|
@@ -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,7 +23,7 @@ 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
29
|
# feen = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
@@ -42,25 +42,6 @@ module Feen
|
|
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 = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - 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,11 +6,19 @@ 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
|
@@ -30,14 +38,17 @@ module Feen
|
|
30
38
|
# games_turn: ["CHESS", "chess"]
|
31
39
|
# )
|
32
40
|
# # => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
33
|
-
def self.dump(
|
34
|
-
Dumper.dump(
|
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
54
|
# feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
@@ -56,21 +67,63 @@ module Feen
|
|
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("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - 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.beta4
|
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,12 +35,20 @@ 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/
|
49
|
+
funding_uri: https://github.com/sponsors/cyril
|
40
50
|
rubygems_mfa_required: 'true'
|
51
|
+
article_uri: https://blog.cyril.email/posts/2025-05-01/introducing-feen-notation.html
|
41
52
|
rdoc_options: []
|
42
53
|
require_paths:
|
43
54
|
- lib
|
@@ -54,5 +65,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
65
|
requirements: []
|
55
66
|
rubygems_version: 3.6.7
|
56
67
|
specification_version: 4
|
57
|
-
summary: FEEN support for the Ruby language.
|
68
|
+
summary: FEEN (Forsyth–Edwards Enhanced Notation) support for the Ruby language.
|
58
69
|
test_files: []
|