sashite-ggn 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 8fc9192ea7b3a42e4f8a039bec43da970ee3e4aa
4
- data.tar.gz: 7edd6351985e71da38ab7b8dd0d057bae17aa2ec
2
+ SHA256:
3
+ metadata.gz: 3841392373c865dd1e96e3cd8321cb8720767585d815fc16449f05a00c8d0900
4
+ data.tar.gz: a00ebca510e8af7ff2e77ea8056009ca2198a43d79885a61fd8ea682f6c9aaf3
5
5
  SHA512:
6
- metadata.gz: fe4aa42eac809ad5a93154b3a686c812adb98491c3d8a86ec91ed63403e0bea3b0463017f431cf9bb7942071b88e7244c991648c22928e18096ad15c430d2683
7
- data.tar.gz: a251832c03b303ea0011e87088945f64453b64a1cc19900666beddde7d5cd9043f90dcafe175499f6f983925835a649b6085de9ef8f428e42e28404a2714ea4e
6
+ metadata.gz: 9cc52a786d043b1e2ed437d58f9c939268d23f97df82211732dacf955964bbc27fbc24a3ac280c5cb63a84e6aeaf7fb22aa88e659832020802ac1e78e949f6b5
7
+ data.tar.gz: 35a6f1b1d9d0f5878ebef7c443f675a0c15027d0895ca96c92c69c1f9a6864c152b7c58fb25e57cbb5ff50223a007003eb994ddb265d1e0e67707fa5b692a6b3
data/LICENSE.md CHANGED
@@ -1,22 +1,21 @@
1
- Copyright (c) 2014 Cyril Wack
1
+ # The MIT License
2
2
 
3
- MIT License
3
+ Copyright (c) 2014-2025 Sashité
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
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
- included in all copies or substantial portions of the Software.
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
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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,418 @@
1
- # Sashite::GGN
1
+ # Ggn.rb
2
2
 
3
- A collection of mappers for [GGN](http://sashite.wiki/General_Gameplay_Notation) objects.
3
+ [![Version](https://img.shields.io/github/v/tag/sashite/ggn.rb?label=Version&logo=github)](https://github.com/sashite/ggn.rb/tags)
4
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/ggn.rb/main)
5
+ ![Ruby](https://github.com/sashite/ggn.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
+ [![License](https://img.shields.io/github/license/sashite/ggn.rb?label=License&logo=github)](https://github.com/sashite/ggn.rb/raw/main/LICENSE.md)
4
7
 
5
- ## Status
8
+ > **GGN** (General Gameplay Notation) support for the Ruby language.
6
9
 
7
- * [![Gem Version](https://badge.fury.io/rb/sashite-ggn.svg)](//badge.fury.io/rb/sashite-ggn)
8
- * [![Build Status](https://secure.travis-ci.org/sashite/ggn.rb.svg?branch=master)](//travis-ci.org/sashite/ggn.rb?branch=master)
9
- * [![Dependency Status](https://gemnasium.com/sashite/ggn.rb.svg)](//gemnasium.com/sashite/ggn.rb)
10
+ ## What is GGN?
11
+
12
+ GGN (General Gameplay Notation) is a rule-agnostic, JSON-based format for describing **pseudo-legal moves** in abstract strategy board games. Unlike move notations that express *what* a move does, GGN expresses *whether* that move is **possible** under basic movement constraints.
13
+
14
+ GGN is deliberately silent about higher-level, game-specific legality questions (e.g., check, ko, repetition, castling paths). This neutrality makes the format universal: any engine can pre-compute and share a library of pseudo-legal moves for any mix of games.
15
+
16
+ This gem implements the [GGN Specification v1.0.0](https://sashite.dev/documents/ggn/1.0.0/), providing a Ruby interface for:
17
+
18
+ - Loading and validating GGN JSON documents
19
+ - Querying pseudo-legal moves for specific pieces and positions
20
+ - Evaluating move validity under current board conditions
21
+ - Processing complex move conditions including captures, drops, and promotions
10
22
 
11
23
  ## Installation
12
24
 
13
- Add this line to your application's Gemfile:
25
+ ```ruby
26
+ # In your Gemfile
27
+ gem "sashite-ggn"
28
+ ```
29
+
30
+ Or install manually:
31
+
32
+ ```sh
33
+ gem install sashite-ggn
34
+ ```
35
+
36
+ ## GGN Format
37
+
38
+ A single GGN **entry** answers the question:
39
+
40
+ > Can this piece, currently on this square, reach that square?
41
+
42
+ It encodes:
43
+
44
+ 1. **Which piece** (via GAN identifier)
45
+ 2. **From where** (source square label, or "`*`" for off-board)
46
+ 3. **To where** (destination square label)
47
+ 4. **Which pre-conditions** must hold (`require`)
48
+ 5. **Which pre-conditions** must not hold (`prevent`)
49
+ 6. **Which post-conditions** result (`perform`, plus optional `gain` or `drop`)
50
+
51
+ ### JSON Structure
52
+
53
+ ```json
54
+ {
55
+ "<Source piece GAN>": {
56
+ "<Source square>": {
57
+ "<Destination square>": [
58
+ {
59
+ "require": { "<square>": "<required state>", … },
60
+ "prevent": { "<square>": "<forbidden state>", … },
61
+ "perform": { "<square>": "<new state | null>", … },
62
+ "gain": "<piece GAN>" | null,
63
+ "drop": "<piece GAN>" | null
64
+ }
65
+ ]
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ## Basic Usage
72
+
73
+ ### Loading GGN Data
74
+
75
+ Load GGN data from various sources:
76
+
77
+ ```ruby
78
+ require "sashite-ggn"
79
+
80
+ # From file
81
+ piece_data = Sashite::Ggn.load_file("chess_moves.json")
82
+
83
+ # From JSON string
84
+ json_string = '{"CHESS:P": {"e2": {"e4": [{"require": {"e3": "empty", "e4": "empty"}, "perform": {"e2": null, "e4": "CHESS:P"}}]}}}'
85
+ piece_data = Sashite::Ggn.load_string(json_string)
86
+
87
+ # From Hash
88
+ ggn_hash = { "CHESS:P" => { "e2" => { "e4" => [{ "require" => { "e3" => "empty", "e4" => "empty" }, "perform" => { "e2" => nil, "e4" => "CHESS:P" } }] } } }
89
+ piece_data = Sashite::Ggn.load_hash(ggn_hash)
90
+ ```
91
+
92
+ ### Querying Moves
14
93
 
15
- gem 'sashite-ggn'
94
+ Navigate through the GGN structure to find specific moves:
16
95
 
17
- And then execute:
96
+ ```ruby
97
+ require "sashite-ggn"
98
+
99
+ piece_data = Sashite::Ggn.load_file("chess_moves.json")
100
+
101
+ # Select a piece type
102
+ source = piece_data.select("CHESS:P")
103
+
104
+ # Get destinations from a specific source square
105
+ destinations = source.from("e2")
106
+
107
+ # Get the engine for a specific target square
108
+ engine = destinations.to("e4")
109
+ ```
110
+
111
+ ### Evaluating Move Validity
18
112
 
19
- $ bundle
113
+ Check if a move is valid under current board conditions:
20
114
 
21
- Or install it yourself as:
115
+ ```ruby
116
+ require "sashite-ggn"
117
+
118
+ # Load piece data and get the movement engine
119
+ piece_data = Sashite::Ggn.load_file("chess_moves.json")
120
+ engine = piece_data.select("CHESS:P").from("e2").to("e4")
22
121
 
23
- $ gem install sashite-ggn
122
+ # Define current board state
123
+ board_state = {
124
+ "e2" => "CHESS:P", # White pawn on e2
125
+ "e3" => nil, # Empty square
126
+ "e4" => nil # Empty square
127
+ }
24
128
 
25
- ## Usage
129
+ # Evaluate the move
130
+ result = engine.where(board_state, {}, "CHESS")
131
+
132
+ if result
133
+ puts "Move is valid!"
134
+ puts "Board changes: #{result.diff}"
135
+ # => { "e2" => nil, "e4" => "CHESS:P" }
136
+ puts "Piece gained: #{result.gain}" # => nil (no capture)
137
+ puts "Piece dropped: #{result.drop}" # => nil (not a drop move)
138
+ else
139
+ puts "Move is not valid under current conditions"
140
+ end
141
+ ```
26
142
 
27
- Working with GGN can be very simple, for example:
143
+ ### Handling Captures
144
+
145
+ Process moves that capture enemy pieces:
28
146
 
29
147
  ```ruby
30
- require 'sashite-ggn'
148
+ require "sashite-ggn"
149
+
150
+ # Load piece data for a capture move
151
+ piece_data = Sashite::Ggn.load_file("chess_moves.json")
152
+ engine = piece_data.select("CHESS:P").from("e5").to("d6")
153
+
154
+ # Board state with enemy piece to capture
155
+ board_state = {
156
+ "e5" => "CHESS:P", # Our pawn
157
+ "d6" => "chess:p" # Enemy pawn (lowercase = opponent)
158
+ }
31
159
 
32
- state = Sashite::GGN::State.new
33
- state.last_moved_actor = nil
34
- state.previous_moves_counter = nil
160
+ result = engine.where(board_state, {}, "CHESS")
35
161
 
36
- subject = Sashite::GGN::Subject.new
37
- subject.ally = true
38
- subject.actor = :self
39
- subject.state = state
162
+ if result
163
+ puts "Capture is valid!"
164
+ puts "Board changes: #{result.diff}"
165
+ # => { "e5" => nil, "d6" => "CHESS:P" }
166
+ puts "Captured piece: #{result.gain}" # => "CHESS:P" (gained in hand)
167
+ end
40
168
  ```
41
169
 
42
- ## Contributing
170
+ ### Piece Drops (Shogi-style)
171
+
172
+ Handle dropping pieces from hand onto the board:
173
+
174
+ ```ruby
175
+ require "sashite-ggn"
176
+
177
+ # Load Shogi piece data
178
+ piece_data = Sashite::Ggn.load_file("shogi_moves.json")
179
+ engine = piece_data.select("SHOGI:P").from("*").to("5e")
180
+
181
+ # Player has captured pawns available
182
+ captures = { "SHOGI:P" => 2 }
183
+
184
+ # Current board state (5th file is clear of unpromoted pawns)
185
+ board_state = {
186
+ "5e" => nil, # Target square is empty
187
+ "5a" => nil, "5b" => nil, "5c" => nil, "5d" => nil,
188
+ "5f" => nil, "5g" => nil, "5h" => nil, "5i" => nil
189
+ }
190
+
191
+ result = engine.where(board_state, captures, "SHOGI")
192
+
193
+ if result
194
+ puts "Pawn drop is valid!"
195
+ puts "Board changes: #{result.diff}" # => { "5e" => "SHOGI:P" }
196
+ puts "Piece dropped from hand: #{result.drop}" # => "SHOGI:P"
197
+ end
198
+ ```
199
+
200
+ ## Validation
201
+
202
+ ### Schema Validation
203
+
204
+ Validate GGN data against the official JSON Schema:
205
+
206
+ ```ruby
207
+ require "sashite-ggn"
208
+
209
+ # Validate during loading (default behavior)
210
+ begin
211
+ piece_data = Sashite::Ggn.load_file("moves.json")
212
+ puts "GGN data is valid!"
213
+ rescue Sashite::Ggn::ValidationError => e
214
+ puts "Validation failed: #{e.message}"
215
+ end
216
+
217
+ # Skip validation for performance (large files)
218
+ piece_data = Sashite::Ggn.load_file("large_moves.json", validate: false)
219
+
220
+ # Validate manually
221
+ begin
222
+ Sashite::Ggn.validate!(my_data)
223
+ puts "Data is valid"
224
+ rescue Sashite::Ggn::ValidationError => e
225
+ puts "Invalid: #{e.message}"
226
+ end
227
+
228
+ # Check validity without exceptions
229
+ if Sashite::Ggn.valid?(my_data)
230
+ puts "Data is valid"
231
+ else
232
+ errors = Sashite::Ggn.validation_errors(my_data)
233
+ puts "Validation errors: #{errors.join(', ')}"
234
+ end
235
+ ```
236
+
237
+ ## Occupation States
238
+
239
+ GGN recognizes several occupation states for move conditions:
240
+
241
+ | State | Meaning |
242
+ | ---------------- | ---------------------------------------------------------------------------- |
243
+ | `"empty"` | Square must be empty |
244
+ | `"enemy"` | Square must contain a standard opposing piece |
245
+ | *GAN identifier* | Square must contain **exactly** the specified piece |
246
+
247
+ ### Implicit States
248
+
249
+ Through the `prevent` field, additional states can be expressed:
250
+
251
+ | Implicit State | Expression | Meaning |
252
+ | ---------------- | ---------------------------- | -------------------------------------------------------- |
253
+ | `"occupied"` | `"prevent": { "a1": "empty" }` | Square must be occupied by any piece |
254
+ | `"ally"` | `"prevent": { "a1": "enemy" }` | Square must contain a friendly piece |
255
+
256
+ ## Examples
257
+
258
+ ### Simple Move
259
+
260
+ A piece moving from one square to another without conditions:
261
+
262
+ ```json
263
+ {
264
+ "CHESS:K": {
265
+ "e1": {
266
+ "e2": [
267
+ {
268
+ "perform": { "e1": null, "e2": "CHESS:K" }
269
+ }
270
+ ]
271
+ }
272
+ }
273
+ }
274
+ ```
275
+
276
+ ### Sliding Move
277
+
278
+ A piece that slides along empty squares:
279
+
280
+ ```json
281
+ {
282
+ "CHESS:R": {
283
+ "a1": {
284
+ "a3": [
285
+ {
286
+ "require": { "a2": "empty", "a3": "empty" },
287
+ "perform": { "a1": null, "a3": "CHESS:R" }
288
+ }
289
+ ]
290
+ }
291
+ }
292
+ }
293
+ ```
294
+
295
+ ### Capture with Gain
296
+
297
+ A piece capturing an enemy and gaining it in hand:
298
+
299
+ ```json
300
+ {
301
+ "SHOGI:P": {
302
+ "5f": {
303
+ "5e": [
304
+ {
305
+ "require": { "5e": "enemy" },
306
+ "perform": { "5f": null, "5e": "SHOGI:P" },
307
+ "gain": "SHOGI:P"
308
+ }
309
+ ]
310
+ }
311
+ }
312
+ }
313
+ ```
314
+
315
+ ### Piece Drop
316
+
317
+ Dropping a piece from hand onto the board:
318
+
319
+ ```json
320
+ {
321
+ "SHOGI:P": {
322
+ "*": {
323
+ "5e": [
324
+ {
325
+ "require": { "5e": "empty" },
326
+ "prevent": {
327
+ "5a": "SHOGI:P", "5b": "SHOGI:P", "5c": "SHOGI:P",
328
+ "5d": "SHOGI:P", "5f": "SHOGI:P", "5g": "SHOGI:P",
329
+ "5h": "SHOGI:P", "5i": "SHOGI:P"
330
+ },
331
+ "perform": { "5e": "SHOGI:P" },
332
+ "drop": "SHOGI:P"
333
+ }
334
+ ]
335
+ }
336
+ }
337
+ }
338
+ ```
339
+
340
+ ### Promotion
341
+
342
+ A piece moving and changing to a different piece type:
343
+
344
+ ```json
345
+ {
346
+ "CHESS:P": {
347
+ "g7": {
348
+ "g8": [
349
+ {
350
+ "require": { "g8": "empty" },
351
+ "perform": { "g7": null, "g8": "CHESS:Q" }
352
+ }
353
+ ]
354
+ }
355
+ }
356
+ }
357
+ ```
358
+
359
+ ## Error Handling
360
+
361
+ The library provides comprehensive error handling:
362
+
363
+ ```ruby
364
+ require "sashite-ggn"
365
+
366
+ begin
367
+ # Various operations that might fail
368
+ piece_data = Sashite::Ggn.load_file("nonexistent.json")
369
+ source = piece_data.select("INVALID:PIECE")
370
+ destinations = source.from("invalid_square")
371
+ engine = destinations.to("another_invalid")
372
+ result = engine.where({}, {}, "")
373
+ rescue Sashite::Ggn::ValidationError => e
374
+ puts "GGN validation error: #{e.message}"
375
+ rescue KeyError => e
376
+ puts "Key not found: #{e.message}"
377
+ rescue ArgumentError => e
378
+ puts "Invalid argument: #{e.message}"
379
+ end
380
+ ```
381
+
382
+ ## Performance Considerations
383
+
384
+ For large GGN files or high-frequency operations:
385
+
386
+ ```ruby
387
+ # Skip validation for better performance
388
+ piece_data = Sashite::Ggn.load_file("large_dataset.json", validate: false)
389
+
390
+ # Cache frequently used engines
391
+ @engines = {}
392
+ def get_engine(piece, from, to)
393
+ key = "#{piece}:#{from}:#{to}"
394
+ @engines[key] ||= piece_data.select(piece).from(from).to(to)
395
+ end
396
+ ```
397
+
398
+ ## Related Specifications
399
+
400
+ GGN works alongside other Sashité specifications:
401
+
402
+ - **[GAN](https://sashite.dev/documents/gan/1.0.0/)** (General Actor Notation): Unique piece identifiers
403
+ - **[FEEN](https://sashite.dev/documents/feen/1.0.0/)** (Forsyth-Edwards Enhanced Notation): Board position representation
404
+ - **[PMN](https://sashite.dev/documents/pmn/1.0.0/)** (Portable Move Notation): Move sequence representation
405
+
406
+ ## Documentation
407
+
408
+ - [Official GGN Specification](https://sashite.dev/documents/ggn/1.0.0/)
409
+ - [JSON Schema](https://sashite.dev/schemas/ggn/1.0.0/schema.json)
410
+ - [API Documentation](https://rubydoc.info/github/sashite/ggn.rb/main)
411
+
412
+ ## License
413
+
414
+ 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).
415
+
416
+ ## About Sashité
43
417
 
44
- 1. Fork it
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
418
+ 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,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sashite
4
+ module Ggn
5
+ class Piece
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