sashite-ggn 0.5.0 → 0.6.0
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 +31 -14
- data/lib/sashite/ggn/move_validator.rb +97 -69
- data/lib/sashite/ggn/ruleset/source/destination/engine/transition.rb +41 -50
- data/lib/sashite/ggn/ruleset/source/destination/engine.rb +92 -172
- data/lib/sashite/ggn/ruleset/source/destination.rb +53 -7
- data/lib/sashite/ggn/ruleset/source.rb +42 -14
- data/lib/sashite/ggn/ruleset.rb +45 -105
- data/lib/sashite/ggn/schema.rb +96 -77
- data/lib/sashite/ggn/validation_error.rb +26 -1
- data/lib/sashite/ggn.rb +12 -11
- data/lib/sashite-ggn.rb +47 -33
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6dab117559875291772b7e8f60077f670928755870b39badd3555f4d66fd8357
|
4
|
+
data.tar.gz: 2fe3fd39dbb288dc140b10c4c9fec523d2ce00a7d33d7802d8612b95e2268ed3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 805c62503574bbc3b985cc21cb4dfe7a15e03198d97a5e7f0d91508111fd7290ed7382f93f2db6d653f07593f06a65c2640ac4e027127e0bdd964cdc61edab5c
|
7
|
+
data.tar.gz: fc0330900649313e22511f44d391d5a73befdabe4038aa4744de3f8af12684767c410bd6f8aa7e4a88f961cecc6d7be1223e00cfd9736396d92d7c781fa5abea
|
data/README.md
CHANGED
@@ -9,13 +9,15 @@ A Ruby implementation of the General Gameplay Notation (GGN) specification for d
|
|
9
9
|
|
10
10
|
GGN (General Gameplay Notation) is a rule-agnostic, JSON-based format for representing pseudo-legal moves in abstract strategy board games. This gem implements the [GGN Specification v1.0.0](https://sashite.dev/documents/ggn/1.0.0/).
|
11
11
|
|
12
|
-
GGN focuses on basic movement constraints rather than game-specific legality rules, making it suitable for:
|
12
|
+
GGN focuses exclusively on **board-to-board transformations**: pieces moving, capturing, or transforming on the game board. It describes basic movement constraints rather than game-specific legality rules, making it suitable for:
|
13
13
|
|
14
14
|
- Cross-game move analysis and engine development
|
15
15
|
- Hybrid games combining elements from different chess variants
|
16
16
|
- Database systems requiring piece disambiguation across game types
|
17
17
|
- Performance-critical applications with pre-computed move libraries
|
18
18
|
|
19
|
+
**Note**: GGN does not support hand management, piece drops, or captures-to-hand. These mechanics should be handled at a higher level by game engines.
|
20
|
+
|
19
21
|
## Installation
|
20
22
|
|
21
23
|
```ruby
|
@@ -47,7 +49,7 @@ engine = ruleset.select("CHESS:P").from("e2").to("e4")
|
|
47
49
|
board_state = { "e2" => "CHESS:P", "e3" => nil, "e4" => nil }
|
48
50
|
|
49
51
|
# Evaluate move
|
50
|
-
transitions = engine.where(board_state,
|
52
|
+
transitions = engine.where(board_state, "CHESS")
|
51
53
|
# => [#<Transition diff={"e2"=>nil, "e4"=>"CHESS:P"}>]
|
52
54
|
```
|
53
55
|
|
@@ -55,7 +57,7 @@ transitions = engine.where(board_state, {}, "CHESS")
|
|
55
57
|
|
56
58
|
```ruby
|
57
59
|
# Get all pseudo-legal moves for current position
|
58
|
-
all_moves = ruleset.pseudo_legal_transitions(board_state,
|
60
|
+
all_moves = ruleset.pseudo_legal_transitions(board_state, "CHESS")
|
59
61
|
|
60
62
|
# Each move is represented as [actor, origin, target, transitions]
|
61
63
|
all_moves.each do |actor, origin, target, transitions|
|
@@ -70,7 +72,7 @@ GGN supports multiple outcomes for a single move (e.g., promotion choices):
|
|
70
72
|
```ruby
|
71
73
|
# Chess pawn promotion
|
72
74
|
engine = ruleset.select("CHESS:P").from("e7").to("e8")
|
73
|
-
transitions = engine.where({"e7" => "CHESS:P", "e8" => nil},
|
75
|
+
transitions = engine.where({"e7" => "CHESS:P", "e8" => nil}, "CHESS")
|
74
76
|
|
75
77
|
transitions.each do |transition|
|
76
78
|
promoted_piece = transition.diff["e8"]
|
@@ -79,19 +81,34 @@ end
|
|
79
81
|
# Output: CHESS:Q, CHESS:R, CHESS:B, CHESS:N
|
80
82
|
```
|
81
83
|
|
82
|
-
##
|
84
|
+
## Complex Moves
|
83
85
|
|
84
|
-
|
86
|
+
GGN can represent multi-square moves like castling:
|
85
87
|
|
86
88
|
```ruby
|
87
|
-
#
|
88
|
-
engine = ruleset.select("
|
89
|
+
# Castling (king and rook move simultaneously)
|
90
|
+
engine = ruleset.select("CHESS:K").from("e1").to("g1")
|
91
|
+
board_state = {
|
92
|
+
"e1" => "CHESS:K", "f1" => nil, "g1" => nil, "h1" => "CHESS:R"
|
93
|
+
}
|
94
|
+
|
95
|
+
transitions = engine.where(board_state, "CHESS")
|
96
|
+
# => [#<Transition diff={"e1"=>nil, "f1"=>"CHESS:R", "g1"=>"CHESS:K", "h1"=>nil}>]
|
97
|
+
```
|
89
98
|
|
90
|
-
|
91
|
-
board_state = { "5e" => nil } # Empty target square
|
99
|
+
## Conditional Moves
|
92
100
|
|
93
|
-
|
94
|
-
|
101
|
+
GGN supports moves with complex requirements and prevent conditions:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
# En passant capture (removes pawn from different square)
|
105
|
+
engine = ruleset.select("CHESS:P").from("d5").to("e6")
|
106
|
+
board_state = {
|
107
|
+
"d5" => "CHESS:P", "e5" => "chess:p", "e6" => nil
|
108
|
+
}
|
109
|
+
|
110
|
+
transitions = engine.where(board_state, "CHESS")
|
111
|
+
# => [#<Transition diff={"d5"=>nil, "e5"=>nil, "e6"=>"CHESS:P"}>]
|
95
112
|
```
|
96
113
|
|
97
114
|
## Validation
|
@@ -114,9 +131,9 @@ Sashite::Ggn.valid?(data) # Returns boolean
|
|
114
131
|
|
115
132
|
### Key Methods
|
116
133
|
|
117
|
-
- `#pseudo_legal_transitions(board_state,
|
134
|
+
- `#pseudo_legal_transitions(board_state, active_game)` - Generate all moves
|
118
135
|
- `#select(actor).from(origin).to(target)` - Query specific move
|
119
|
-
- `#where(board_state,
|
136
|
+
- `#where(board_state, active_game)` - Evaluate move validity
|
120
137
|
|
121
138
|
## Related Specifications
|
122
139
|
|
@@ -3,40 +3,48 @@
|
|
3
3
|
module Sashite
|
4
4
|
module Ggn
|
5
5
|
# Centralized module for move condition validation.
|
6
|
-
# Contains shared logic
|
6
|
+
# Contains shared logic for validating piece ownership and board positions
|
7
|
+
# in GGN move evaluation.
|
8
|
+
#
|
9
|
+
# This module focuses exclusively on board-based validation since GGN
|
10
|
+
# only handles board-to-board transformations. All methods work with
|
11
|
+
# pieces on the board and use GAN (General Actor Notation) identifiers.
|
7
12
|
module MoveValidator
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
#
|
13
|
+
# Separator in GAN (General Actor Notation) identifiers.
|
14
|
+
# Used to split game identifiers from piece identifiers.
|
15
|
+
#
|
16
|
+
# @example GAN format
|
17
|
+
# "CHESS:K" # game: "CHESS", piece: "K"
|
18
|
+
# "shogi:+p" # game: "shogi", piece: "+p"
|
12
19
|
GAN_SEPARATOR = ":"
|
13
20
|
|
14
|
-
private_constant :DROP_ORIGIN, :GAN_SEPARATOR
|
15
|
-
|
16
21
|
private
|
17
22
|
|
18
|
-
# Checks if the piece is available in hand for a drop move.
|
19
|
-
#
|
20
|
-
# @param actor [String] GAN identifier of the piece
|
21
|
-
# @param captures [Hash] Pieces available in hand
|
22
|
-
#
|
23
|
-
# @return [Boolean] true if the piece can be dropped
|
24
|
-
def piece_available_in_hand?(actor, captures)
|
25
|
-
return false unless valid_gan_format?(actor)
|
26
|
-
|
27
|
-
base_piece = extract_base_piece(actor)
|
28
|
-
return false if base_piece.nil?
|
29
|
-
|
30
|
-
(captures[base_piece] || 0) > 0
|
31
|
-
end
|
32
|
-
|
33
23
|
# Checks if the correct piece is present at the origin square on the board.
|
34
24
|
#
|
25
|
+
# This method validates that the expected piece is actually present at the
|
26
|
+
# specified origin square, which is a fundamental requirement for any move.
|
27
|
+
#
|
35
28
|
# @param actor [String] GAN identifier of the piece
|
36
29
|
# @param origin [String] Origin square
|
37
30
|
# @param board_state [Hash] Current board state
|
38
31
|
#
|
39
32
|
# @return [Boolean] true if the piece is at the correct position
|
33
|
+
#
|
34
|
+
# @example Valid piece placement
|
35
|
+
# board_state = { "e1" => "CHESS:K", "e2" => "CHESS:P" }
|
36
|
+
# piece_on_board_at_origin?("CHESS:K", "e1", board_state)
|
37
|
+
# # => true
|
38
|
+
#
|
39
|
+
# @example Invalid piece placement
|
40
|
+
# board_state = { "e1" => "CHESS:Q", "e2" => "CHESS:P" }
|
41
|
+
# piece_on_board_at_origin?("CHESS:K", "e1", board_state)
|
42
|
+
# # => false (wrong piece at e1)
|
43
|
+
#
|
44
|
+
# @example Empty square
|
45
|
+
# board_state = { "e1" => nil, "e2" => "CHESS:P" }
|
46
|
+
# piece_on_board_at_origin?("CHESS:K", "e1", board_state)
|
47
|
+
# # => false (no piece at e1)
|
40
48
|
def piece_on_board_at_origin?(actor, origin, board_state)
|
41
49
|
return false unless valid_gan_format?(actor)
|
42
50
|
return false unless origin.is_a?(String) && !origin.empty?
|
@@ -45,67 +53,56 @@ module Sashite
|
|
45
53
|
board_state[origin] == actor
|
46
54
|
end
|
47
55
|
|
48
|
-
# Checks if the piece belongs to the current player.
|
49
|
-
# Fixed version that verifies both the game name AND the case.
|
56
|
+
# Checks if the piece belongs to the current player based on case matching.
|
50
57
|
#
|
51
|
-
# This method
|
52
|
-
# -
|
53
|
-
# -
|
54
|
-
# -
|
58
|
+
# This method implements the corrected ownership logic based on FEEN specification:
|
59
|
+
# - Ownership is determined by case correspondence, not exact string matching
|
60
|
+
# - If active_game is uppercase, the player owns uppercase-cased pieces
|
61
|
+
# - If active_game is lowercase, the player owns lowercase-cased pieces
|
62
|
+
# - This allows for hybrid games where a player may control pieces from different games
|
55
63
|
#
|
56
64
|
# @param actor [String] GAN identifier of the piece
|
57
|
-
# @param
|
65
|
+
# @param active_game [String] Current player's game identifier
|
58
66
|
#
|
59
67
|
# @return [Boolean] true if the piece belongs to the current player
|
60
|
-
|
68
|
+
#
|
69
|
+
# @example Same game, same case (typical scenario)
|
70
|
+
# piece_belongs_to_current_player?("CHESS:K", "CHESS")
|
71
|
+
# # => true (both uppercase)
|
72
|
+
#
|
73
|
+
# @example Different games, same case (hybrid scenario)
|
74
|
+
# piece_belongs_to_current_player?("MAKRUK:K", "CHESS")
|
75
|
+
# # => true (both uppercase, player controls both)
|
76
|
+
#
|
77
|
+
# @example Same game, different case
|
78
|
+
# piece_belongs_to_current_player?("chess:k", "CHESS")
|
79
|
+
# # => false (different players)
|
80
|
+
#
|
81
|
+
# @example Mixed case active_game (invalid)
|
82
|
+
# piece_belongs_to_current_player?("CHESS:K", "Chess")
|
83
|
+
# # => false (invalid active_game format)
|
84
|
+
def piece_belongs_to_current_player?(actor, active_game)
|
61
85
|
return false unless valid_gan_format?(actor)
|
62
|
-
return false unless valid_game_identifier?(
|
86
|
+
return false unless valid_game_identifier?(active_game)
|
63
87
|
|
64
88
|
game_part, piece_part = actor.split(GAN_SEPARATOR, 2)
|
65
89
|
|
66
|
-
#
|
67
|
-
#
|
68
|
-
|
69
|
-
|
90
|
+
# Determine player ownership based on case correspondence
|
91
|
+
# If active_game is uppercase, player owns uppercase pieces
|
92
|
+
# If active_game is lowercase, player owns lowercase pieces
|
93
|
+
case active_game
|
94
|
+
when active_game.upcase
|
70
95
|
# Current player is the uppercase one
|
71
|
-
game_part ==
|
72
|
-
when
|
96
|
+
game_part == game_part.upcase && piece_part.match?(/\A[-+]?[A-Z]'?\z/)
|
97
|
+
when active_game.downcase
|
73
98
|
# Current player is the lowercase one
|
74
|
-
game_part ==
|
99
|
+
game_part == game_part.downcase && piece_part.match?(/\A[-+]?[a-z]'?\z/)
|
75
100
|
else
|
76
|
-
#
|
101
|
+
# active_game is neither entirely uppercase nor lowercase
|
77
102
|
false
|
78
103
|
end
|
79
104
|
end
|
80
105
|
|
81
|
-
# Extracts the base form of a piece (removes modifiers).
|
82
|
-
#
|
83
|
-
# According to FEEN specification, pieces in hand are always stored
|
84
|
-
# in their base form without any state modifiers (prefixes/suffixes).
|
85
|
-
#
|
86
|
-
# @param actor [String] Complete GAN identifier
|
87
|
-
#
|
88
|
-
# @return [String, nil] Base form for hand storage, nil if invalid format
|
89
|
-
def extract_base_piece(actor)
|
90
|
-
return nil unless valid_gan_format?(actor)
|
91
|
-
|
92
|
-
game_part, piece_part = actor.split(GAN_SEPARATOR, 2)
|
93
|
-
|
94
|
-
# Safe extraction of the base character
|
95
|
-
match = piece_part.match(/\A[-+]?([A-Za-z])[']?\z/)
|
96
|
-
return nil unless match
|
97
|
-
|
98
|
-
clean_piece = match[1]
|
99
|
-
|
100
|
-
# Case consistency verification
|
101
|
-
game_is_upper = game_part == game_part.upcase
|
102
|
-
piece_is_upper = clean_piece == clean_piece.upcase
|
103
|
-
|
104
|
-
return nil unless game_is_upper == piece_is_upper
|
105
|
-
|
106
|
-
"#{game_part}#{GAN_SEPARATOR}#{clean_piece}"
|
107
|
-
end
|
108
|
-
|
109
106
|
# Validates the GAN format of an identifier.
|
110
107
|
#
|
111
108
|
# A valid GAN identifier must:
|
@@ -117,6 +114,16 @@ module Sashite
|
|
117
114
|
# @param actor [String] Identifier to validate
|
118
115
|
#
|
119
116
|
# @return [Boolean] true if the format is valid
|
117
|
+
#
|
118
|
+
# @example Valid GAN identifiers
|
119
|
+
# valid_gan_format?("CHESS:K") # => true
|
120
|
+
# valid_gan_format?("shogi:+p") # => true
|
121
|
+
# valid_gan_format?("MAKRUK:R'") # => true
|
122
|
+
#
|
123
|
+
# @example Invalid GAN identifiers
|
124
|
+
# valid_gan_format?("CHESS") # => false (no colon)
|
125
|
+
# valid_gan_format?("chess:K") # => false (case mismatch)
|
126
|
+
# valid_gan_format?("CHESS:") # => false (no piece part)
|
120
127
|
def valid_gan_format?(actor)
|
121
128
|
return false unless actor.is_a?(String)
|
122
129
|
return false unless actor.include?(GAN_SEPARATOR)
|
@@ -131,7 +138,7 @@ module Sashite
|
|
131
138
|
|
132
139
|
# Case consistency verification between game and piece
|
133
140
|
game_is_upper = game_part == game_part.upcase
|
134
|
-
piece_match = piece_part.match(/\A[-+]?([A-Za-z])
|
141
|
+
piece_match = piece_part.match(/\A[-+]?([A-Za-z])'?\z/)
|
135
142
|
return false unless piece_match
|
136
143
|
|
137
144
|
piece_char = piece_match[1]
|
@@ -149,6 +156,16 @@ module Sashite
|
|
149
156
|
# @param game_id [String] Game identifier to validate
|
150
157
|
#
|
151
158
|
# @return [Boolean] true if the identifier is valid
|
159
|
+
#
|
160
|
+
# @example Valid game identifiers
|
161
|
+
# valid_game_identifier?("CHESS") # => true
|
162
|
+
# valid_game_identifier?("shogi") # => true
|
163
|
+
# valid_game_identifier?("XIANGQI") # => true
|
164
|
+
#
|
165
|
+
# @example Invalid game identifiers
|
166
|
+
# valid_game_identifier?("Chess") # => false (mixed case)
|
167
|
+
# valid_game_identifier?("") # => false (empty)
|
168
|
+
# valid_game_identifier?("CHESS1") # => false (contains digit)
|
152
169
|
def valid_game_identifier?(game_id)
|
153
170
|
return false unless game_id.is_a?(String)
|
154
171
|
return false if game_id.empty?
|
@@ -168,12 +185,23 @@ module Sashite
|
|
168
185
|
# @param piece_id [String] Piece identifier to validate
|
169
186
|
#
|
170
187
|
# @return [Boolean] true if the identifier is valid
|
188
|
+
#
|
189
|
+
# @example Valid piece identifiers
|
190
|
+
# valid_piece_identifier?("K") # => true
|
191
|
+
# valid_piece_identifier?("+p") # => true
|
192
|
+
# valid_piece_identifier?("R'") # => true
|
193
|
+
# valid_piece_identifier?("-Q'") # => true
|
194
|
+
#
|
195
|
+
# @example Invalid piece identifiers
|
196
|
+
# valid_piece_identifier?("") # => false (empty)
|
197
|
+
# valid_piece_identifier?("++K") # => false (double prefix)
|
198
|
+
# valid_piece_identifier?("K''") # => false (double suffix)
|
171
199
|
def valid_piece_identifier?(piece_id)
|
172
200
|
return false unless piece_id.is_a?(String)
|
173
201
|
return false if piece_id.empty?
|
174
202
|
|
175
203
|
# Format: [optional prefix][letter][optional suffix]
|
176
|
-
piece_id.match?(/\A[-+]?[A-Za-z]
|
204
|
+
piece_id.match?(/\A[-+]?[A-Za-z]'?\z/)
|
177
205
|
end
|
178
206
|
end
|
179
207
|
end
|
@@ -8,79 +8,70 @@ module Sashite
|
|
8
8
|
class Engine
|
9
9
|
# Represents the result of a valid pseudo-legal move evaluation.
|
10
10
|
#
|
11
|
-
# A Transition encapsulates the changes that occur when a move is executed
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
11
|
+
# A Transition encapsulates the changes that occur when a move is executed
|
12
|
+
# on the game board. Since GGN focuses exclusively on board-to-board
|
13
|
+
# transformations, a Transition only contains board state changes: pieces
|
14
|
+
# moving, appearing, or disappearing on the board.
|
15
15
|
#
|
16
16
|
# @example Basic move (pawn advance)
|
17
|
-
# transition = Transition.new(
|
17
|
+
# transition = Transition.new("e2" => nil, "e4" => "CHESS:P")
|
18
18
|
# transition.diff # => { "e2" => nil, "e4" => "CHESS:P" }
|
19
|
-
# transition.gain # => nil
|
20
|
-
# transition.drop # => nil
|
21
19
|
#
|
22
|
-
# @example Capture
|
23
|
-
# transition = Transition.new("
|
24
|
-
# transition.
|
20
|
+
# @example Capture (piece takes enemy piece)
|
21
|
+
# transition = Transition.new("d4" => nil, "e5" => "CHESS:P")
|
22
|
+
# transition.diff # => { "d4" => nil, "e5" => "CHESS:P" }
|
25
23
|
#
|
26
|
-
# @example
|
27
|
-
# transition = Transition.new(
|
28
|
-
#
|
24
|
+
# @example Complex move (castling with king and rook)
|
25
|
+
# transition = Transition.new(
|
26
|
+
# "e1" => nil, "f1" => "CHESS:R", "g1" => "CHESS:K", "h1" => nil
|
27
|
+
# )
|
28
|
+
# transition.diff # => { "e1" => nil, "f1" => "CHESS:R", "g1" => "CHESS:K", "h1" => nil }
|
29
|
+
#
|
30
|
+
# @example Promotion (pawn becomes queen)
|
31
|
+
# transition = Transition.new("e7" => nil, "e8" => "CHESS:Q")
|
32
|
+
# transition.diff # => { "e7" => nil, "e8" => "CHESS:Q" }
|
29
33
|
class Transition
|
30
34
|
# @return [Hash<String, String|nil>] Board state changes after the move.
|
31
35
|
# Keys are square labels, values are piece identifiers or nil for empty squares.
|
32
36
|
attr_reader :diff
|
33
37
|
|
34
|
-
#
|
35
|
-
# typically from a capture. Nil if no piece is gained.
|
36
|
-
attr_reader :gain
|
37
|
-
|
38
|
-
# @return [String, nil] Piece identifier removed from the current player's hand
|
39
|
-
# for drop moves. Nil if no piece is dropped.
|
40
|
-
attr_reader :drop
|
41
|
-
|
42
|
-
# Creates a new Transition with the specified changes.
|
38
|
+
# Creates a new Transition with the specified board changes.
|
43
39
|
#
|
44
|
-
# @param gain [String, nil] Piece gained in hand (usually from capture)
|
45
|
-
# @param drop [String, nil] Piece dropped from hand (for drop moves)
|
46
40
|
# @param diff [Hash] Board state changes as keyword arguments.
|
47
41
|
# Keys should be square labels, values should be piece identifiers or nil.
|
48
42
|
#
|
49
43
|
# @example Creating a simple move transition
|
50
|
-
# Transition.new(
|
44
|
+
# Transition.new("e2" => nil, "e4" => "CHESS:P")
|
51
45
|
#
|
52
46
|
# @example Creating a capture transition
|
53
|
-
# Transition.new("
|
47
|
+
# Transition.new("d4" => nil, "e5" => "CHESS:P")
|
54
48
|
#
|
55
|
-
# @example Creating a
|
56
|
-
# Transition.new(
|
57
|
-
|
58
|
-
|
59
|
-
|
49
|
+
# @example Creating a complex multi-square transition (castling)
|
50
|
+
# Transition.new(
|
51
|
+
# "e1" => nil, # King leaves e1
|
52
|
+
# "f1" => "CHESS:R", # Rook moves to f1
|
53
|
+
# "g1" => "CHESS:K", # King moves to g1
|
54
|
+
# "h1" => nil # Rook leaves h1
|
55
|
+
# )
|
56
|
+
#
|
57
|
+
# @example Creating a promotion transition
|
58
|
+
# Transition.new("e7" => nil, "e8" => "CHESS:Q")
|
59
|
+
#
|
60
|
+
# @example Creating an en passant capture
|
61
|
+
# Transition.new(
|
62
|
+
# "d5" => nil, # Attacking pawn leaves d5
|
63
|
+
# "e5" => nil, # Captured pawn removed from e5
|
64
|
+
# "e6" => "CHESS:P" # Attacking pawn lands on e6
|
65
|
+
# )
|
66
|
+
def initialize(**diff)
|
60
67
|
@diff = diff
|
61
68
|
|
62
69
|
freeze
|
63
70
|
end
|
64
71
|
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
# @example
|
70
|
-
# transition.gain? # => true if @gain is not nil
|
71
|
-
def gain?
|
72
|
-
!@gain.nil?
|
73
|
-
end
|
74
|
-
|
75
|
-
# Checks if this transition involves dropping a piece from hand.
|
76
|
-
#
|
77
|
-
# @return [Boolean] true if a piece is dropped from hand
|
78
|
-
#
|
79
|
-
# @example
|
80
|
-
# transition.drop? # => true if @drop is not nil
|
81
|
-
def drop?
|
82
|
-
!@drop.nil?
|
83
|
-
end
|
72
|
+
# This class remains intentionally simple and rule-agnostic.
|
73
|
+
# Any interpretation of what constitutes a "capture" or "promotion"
|
74
|
+
# is left to higher-level game logic, maintaining GGN's neutrality.
|
84
75
|
end
|
85
76
|
end
|
86
77
|
end
|