sashite-ggn 0.2.0 → 0.5.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 +5 -5
- data/LICENSE.md +17 -18
- data/README.md +115 -28
- data/lib/sashite/ggn/move_validator.rb +180 -0
- data/lib/sashite/ggn/ruleset/source/destination/engine/transition.rb +90 -0
- data/lib/sashite/ggn/ruleset/source/destination/engine.rb +454 -0
- data/lib/sashite/ggn/ruleset/source/destination.rb +65 -0
- data/lib/sashite/ggn/ruleset/source.rb +71 -0
- data/lib/sashite/ggn/ruleset.rb +371 -0
- data/lib/sashite/ggn/schema.rb +152 -0
- data/lib/sashite/ggn/validation_error.rb +31 -0
- data/lib/sashite/ggn.rb +317 -5
- data/lib/sashite-ggn.rb +112 -1
- metadata +32 -82
- data/.gitignore +0 -22
- data/.ruby-version +0 -1
- data/.travis.yml +0 -3
- data/Gemfile +0 -2
- data/Rakefile +0 -7
- data/VERSION.semver +0 -1
- data/lib/sashite/ggn/ability.rb +0 -11
- data/lib/sashite/ggn/gameplay.rb +0 -9
- data/lib/sashite/ggn/object.rb +0 -9
- data/lib/sashite/ggn/pattern.rb +0 -9
- data/lib/sashite/ggn/square.rb +0 -7
- data/lib/sashite/ggn/state.rb +0 -7
- data/lib/sashite/ggn/subject.rb +0 -9
- data/lib/sashite/ggn/verb.rb +0 -7
- data/sashite-ggn.gemspec +0 -19
- data/test/_test_helper.rb +0 -2
- data/test/test_ggn.rb +0 -15
- data/test/test_ggn_ability.rb +0 -41
- data/test/test_ggn_gameplay.rb +0 -17
- data/test/test_ggn_object.rb +0 -41
- data/test/test_ggn_pattern.rb +0 -17
- data/test/test_ggn_square.rb +0 -41
- data/test/test_ggn_state.rb +0 -29
- data/test/test_ggn_subject.rb +0 -41
- data/test/test_ggn_verb.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1af50f2e0d1bcc29f645560a6889184b953b18cc5a10096e2fd5042b61adac22
|
4
|
+
data.tar.gz: d01e4f3b4dfe6d34dd68d6155c541dd5fd17488e4b86e08710e9872ccd901cba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8565e8a83cac905bf9cfd368c136964c83e26f81b8e0fbd0c11c28c4c9da358511cda025688304cb37a16a21b79e07965209dd1027aab478b1fd49e976f00a80
|
7
|
+
data.tar.gz: 4bc9121f894dc379b85c4440a1844946f88cd5c9aa3cf25c8036845fc76060072e7d66579efed704b20f4b8ef851ded94e09b267c8249c231a09ae777d9ebf2e
|
data/LICENSE.md
CHANGED
@@ -1,22 +1,21 @@
|
|
1
|
-
|
1
|
+
# The MIT License
|
2
2
|
|
3
|
-
|
3
|
+
Copyright (c) 2014-2025 Sashité
|
4
4
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
the following conditions:
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
12
11
|
|
13
|
-
The above copyright notice and this permission notice shall be
|
14
|
-
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
15
14
|
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
OF
|
22
|
-
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,48 +1,135 @@
|
|
1
|
-
#
|
1
|
+
# Ggn.rb
|
2
2
|
|
3
|
-
A
|
3
|
+
A Ruby implementation of the General Gameplay Notation (GGN) specification for describing pseudo-legal moves in abstract strategy board games.
|
4
4
|
|
5
|
-
|
5
|
+
[](https://badge.fury.io/rb/sashite-ggn)
|
6
|
+
[](https://github.com/sashite/ggn.rb/actions)
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
## What is GGN?
|
9
|
+
|
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
|
+
|
12
|
+
GGN focuses on basic movement constraints rather than game-specific legality rules, making it suitable for:
|
13
|
+
|
14
|
+
- Cross-game move analysis and engine development
|
15
|
+
- Hybrid games combining elements from different chess variants
|
16
|
+
- Database systems requiring piece disambiguation across game types
|
17
|
+
- Performance-critical applications with pre-computed move libraries
|
10
18
|
|
11
19
|
## Installation
|
12
20
|
|
13
|
-
|
21
|
+
```ruby
|
22
|
+
gem 'sashite-ggn'
|
23
|
+
```
|
24
|
+
|
25
|
+
## Basic Usage
|
14
26
|
|
15
|
-
|
27
|
+
### Loading GGN Data
|
16
28
|
|
17
|
-
|
29
|
+
```ruby
|
30
|
+
require "sashite-ggn"
|
18
31
|
|
19
|
-
|
32
|
+
# Load from file
|
33
|
+
ruleset = Sashite::Ggn.load_file("chess_moves.json")
|
20
34
|
|
21
|
-
|
35
|
+
# Load from string
|
36
|
+
json = '{"CHESS:P": {"e2": {"e4": [{"perform": {"e2": null, "e4": "CHESS:P"}}]}}}'
|
37
|
+
ruleset = Sashite::Ggn.load_string(json)
|
38
|
+
```
|
22
39
|
|
23
|
-
|
40
|
+
### Evaluating Moves
|
24
41
|
|
25
|
-
|
42
|
+
```ruby
|
43
|
+
# Query specific move
|
44
|
+
engine = ruleset.select("CHESS:P").from("e2").to("e4")
|
26
45
|
|
27
|
-
|
46
|
+
# Define board state
|
47
|
+
board_state = { "e2" => "CHESS:P", "e3" => nil, "e4" => nil }
|
48
|
+
|
49
|
+
# Evaluate move
|
50
|
+
transitions = engine.where(board_state, {}, "CHESS")
|
51
|
+
# => [#<Transition diff={"e2"=>nil, "e4"=>"CHESS:P"}>]
|
52
|
+
```
|
53
|
+
|
54
|
+
### Generating All Moves
|
28
55
|
|
29
56
|
```ruby
|
30
|
-
|
57
|
+
# Get all pseudo-legal moves for current position
|
58
|
+
all_moves = ruleset.pseudo_legal_transitions(board_state, captures, "CHESS")
|
59
|
+
|
60
|
+
# Each move is represented as [actor, origin, target, transitions]
|
61
|
+
all_moves.each do |actor, origin, target, transitions|
|
62
|
+
puts "#{actor}: #{origin} → #{target} (#{transitions.size} variants)"
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
## Move Variants
|
31
67
|
|
32
|
-
|
33
|
-
state.last_moved_actor = nil
|
34
|
-
state.previous_moves_counter = nil
|
68
|
+
GGN supports multiple outcomes for a single move (e.g., promotion choices):
|
35
69
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
70
|
+
```ruby
|
71
|
+
# Chess pawn promotion
|
72
|
+
engine = ruleset.select("CHESS:P").from("e7").to("e8")
|
73
|
+
transitions = engine.where({"e7" => "CHESS:P", "e8" => nil}, {}, "CHESS")
|
74
|
+
|
75
|
+
transitions.each do |transition|
|
76
|
+
promoted_piece = transition.diff["e8"]
|
77
|
+
puts "Promote to #{promoted_piece}"
|
78
|
+
end
|
79
|
+
# Output: CHESS:Q, CHESS:R, CHESS:B, CHESS:N
|
40
80
|
```
|
41
81
|
|
42
|
-
##
|
82
|
+
## Piece Drops
|
83
|
+
|
84
|
+
For games supporting piece drops (e.g., Shogi):
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# Drop from hand (origin "*")
|
88
|
+
engine = ruleset.select("SHOGI:P").from("*").to("5e")
|
89
|
+
|
90
|
+
captures = { "SHOGI:P" => 1 } # One pawn in hand
|
91
|
+
board_state = { "5e" => nil } # Empty target square
|
92
|
+
|
93
|
+
transitions = engine.where(board_state, captures, "SHOGI")
|
94
|
+
# => [#<Transition diff={"5e"=>"SHOGI:P"} drop="SHOGI:P">]
|
95
|
+
```
|
96
|
+
|
97
|
+
## Validation
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
# Validate GGN data
|
101
|
+
Sashite::Ggn.validate!(data) # Raises exception on failure
|
102
|
+
Sashite::Ggn.valid?(data) # Returns boolean
|
103
|
+
```
|
104
|
+
|
105
|
+
## API Reference
|
106
|
+
|
107
|
+
### Core Classes
|
108
|
+
|
109
|
+
- `Sashite::Ggn::Ruleset` - Main entry point for querying moves
|
110
|
+
- `Sashite::Ggn::Ruleset::Source` - Piece type with source positions
|
111
|
+
- `Sashite::Ggn::Ruleset::Source::Destination` - Available destinations
|
112
|
+
- `Sashite::Ggn::Ruleset::Source::Destination::Engine` - Move evaluator
|
113
|
+
- `Sashite::Ggn::Ruleset::Source::Destination::Engine::Transition` - Move result
|
114
|
+
|
115
|
+
### Key Methods
|
116
|
+
|
117
|
+
- `#pseudo_legal_transitions(board_state, captures, turn)` - Generate all moves
|
118
|
+
- `#select(actor).from(origin).to(target)` - Query specific move
|
119
|
+
- `#where(board_state, captures, turn)` - Evaluate move validity
|
120
|
+
|
121
|
+
## Related Specifications
|
122
|
+
|
123
|
+
GGN works alongside other Sashité specifications:
|
124
|
+
|
125
|
+
- [GAN](https://sashite.dev/documents/gan/1.0.0/) - General Actor Notation for piece identifiers
|
126
|
+
- [FEEN](https://sashite.dev/documents/feen/1.0.0/) - Board position representation
|
127
|
+
- [PMN](https://sashite.dev/documents/pmn/1.0.0/) - Move sequence representation
|
128
|
+
|
129
|
+
## License
|
130
|
+
|
131
|
+
The [gem](https://rubygems.org/gems/sashite-ggn) is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
132
|
+
|
133
|
+
## About Sashité
|
43
134
|
|
44
|
-
|
45
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
46
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
47
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
48
|
-
5. Create a new Pull Request
|
135
|
+
This project is maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sashite
|
4
|
+
module Ggn
|
5
|
+
# Centralized module for move condition validation.
|
6
|
+
# Contains shared logic between Engine and performance optimizations.
|
7
|
+
module MoveValidator
|
8
|
+
# Reserved for drops from hand
|
9
|
+
DROP_ORIGIN = "*"
|
10
|
+
|
11
|
+
# Separator in GAN (General Actor Notation) identifiers
|
12
|
+
GAN_SEPARATOR = ":"
|
13
|
+
|
14
|
+
private_constant :DROP_ORIGIN, :GAN_SEPARATOR
|
15
|
+
|
16
|
+
private
|
17
|
+
|
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
|
+
# Checks if the correct piece is present at the origin square on the board.
|
34
|
+
#
|
35
|
+
# @param actor [String] GAN identifier of the piece
|
36
|
+
# @param origin [String] Origin square
|
37
|
+
# @param board_state [Hash] Current board state
|
38
|
+
#
|
39
|
+
# @return [Boolean] true if the piece is at the correct position
|
40
|
+
def piece_on_board_at_origin?(actor, origin, board_state)
|
41
|
+
return false unless valid_gan_format?(actor)
|
42
|
+
return false unless origin.is_a?(String) && !origin.empty?
|
43
|
+
return false unless board_state.is_a?(Hash)
|
44
|
+
|
45
|
+
board_state[origin] == actor
|
46
|
+
end
|
47
|
+
|
48
|
+
# Checks if the piece belongs to the current player.
|
49
|
+
# Fixed version that verifies both the game name AND the case.
|
50
|
+
#
|
51
|
+
# This method now performs strict validation by ensuring:
|
52
|
+
# - The game identifier matches exactly with the turn parameter
|
53
|
+
# - The case convention is consistent (uppercase/lowercase players)
|
54
|
+
# - The piece part follows the same case convention as the game part
|
55
|
+
#
|
56
|
+
# @param actor [String] GAN identifier of the piece
|
57
|
+
# @param turn [String] Current player's game identifier
|
58
|
+
#
|
59
|
+
# @return [Boolean] true if the piece belongs to the current player
|
60
|
+
def piece_belongs_to_current_player?(actor, turn)
|
61
|
+
return false unless valid_gan_format?(actor)
|
62
|
+
return false unless valid_game_identifier?(turn)
|
63
|
+
|
64
|
+
game_part, piece_part = actor.split(GAN_SEPARATOR, 2)
|
65
|
+
|
66
|
+
# Strict verification: the game name must match exactly
|
67
|
+
# while considering case to determine the player
|
68
|
+
case turn
|
69
|
+
when turn.upcase
|
70
|
+
# Current player is the uppercase one
|
71
|
+
game_part == turn && game_part == game_part.upcase && piece_part.match?(/\A[-+]?[A-Z][']?\z/)
|
72
|
+
when turn.downcase
|
73
|
+
# Current player is the lowercase one
|
74
|
+
game_part == turn && game_part == game_part.downcase && piece_part.match?(/\A[-+]?[a-z][']?\z/)
|
75
|
+
else
|
76
|
+
# Turn is neither entirely uppercase nor lowercase
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
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
|
+
# Validates the GAN format of an identifier.
|
110
|
+
#
|
111
|
+
# A valid GAN identifier must:
|
112
|
+
# - Be a string containing exactly one colon separator
|
113
|
+
# - Have a valid game identifier before the colon
|
114
|
+
# - Have a valid piece identifier after the colon
|
115
|
+
# - Maintain case consistency between game and piece parts
|
116
|
+
#
|
117
|
+
# @param actor [String] Identifier to validate
|
118
|
+
#
|
119
|
+
# @return [Boolean] true if the format is valid
|
120
|
+
def valid_gan_format?(actor)
|
121
|
+
return false unless actor.is_a?(String)
|
122
|
+
return false unless actor.include?(GAN_SEPARATOR)
|
123
|
+
|
124
|
+
parts = actor.split(GAN_SEPARATOR, 2)
|
125
|
+
return false unless parts.length == 2
|
126
|
+
|
127
|
+
game_part, piece_part = parts
|
128
|
+
|
129
|
+
return false unless valid_game_identifier?(game_part)
|
130
|
+
return false unless valid_piece_identifier?(piece_part)
|
131
|
+
|
132
|
+
# Case consistency verification between game and piece
|
133
|
+
game_is_upper = game_part == game_part.upcase
|
134
|
+
piece_match = piece_part.match(/\A[-+]?([A-Za-z])[']?\z/)
|
135
|
+
return false unless piece_match
|
136
|
+
|
137
|
+
piece_char = piece_match[1]
|
138
|
+
piece_is_upper = piece_char == piece_char.upcase
|
139
|
+
|
140
|
+
game_is_upper == piece_is_upper
|
141
|
+
end
|
142
|
+
|
143
|
+
# Validates a game identifier.
|
144
|
+
#
|
145
|
+
# Game identifiers must be non-empty strings containing only
|
146
|
+
# alphabetic characters, either all uppercase or all lowercase.
|
147
|
+
# Mixed case is not allowed as it breaks the player distinction.
|
148
|
+
#
|
149
|
+
# @param game_id [String] Game identifier to validate
|
150
|
+
#
|
151
|
+
# @return [Boolean] true if the identifier is valid
|
152
|
+
def valid_game_identifier?(game_id)
|
153
|
+
return false unless game_id.is_a?(String)
|
154
|
+
return false if game_id.empty?
|
155
|
+
|
156
|
+
# Must be either entirely uppercase or entirely lowercase
|
157
|
+
game_id.match?(/\A[A-Z]+\z/) || game_id.match?(/\A[a-z]+\z/)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Validates a piece identifier (part after the colon).
|
161
|
+
#
|
162
|
+
# Piece identifiers follow the pattern: [optional prefix][letter][optional suffix]
|
163
|
+
# Where:
|
164
|
+
# - Optional prefix: + or -
|
165
|
+
# - Letter: A-Z or a-z (must match game part case)
|
166
|
+
# - Optional suffix: ' (apostrophe)
|
167
|
+
#
|
168
|
+
# @param piece_id [String] Piece identifier to validate
|
169
|
+
#
|
170
|
+
# @return [Boolean] true if the identifier is valid
|
171
|
+
def valid_piece_identifier?(piece_id)
|
172
|
+
return false unless piece_id.is_a?(String)
|
173
|
+
return false if piece_id.empty?
|
174
|
+
|
175
|
+
# Format: [optional prefix][letter][optional suffix]
|
176
|
+
piece_id.match?(/\A[-+]?[A-Za-z][']?\z/)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sashite
|
4
|
+
module Ggn
|
5
|
+
class Ruleset
|
6
|
+
class Source
|
7
|
+
class Destination
|
8
|
+
class Engine
|
9
|
+
# Represents the result of a valid pseudo-legal move evaluation.
|
10
|
+
#
|
11
|
+
# A Transition encapsulates the changes that occur when a move is executed:
|
12
|
+
# - Board state changes (pieces moving, appearing, or disappearing)
|
13
|
+
# - Pieces gained in hand (from captures)
|
14
|
+
# - Pieces dropped from hand (for drop moves)
|
15
|
+
#
|
16
|
+
# @example Basic move (pawn advance)
|
17
|
+
# transition = Transition.new(nil, nil, "e2" => nil, "e4" => "CHESS:P")
|
18
|
+
# transition.diff # => { "e2" => nil, "e4" => "CHESS:P" }
|
19
|
+
# transition.gain # => nil
|
20
|
+
# transition.drop # => nil
|
21
|
+
#
|
22
|
+
# @example Capture with piece gain
|
23
|
+
# transition = Transition.new("CHESS:R", nil, "g7" => nil, "h8" => "CHESS:Q")
|
24
|
+
# transition.gain # => "CHESS:R" (captured rook goes to hand)
|
25
|
+
#
|
26
|
+
# @example Piece drop from hand
|
27
|
+
# transition = Transition.new(nil, "SHOGI:P", "5e" => "SHOGI:P")
|
28
|
+
# transition.drop # => "SHOGI:P" (pawn removed from hand)
|
29
|
+
class Transition
|
30
|
+
# @return [Hash<String, String|nil>] Board state changes after the move.
|
31
|
+
# Keys are square labels, values are piece identifiers or nil for empty squares.
|
32
|
+
attr_reader :diff
|
33
|
+
|
34
|
+
# @return [String, nil] Piece identifier added to the current player's hand,
|
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.
|
43
|
+
#
|
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
|
+
# @param diff [Hash] Board state changes as keyword arguments.
|
47
|
+
# Keys should be square labels, values should be piece identifiers or nil.
|
48
|
+
#
|
49
|
+
# @example Creating a simple move transition
|
50
|
+
# Transition.new(nil, nil, "e2" => nil, "e4" => "CHESS:P")
|
51
|
+
#
|
52
|
+
# @example Creating a capture transition
|
53
|
+
# Transition.new("CHESS:R", nil, "d4" => nil, "e5" => "CHESS:P")
|
54
|
+
#
|
55
|
+
# @example Creating a drop transition
|
56
|
+
# Transition.new(nil, "SHOGI:P", "3c" => "SHOGI:P")
|
57
|
+
def initialize(gain, drop, **diff)
|
58
|
+
@gain = gain
|
59
|
+
@drop = drop
|
60
|
+
@diff = diff
|
61
|
+
|
62
|
+
freeze
|
63
|
+
end
|
64
|
+
|
65
|
+
# Checks if this transition involves gaining a piece.
|
66
|
+
#
|
67
|
+
# @return [Boolean] true if a piece is gained (typically from capture)
|
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
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|