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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3dbd219b5b2e7870e60da133faae1f342179e35682590d2f5fbe06c4f0130d39
4
- data.tar.gz: c1042208ec1799d5700e87327bafaa39dc918b5e101304c9b90a67e22be76c25
3
+ metadata.gz: ad6a4426ae68ac5344888465a23eea36cf28b3e0137b50af8483b98288012969
4
+ data.tar.gz: f9f42840240c5b75629cdb5459f492f966d3fc2706a354c18abc9841a9f17e21
5
5
  SHA512:
6
- metadata.gz: e8480101aa872bba2295a2ee74d47f4bde63a7d94c50b6504e8acadbf8347ee5a136bc962b65fdbb75aed3dd887693b6b012b53aed77c4e101313fd2189c59d5
7
- data.tar.gz: fde52d865df00f7602017789aeb9d6520b809be88fad13a14215f289abcc60f61d88ae73624cf01fd3a158aa62a94b98814bb0a1aff510b2e218833fb59094d9
6
+ metadata.gz: 31141ae8b866a11ffec6eed44308b475d03e75174ec0bc12b14ff0dc60bfe8b21ad60af3d32e1747d7dce94fe0cff0bbd5add502f26c26aeffac97104fbeeaca
7
+ data.tar.gz: 82c82de3cf0024e596d9f318e612a8077069a9b519e6b4e753dc808bbee91045f72328489a26307f0877194a4e99ffc8502dd3d3d1610900215ab697fa479ce4
data/README.md CHANGED
@@ -1,28 +1,27 @@
1
1
  # Feen.rb
2
2
 
3
- [![Version](https://img.shields.io/github/v/tag/sashite/feen.rb?label=Version&logo=github)](https://github.com/sashite/feen.rb/releases)
3
+ [![Version](https://img.shields.io/github/v/tag/sashite/feen.rb?label=Version&logo=github)](https://github.com/sashite/feen.rb/tags)
4
4
  [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/feen.rb/main)
5
- [![CI](https://github.com/sashite/feen.rb/workflows/CI/badge.svg?branch=main)](https://github.com/sashite/feen.rb/actions?query=workflow%3Aci+branch%3Amain)
6
- [![RuboCop](https://github.com/sashite/feen.rb/workflows/RuboCop/badge.svg?branch=main)](https://github.com/sashite/feen.rb/actions?query=workflow%3Arubocop+branch%3Amain)
5
+ ![Ruby](https://github.com/sashite/feen.rb/actions/workflows/main.yml/badge.svg?branch=main)
7
6
  [![License](https://img.shields.io/github/license/sashite/feen.rb?label=License&logo=github)](https://github.com/sashite/feen.rb/raw/main/LICENSE.md)
8
7
 
9
- > **FEEN** (Forsyth–Edwards Essential Notation) support for the Ruby language.
8
+ > **FEEN** (Format for Encounter & Entertainment Notation) support for the Ruby language.
10
9
 
11
10
  ## What is FEEN?
12
11
 
13
- FEEN (Forsyth–Edwards Essential Notation) is a compact, canonical, and rule-agnostic textual format for representing static board positions in two-player piece-placement games.
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
- - Multiple game types (chess, shogi, xiangqi, etc.)
17
- - Hybrid or cross-game positions
18
- - Arbitrary-dimensional boards
19
- - Pieces in hand (as used in Shogi)
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.beta2"
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> <GAMES-TURN> <PIECES-IN-HAND>
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 with structured position data
55
- # position[:piece_placement] # 2D array of board pieces
56
- # position[:games_turn] # Details about active player and game
57
- # position[:pieces_in_hand] # Array of pieces held for dropping
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 a position structure to a FEEN string:
72
+ Convert position components to a FEEN string using named arguments:
63
73
 
64
74
  ```ruby
65
75
  require "feen"
66
76
 
67
- position = {
68
- piece_placement: [
69
- [{ id: "r" }, { id: "n" }, { id: "b" }, { id: "q" }, { id: "k", suffix: "=" }, { id: "b" }, { id: "n" }, { id: "r" }],
70
- [{ id: "p" }, { id: "p" }, { id: "p" }, { id: "p" }, { id: "p" }, { id: "p" }, { id: "p" }, { id: "p" }],
71
- [nil, nil, nil, nil, nil, nil, nil, nil],
72
- [nil, nil, nil, nil, nil, nil, nil, nil],
73
- [nil, nil, nil, nil, nil, nil, nil, nil],
74
- [nil, nil, nil, nil, nil, nil, nil, nil],
75
- [{ id: "P" }, { id: "P" }, { id: "P" }, { id: "P" }, { id: "P" }, { id: "P" }, { id: "P" }, { id: "P" }],
76
- [{ id: "R" }, { id: "N" }, { id: "B" }, { id: "Q" }, { id: "K", suffix: "=" }, { id: "B" }, { id: "N" }, { id: "R" }]
77
- ],
78
- games_turn: {
79
- active_player: "CHESS",
80
- inactive_player: "chess"
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
- ## FEN Compatibility
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
- ### Converting FEN to FEEN
115
+ ### International Chess
106
116
 
107
117
  ```ruby
108
- require "feen"
118
+ feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
119
+ ```
109
120
 
110
- fen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
111
- feen_string = Feen.from_fen(fen_string)
112
- # => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR CHESS/chess -"
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
- ### Converting FEEN to FEN
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
- require "feen"
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
- feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR CHESS/chess -"
121
- fen_string = Feen.to_fen(feen_string)
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
- > ⚠️ `Feen.to_fen` only supports FEEN positions where `games_turn` is `CHESS/chess` or `chess/CHESS`.
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
- position = {
138
- piece_placement: [
139
- [
140
- [{ id: "r" }, { id: "n" }, { id: "b" }],
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
- games_turn: {
149
- active_player: "CHESS",
150
- inactive_player: "chess"
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 `+`: Often used for promotion (e.g., `+P` for promoted pawn in Shogi)
164
- - Suffix `=`: Dual-option status (e.g., `K=` for king eligible for both castling sides)
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
- ### Sanitizing FEN Strings
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
- FEEN includes utilities to clean FEN strings by validating and removing invalid castling rights and en passant targets:
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
- ```ruby
173
- require "feen"
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
- # FEN with invalid castling rights
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 v1.0.0](https://sashite.dev/documents/feen/1.0.0/)
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 [gem](https://rubygems.org/gems/feen) is maintained by [Sashité](https://sashite.com/).
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 structure to FEEN notation string
5
+ # Handles conversion of games turn data to FEEN notation string
6
6
  module GamesTurn
7
7
  ERRORS = {
8
- missing_key: "Missing required key in games_turn: %s",
9
- invalid_type: "Invalid type for games_turn[%s]: expected String, got %s",
10
- empty_string: "Empty string for games_turn[%s]",
11
- casing_requirement: "One game must be uppercase and the other lowercase",
12
- invalid_chars: "Game identifiers must contain only alphabetic characters (a-z, A-Z)"
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
- REQUIRED_KEYS = %i[active_player inactive_player].freeze
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 games_turn [Hash] Hash containing game turn information
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(games_turn)
22
- validate_games_turn(games_turn)
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 basic structure of games_turn
25
+ # Validates the game variant identifiers
42
26
  #
43
- # @param games_turn [Hash] The games turn data to validate
44
- # @raise [ArgumentError] If the structure is invalid
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.validate_structure(games_turn)
47
- REQUIRED_KEYS.each do |key|
48
- raise ArgumentError, format(ERRORS[:missing_key], key) unless games_turn.key?(key)
49
-
50
- unless games_turn[key].is_a?(String)
51
- raise ArgumentError, format(ERRORS[:invalid_type], key, games_turn[key].class)
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
- # Validates the casing requirement (one uppercase, one lowercase)
59
- #
60
- # @param games_turn [Hash] The games turn data to validate
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
- # Ensure exactly one has uppercase
68
- raise ArgumentError, ERRORS[:casing_requirement] if active_has_uppercase == inactive_has_uppercase
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 that uppercase game is all caps and lowercase game has no caps
71
- if active_has_uppercase && games_turn[:active_player].match?(/[a-z]/)
72
- raise ArgumentError, "Active game has mixed case: #{games_turn[:active_player]}"
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
- return unless inactive_has_uppercase && games_turn[:inactive_player].match?(/[a-z]/)
51
+ if inactive_uppercase && inactive != inactive.upcase
52
+ raise ArgumentError, format(ERRORS[:mixed], "Inactive variant", inactive)
53
+ end
76
54
 
77
- raise ArgumentError, "Inactive game has mixed case: #{games_turn[:inactive_player]}"
78
- end
55
+ if !active_uppercase && active != active.downcase
56
+ raise ArgumentError, format(ERRORS[:mixed], "Active variant", active)
57
+ end
79
58
 
80
- # Validates that identifiers only contain allowed characters
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