sashite-ggn 0.8.0 → 0.9.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 +74 -61
- data/lib/sashite/ggn/ruleset/source/destination/engine.rb +40 -106
- data/lib/sashite/ggn/ruleset/source/destination.rb +4 -18
- data/lib/sashite/ggn/ruleset/source.rb +4 -13
- data/lib/sashite/ggn/ruleset.rb +14 -198
- data/lib/sashite/ggn.rb +166 -7
- metadata +1 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d3ac56b5ebc6dd1d9f3d9f559de876d0bafa5c37158b5aa687627f3a8c7d9e95
|
4
|
+
data.tar.gz: bde9b0445dd893eecb34a3d61c647b659766712786aedb34caa801468dcf5472
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 75c93971040f868aa0eb128b2a5d3f67de321efe2c943d56faa68155ecdc8e3ca2c582fa848d6f15138451bc2144ffd10af6a88535d0faa3831fc7441a9377ee
|
7
|
+
data.tar.gz: bb1d1e65bd526f8d459a1c4e96a69d97eaaeed82f1268bae72a68c4d1778875088c2fb67bd0ae45cd2663444b19a67c83ce86467ce915c78addb77790caaa7a2
|
data/README.md
CHANGED
@@ -52,7 +52,6 @@ GGN builds upon foundational Sashité specifications:
|
|
52
52
|
|
53
53
|
```ruby
|
54
54
|
gem "sashite-cell" # Coordinate Encoding for Layered Locations
|
55
|
-
gem "sashite-feen" # Forsyth–Edwards Enhanced Notation
|
56
55
|
gem "sashite-hand" # Hold And Notation Designator
|
57
56
|
gem "sashite-lcn" # Location Condition Notation
|
58
57
|
gem "sashite-qpi" # Qualified Piece Identifier
|
@@ -66,7 +65,7 @@ gem "sashite-stn" # State Transition Notation
|
|
66
65
|
```ruby
|
67
66
|
require "sashite/ggn"
|
68
67
|
|
69
|
-
#
|
68
|
+
# Define GGN data structure
|
70
69
|
ggn_data = {
|
71
70
|
"C:P" => {
|
72
71
|
"e2" => {
|
@@ -84,12 +83,26 @@ ggn_data = {
|
|
84
83
|
}
|
85
84
|
}
|
86
85
|
|
86
|
+
# Validate GGN structure
|
87
|
+
Sashite::Ggn.valid?(ggn_data) # => true
|
88
|
+
|
89
|
+
# Parse into ruleset
|
87
90
|
ruleset = Sashite::Ggn.parse(ggn_data)
|
88
91
|
|
89
92
|
# Query movement possibility through method chaining
|
90
|
-
|
91
|
-
|
93
|
+
source = ruleset.select("C:P")
|
94
|
+
destination = source.from("e2")
|
95
|
+
engine = destination.to("e4")
|
96
|
+
|
97
|
+
# Evaluate against position
|
98
|
+
active_side = :first
|
99
|
+
squares = {
|
100
|
+
"e2" => "C:P",
|
101
|
+
"e3" => nil,
|
102
|
+
"e4" => nil
|
103
|
+
}
|
92
104
|
|
105
|
+
transitions = engine.where(active_side, squares)
|
93
106
|
transitions.any? # => true
|
94
107
|
```
|
95
108
|
|
@@ -152,22 +165,6 @@ source = ruleset.select("C:K")
|
|
152
165
|
|
153
166
|
---
|
154
167
|
|
155
|
-
#### `#pseudo_legal_transitions(feen) → Array<Array>`
|
156
|
-
|
157
|
-
Generates all pseudo-legal moves for the given position.
|
158
|
-
|
159
|
-
```ruby
|
160
|
-
moves = ruleset.pseudo_legal_transitions(feen)
|
161
|
-
# => [["C:P", "e2", "e4", [#<Transition...>]], ...]
|
162
|
-
```
|
163
|
-
|
164
|
-
**Parameters:**
|
165
|
-
- `feen` (String): Position in FEEN format
|
166
|
-
|
167
|
-
**Returns:** `Array<Array>` — Array of `[piece, source, destination, transitions]` tuples
|
168
|
-
|
169
|
-
---
|
170
|
-
|
171
168
|
#### `#piece?(piece) → Boolean`
|
172
169
|
|
173
170
|
Checks if ruleset contains movement rules for specified piece.
|
@@ -195,18 +192,6 @@ ruleset.pieces # => ["C:K", "C:Q", "C:P", ...]
|
|
195
192
|
|
196
193
|
---
|
197
194
|
|
198
|
-
#### `#to_h → Hash`
|
199
|
-
|
200
|
-
Converts ruleset to hash representation.
|
201
|
-
|
202
|
-
```ruby
|
203
|
-
ruleset.to_h # => { "C:K" => { "e1" => { "e2" => [...] } } }
|
204
|
-
```
|
205
|
-
|
206
|
-
**Returns:** `Hash` — GGN data structure
|
207
|
-
|
208
|
-
---
|
209
|
-
|
210
195
|
### `Sashite::Ggn::Ruleset::Source` Class
|
211
196
|
|
212
197
|
Represents movement possibilities for a piece type.
|
@@ -307,34 +292,29 @@ destination.destination?("e2") # => true
|
|
307
292
|
|
308
293
|
Evaluates movement possibility under given position conditions.
|
309
294
|
|
310
|
-
#### `#where(
|
295
|
+
#### `#where(active_side, squares) → Array<Transition>`
|
311
296
|
|
312
297
|
Evaluates movement against position and returns valid transitions.
|
313
298
|
|
314
299
|
```ruby
|
315
|
-
|
300
|
+
active_side = :first
|
301
|
+
squares = {
|
302
|
+
"e2" => "C:P", # White pawn on e2
|
303
|
+
"e3" => nil, # Empty square
|
304
|
+
"e4" => nil # Empty square
|
305
|
+
}
|
306
|
+
|
307
|
+
transitions = engine.where(active_side, squares)
|
316
308
|
```
|
317
309
|
|
318
310
|
**Parameters:**
|
319
|
-
- `
|
311
|
+
- `active_side` (Symbol): Active player side (`:first` or `:second`)
|
312
|
+
- `squares` (Hash): Board state where keys are CELL coordinates and values are QPI identifiers or `nil` for empty squares
|
320
313
|
|
321
314
|
**Returns:** `Array<Sashite::Stn::Transition>` — Valid state transitions (may be empty)
|
322
315
|
|
323
316
|
---
|
324
317
|
|
325
|
-
#### `#possibilities → Array<Hash>`
|
326
|
-
|
327
|
-
Returns raw movement possibility rules.
|
328
|
-
|
329
|
-
```ruby
|
330
|
-
engine.possibilities
|
331
|
-
# => [{ "must" => {...}, "deny" => {...}, "diff" => {...} }]
|
332
|
-
```
|
333
|
-
|
334
|
-
**Returns:** `Array<Hash>` — Movement possibility specifications
|
335
|
-
|
336
|
-
---
|
337
|
-
|
338
318
|
## GGN Format
|
339
319
|
|
340
320
|
### Structure
|
@@ -374,28 +354,65 @@ engine.possibilities
|
|
374
354
|
|
375
355
|
```ruby
|
376
356
|
# Query specific movement
|
377
|
-
|
357
|
+
active_side = :first
|
358
|
+
squares = {
|
359
|
+
"e2" => "C:P",
|
360
|
+
"e3" => nil,
|
361
|
+
"e4" => nil
|
362
|
+
}
|
378
363
|
|
379
364
|
transitions = ruleset
|
380
365
|
.select("C:P")
|
381
366
|
.from("e2")
|
382
367
|
.to("e4")
|
383
|
-
.where(
|
368
|
+
.where(active_side, squares)
|
384
369
|
|
385
370
|
transitions.size # => 1
|
386
371
|
transitions.first.board_changes # => { "e2" => nil, "e4" => "C:P" }
|
387
372
|
```
|
388
373
|
|
389
|
-
###
|
374
|
+
### Building Board State
|
390
375
|
|
391
376
|
```ruby
|
377
|
+
# Example: Build squares hash from FEEN position
|
378
|
+
require "sashite/feen"
|
379
|
+
|
392
380
|
feen = "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c"
|
381
|
+
position = Sashite::Feen.parse(feen)
|
382
|
+
|
383
|
+
# Extract active player side
|
384
|
+
active_side = position.styles.active.side # => :first
|
385
|
+
|
386
|
+
# Build squares hash from placement
|
387
|
+
squares = {}
|
388
|
+
position.placement.ranks.each_with_index do |rank, rank_idx|
|
389
|
+
rank.each_with_index do |piece, file_idx|
|
390
|
+
# Convert rank_idx and file_idx to CELL coordinate
|
391
|
+
cell = Sashite::Cell.from_indices(file_idx, 7 - rank_idx)
|
392
|
+
squares[cell] = piece&.to_s
|
393
|
+
end
|
394
|
+
end
|
393
395
|
|
394
|
-
|
396
|
+
# Use with GGN
|
397
|
+
transitions = engine.where(active_side, squares)
|
398
|
+
```
|
395
399
|
|
396
|
-
|
397
|
-
|
398
|
-
|
400
|
+
### Capture Validation
|
401
|
+
|
402
|
+
```ruby
|
403
|
+
# Check capture possibility
|
404
|
+
active_side = :first
|
405
|
+
squares = {
|
406
|
+
"e4" => "C:P", # White pawn
|
407
|
+
"d5" => "c:p", # Black pawn (enemy)
|
408
|
+
"f5" => "c:p" # Black pawn (enemy)
|
409
|
+
}
|
410
|
+
|
411
|
+
# Pawn can capture diagonally
|
412
|
+
capture_engine = ruleset.select("C:P").from("e4").to("d5")
|
413
|
+
transitions = capture_engine.where(active_side, squares)
|
414
|
+
|
415
|
+
transitions.any? # => true if capture is allowed
|
399
416
|
```
|
400
417
|
|
401
418
|
### Existence Checks
|
@@ -424,10 +441,6 @@ source.sources # => ["e1", "d1", "f1", ...]
|
|
424
441
|
|
425
442
|
# List destinations from a source
|
426
443
|
destination.destinations # => ["d1", "d2", "e2", "f2", "f1"]
|
427
|
-
|
428
|
-
# Access raw possibilities
|
429
|
-
engine.possibilities
|
430
|
-
# => [{ "must" => {...}, "deny" => {...}, "diff" => {...} }]
|
431
444
|
```
|
432
445
|
|
433
446
|
---
|
@@ -437,8 +450,9 @@ engine.possibilities
|
|
437
450
|
- **Functional**: Pure functions with no side effects
|
438
451
|
- **Immutable**: All data structures frozen and unchangeable
|
439
452
|
- **Composable**: Clean method chaining for natural query flow
|
453
|
+
- **Minimal API**: Only exposes what's necessary
|
440
454
|
- **Type-safe**: Strict validation of all inputs
|
441
|
-
- **
|
455
|
+
- **Lightweight**: Minimal dependencies, no unnecessary parsing
|
442
456
|
- **Spec-compliant**: Strictly follows GGN v1.0.0 specification
|
443
457
|
|
444
458
|
---
|
@@ -481,7 +495,6 @@ end
|
|
481
495
|
|
482
496
|
- [GGN v1.0.0](https://sashite.dev/specs/ggn/1.0.0/) — General Gameplay Notation specification
|
483
497
|
- [CELL v1.0.0](https://sashite.dev/specs/cell/1.0.0/) — Coordinate encoding
|
484
|
-
- [FEEN v1.0.0](https://sashite.dev/specs/feen/1.0.0/) — Position notation
|
485
498
|
- [HAND v1.0.0](https://sashite.dev/specs/hand/1.0.0/) — Reserve notation
|
486
499
|
- [LCN v1.0.0](https://sashite.dev/specs/lcn/1.0.0/) — Location conditions
|
487
500
|
- [QPI v1.0.0](https://sashite.dev/specs/qpi/1.0.0/) — Piece identification
|
@@ -1,8 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "sashite/cell"
|
4
|
-
require "sashite/epin"
|
5
|
-
require "sashite/feen"
|
6
3
|
require "sashite/lcn"
|
7
4
|
require "sashite/qpi"
|
8
5
|
require "sashite/stn"
|
@@ -16,166 +13,103 @@ module Sashite
|
|
16
13
|
#
|
17
14
|
# @see https://sashite.dev/specs/ggn/1.0.0/
|
18
15
|
class Engine
|
19
|
-
# @return [String] The QPI piece identifier
|
20
|
-
attr_reader :piece
|
21
|
-
|
22
|
-
# @return [String] The source location
|
23
|
-
attr_reader :source
|
24
|
-
|
25
|
-
# @return [String] The destination location
|
26
|
-
attr_reader :destination
|
27
|
-
|
28
|
-
# @return [Array<Hash>] The movement possibilities
|
29
|
-
attr_reader :data
|
30
|
-
|
31
16
|
# Create a new Engine
|
32
17
|
#
|
33
|
-
# @param
|
34
|
-
|
35
|
-
|
36
|
-
# @param data [Array<Hash>] Movement possibilities data
|
37
|
-
def initialize(piece, source, destination, data)
|
38
|
-
@piece = piece
|
39
|
-
@source = source
|
40
|
-
@destination = destination
|
41
|
-
@data = data
|
42
|
-
|
18
|
+
# @param possibilities [Array<Hash>] Movement possibilities data
|
19
|
+
def initialize(*possibilities)
|
20
|
+
@possibilities = possibilities
|
43
21
|
freeze
|
44
22
|
end
|
45
23
|
|
46
24
|
# Evaluate movement against position and return valid transitions
|
47
25
|
#
|
48
|
-
# @param
|
26
|
+
# @param active_side [Symbol] Active player side (:first or :second)
|
27
|
+
# @param squares [Hash{String => String, nil}] Board state where keys are CELL coordinates
|
28
|
+
# and values are QPI identifiers or nil for empty squares
|
49
29
|
# @return [Array<Sashite::Stn::Transition>] Valid state transitions (may be empty)
|
50
30
|
#
|
51
31
|
# @example
|
52
|
-
#
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
32
|
+
# active_side = :first
|
33
|
+
# squares = {
|
34
|
+
# "e2" => "C:P",
|
35
|
+
# "e3" => nil,
|
36
|
+
# "e4" => nil
|
37
|
+
# }
|
38
|
+
# transitions = engine.where(active_side, squares)
|
39
|
+
def where(active_side, squares)
|
40
|
+
@possibilities.select do |possibility|
|
41
|
+
satisfies_must?(possibility["must"], active_side, squares) &&
|
42
|
+
satisfies_deny?(possibility["deny"], active_side, squares)
|
60
43
|
end.map do |possibility|
|
61
44
|
Stn.parse(possibility["diff"])
|
62
45
|
end
|
63
46
|
end
|
64
47
|
|
65
|
-
# Return raw movement possibility rules
|
66
|
-
#
|
67
|
-
# @return [Array<Hash>] Movement possibility specifications
|
68
|
-
#
|
69
|
-
# @example
|
70
|
-
# engine.possibilities
|
71
|
-
# # => [{ "must" => {...}, "deny" => {...}, "diff" => {...} }]
|
72
|
-
def possibilities
|
73
|
-
data
|
74
|
-
end
|
75
|
-
|
76
48
|
private
|
77
49
|
|
78
50
|
# Check if all 'must' conditions are satisfied
|
79
51
|
#
|
80
52
|
# @param conditions [Hash] LCN conditions
|
81
|
-
# @param
|
82
|
-
# @param
|
53
|
+
# @param active_side [Symbol] Active player side
|
54
|
+
# @param squares [Hash] Board state
|
83
55
|
# @return [Boolean]
|
84
|
-
def satisfies_must?(conditions,
|
56
|
+
def satisfies_must?(conditions, active_side, squares)
|
85
57
|
return true if conditions.empty?
|
86
58
|
|
87
59
|
lcn_conditions = Lcn.parse(conditions)
|
88
60
|
|
89
61
|
lcn_conditions.locations.all? do |location|
|
90
62
|
expected_state = lcn_conditions[location]
|
91
|
-
check_condition(location, expected_state,
|
63
|
+
check_condition(location.to_s, expected_state, active_side, squares)
|
92
64
|
end
|
93
65
|
end
|
94
66
|
|
95
67
|
# Check if all 'deny' conditions are not satisfied
|
96
68
|
#
|
97
69
|
# @param conditions [Hash] LCN conditions
|
98
|
-
# @param
|
99
|
-
# @param
|
70
|
+
# @param active_side [Symbol] Active player side
|
71
|
+
# @param squares [Hash] Board state
|
100
72
|
# @return [Boolean]
|
101
|
-
def satisfies_deny?(conditions,
|
73
|
+
def satisfies_deny?(conditions, active_side, squares)
|
102
74
|
return true if conditions.empty?
|
103
75
|
|
104
76
|
lcn_conditions = Lcn.parse(conditions)
|
105
77
|
|
106
78
|
lcn_conditions.locations.none? do |location|
|
107
79
|
expected_state = lcn_conditions[location]
|
108
|
-
check_condition(location, expected_state,
|
80
|
+
check_condition(location.to_s, expected_state, active_side, squares)
|
109
81
|
end
|
110
82
|
end
|
111
83
|
|
112
84
|
# Check if a location satisfies a condition
|
113
85
|
#
|
114
|
-
# @param location [
|
86
|
+
# @param location [String] Location to check (CELL coordinate)
|
115
87
|
# @param expected_state [String] Expected state value
|
116
|
-
# @param
|
117
|
-
# @param
|
88
|
+
# @param active_side [Symbol] Active player side
|
89
|
+
# @param squares [Hash] Board state
|
118
90
|
# @return [Boolean]
|
119
|
-
def check_condition(location, expected_state,
|
120
|
-
|
121
|
-
epin_value = get_piece_at(position, location_str)
|
91
|
+
def check_condition(location, expected_state, active_side, squares)
|
92
|
+
actual_qpi = squares[location]
|
122
93
|
|
123
94
|
case expected_state
|
124
95
|
when "empty"
|
125
|
-
|
96
|
+
actual_qpi.nil?
|
126
97
|
when "enemy"
|
127
|
-
|
98
|
+
actual_qpi && enemy?(actual_qpi, active_side)
|
128
99
|
else
|
129
|
-
# Expected state is a QPI identifier
|
130
|
-
|
100
|
+
# Expected state is a QPI identifier
|
101
|
+
actual_qpi == expected_state
|
131
102
|
end
|
132
103
|
end
|
133
104
|
|
134
|
-
#
|
105
|
+
# Check if a piece is an enemy relative to active side
|
135
106
|
#
|
136
|
-
# @param
|
137
|
-
# @param
|
138
|
-
# @return [Object, nil] EPIN value or nil if empty
|
139
|
-
def get_piece_at(position, location)
|
140
|
-
indices = Cell.to_indices(location)
|
141
|
-
col_index = indices[0]
|
142
|
-
row_index_from_bottom = indices[1]
|
143
|
-
|
144
|
-
# FEEN ranks are stored top-to-bottom, but CELL indices are bottom-up
|
145
|
-
# Need to invert the rank index
|
146
|
-
total_ranks = position.placement.ranks.size
|
147
|
-
rank_index = total_ranks - 1 - row_index_from_bottom
|
148
|
-
|
149
|
-
position.placement.ranks[rank_index][col_index]
|
150
|
-
end
|
151
|
-
|
152
|
-
# Check if a piece is an enemy relative to reference side
|
153
|
-
#
|
154
|
-
# @param epin_value [Object] EPIN value from ranks
|
155
|
-
# @param reference_side [Symbol] Reference side
|
107
|
+
# @param qpi_str [String] QPI identifier
|
108
|
+
# @param active_side [Symbol] Active player side
|
156
109
|
# @return [Boolean]
|
157
|
-
def
|
158
|
-
|
159
|
-
piece_side
|
160
|
-
piece_side != reference_side
|
161
|
-
end
|
162
|
-
|
163
|
-
# Check if EPIN matches QPI identifier
|
164
|
-
#
|
165
|
-
# @param epin_value [Object] EPIN value from ranks
|
166
|
-
# @param qpi_str [String] QPI identifier to match
|
167
|
-
# @return [Boolean]
|
168
|
-
def matches_qpi?(epin_value, qpi_str)
|
169
|
-
epin_str = epin_value.to_s
|
170
|
-
|
171
|
-
# Extract EPIN part from QPI (after the colon)
|
172
|
-
qpi_parts = qpi_str.split(":")
|
173
|
-
return false if qpi_parts.length != 2
|
174
|
-
|
175
|
-
expected_epin = qpi_parts[1]
|
176
|
-
|
177
|
-
# Direct comparison of EPIN strings
|
178
|
-
epin_str == expected_epin
|
110
|
+
def enemy?(qpi_str, active_side)
|
111
|
+
piece_side = Qpi.parse(qpi_str).side
|
112
|
+
piece_side != active_side
|
179
113
|
end
|
180
114
|
end
|
181
115
|
end
|
@@ -10,25 +10,11 @@ module Sashite
|
|
10
10
|
#
|
11
11
|
# @see https://sashite.dev/specs/ggn/1.0.0/
|
12
12
|
class Destination
|
13
|
-
# @return [String] The QPI piece identifier
|
14
|
-
attr_reader :piece
|
15
|
-
|
16
|
-
# @return [String] The source location
|
17
|
-
attr_reader :source
|
18
|
-
|
19
|
-
# @return [Hash] The destinations data
|
20
|
-
attr_reader :data
|
21
|
-
|
22
13
|
# Create a new Destination
|
23
14
|
#
|
24
|
-
# @param piece [String] QPI piece identifier
|
25
|
-
# @param source [String] Source location
|
26
15
|
# @param data [Hash] Destinations data structure
|
27
|
-
def initialize(
|
28
|
-
@piece = piece
|
29
|
-
@source = source
|
16
|
+
def initialize(data)
|
30
17
|
@data = data
|
31
|
-
|
32
18
|
freeze
|
33
19
|
end
|
34
20
|
|
@@ -43,7 +29,7 @@ module Sashite
|
|
43
29
|
def to(destination)
|
44
30
|
raise ::KeyError, "Destination not found: #{destination}" unless destination?(destination)
|
45
31
|
|
46
|
-
Engine.new(
|
32
|
+
Engine.new(*@data.fetch(destination))
|
47
33
|
end
|
48
34
|
|
49
35
|
# Return all valid destinations from this source
|
@@ -53,7 +39,7 @@ module Sashite
|
|
53
39
|
# @example
|
54
40
|
# destination.destinations # => ["d1", "d2", "e2", "f2", "f1"]
|
55
41
|
def destinations
|
56
|
-
data.keys
|
42
|
+
@data.keys
|
57
43
|
end
|
58
44
|
|
59
45
|
# Check if location is a valid destination from this source
|
@@ -64,7 +50,7 @@ module Sashite
|
|
64
50
|
# @example
|
65
51
|
# destination.destination?("e2") # => true
|
66
52
|
def destination?(location)
|
67
|
-
data.key?(location)
|
53
|
+
@data.key?(location)
|
68
54
|
end
|
69
55
|
end
|
70
56
|
end
|
@@ -9,20 +9,11 @@ module Sashite
|
|
9
9
|
#
|
10
10
|
# @see https://sashite.dev/specs/ggn/1.0.0/
|
11
11
|
class Source
|
12
|
-
# @return [String] The QPI piece identifier
|
13
|
-
attr_reader :piece
|
14
|
-
|
15
|
-
# @return [Hash] The sources data
|
16
|
-
attr_reader :data
|
17
|
-
|
18
12
|
# Create a new Source
|
19
13
|
#
|
20
|
-
# @param piece [String] QPI piece identifier
|
21
14
|
# @param data [Hash] Sources data structure
|
22
|
-
def initialize(
|
23
|
-
@piece = piece
|
15
|
+
def initialize(data)
|
24
16
|
@data = data
|
25
|
-
|
26
17
|
freeze
|
27
18
|
end
|
28
19
|
|
@@ -37,7 +28,7 @@ module Sashite
|
|
37
28
|
def from(source)
|
38
29
|
raise ::KeyError, "Source not found: #{source}" unless source?(source)
|
39
30
|
|
40
|
-
Destination.new(
|
31
|
+
Destination.new(@data.fetch(source))
|
41
32
|
end
|
42
33
|
|
43
34
|
# Return all valid source locations for this piece
|
@@ -47,7 +38,7 @@ module Sashite
|
|
47
38
|
# @example
|
48
39
|
# source.sources # => ["e1", "d1", "*"]
|
49
40
|
def sources
|
50
|
-
data.keys
|
41
|
+
@data.keys
|
51
42
|
end
|
52
43
|
|
53
44
|
# Check if location is a valid source for this piece
|
@@ -58,7 +49,7 @@ module Sashite
|
|
58
49
|
# @example
|
59
50
|
# source.source?("e1") # => true
|
60
51
|
def source?(location)
|
61
|
-
data.key?(location)
|
52
|
+
@data.key?(location)
|
62
53
|
end
|
63
54
|
end
|
64
55
|
end
|
data/lib/sashite/ggn/ruleset.rb
CHANGED
@@ -1,36 +1,28 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "sashite/cell"
|
4
|
-
require "sashite/hand"
|
5
|
-
require "sashite/lcn"
|
6
|
-
require "sashite/qpi"
|
7
|
-
require "sashite/stn"
|
8
|
-
|
9
3
|
require_relative "ruleset/source"
|
10
4
|
|
11
5
|
module Sashite
|
12
6
|
module Ggn
|
13
7
|
# Immutable container for GGN movement rules
|
14
8
|
#
|
9
|
+
# @note Instances are created through {Sashite::Ggn.parse}, which handles validation.
|
10
|
+
# The constructor itself does not validate.
|
11
|
+
#
|
15
12
|
# @see https://sashite.dev/specs/ggn/1.0.0/
|
16
13
|
class Ruleset
|
17
|
-
# @return [Hash] The underlying GGN data structure
|
18
|
-
attr_reader :data
|
19
|
-
|
20
14
|
# Create a new Ruleset from GGN data structure
|
21
15
|
#
|
22
|
-
# @
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
16
|
+
# @note This constructor does not validate the data structure.
|
17
|
+
# Use {Sashite::Ggn.parse} or {Sashite::Ggn.valid?} for validation.
|
18
|
+
#
|
19
|
+
# @param data [Hash] GGN data structure (pre-validated)
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# # Don't use directly - use Sashite::Ggn.parse instead
|
23
|
+
# ruleset = Sashite::Ggn::Ruleset.new(data)
|
30
24
|
def initialize(data)
|
31
|
-
validate_structure!(data)
|
32
25
|
@data = data
|
33
|
-
|
34
26
|
freeze
|
35
27
|
end
|
36
28
|
|
@@ -45,39 +37,7 @@ module Sashite
|
|
45
37
|
def select(piece)
|
46
38
|
raise ::KeyError, "Piece not found: #{piece}" unless piece?(piece)
|
47
39
|
|
48
|
-
Source.new(
|
49
|
-
end
|
50
|
-
|
51
|
-
# Generate all pseudo-legal moves for the given position
|
52
|
-
#
|
53
|
-
# @note This method evaluates all possible moves in the ruleset.
|
54
|
-
# For large rulesets, consider filtering by active pieces first.
|
55
|
-
#
|
56
|
-
# @param feen [String] Position in FEEN format
|
57
|
-
# @return [Array<Array(String, String, String, Array<Sashite::Stn::Transition>)>]
|
58
|
-
# Array of tuples containing:
|
59
|
-
# - piece (String): QPI identifier
|
60
|
-
# - source (String): CELL coordinate or HAND "*"
|
61
|
-
# - destination (String): CELL coordinate or HAND "*"
|
62
|
-
# - transitions (Array<Sashite::Stn::Transition>): Valid state transitions
|
63
|
-
#
|
64
|
-
# @example
|
65
|
-
# moves = ruleset.pseudo_legal_transitions(feen)
|
66
|
-
def pseudo_legal_transitions(feen)
|
67
|
-
pieces.flat_map do |piece|
|
68
|
-
source = select(piece)
|
69
|
-
|
70
|
-
source.sources.flat_map do |src|
|
71
|
-
destination = source.from(src)
|
72
|
-
|
73
|
-
destination.destinations.flat_map do |dest|
|
74
|
-
engine = destination.to(dest)
|
75
|
-
transitions = engine.where(feen)
|
76
|
-
|
77
|
-
transitions.empty? ? [] : [[piece, src, dest, transitions]]
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
40
|
+
Source.new(@data.fetch(piece))
|
81
41
|
end
|
82
42
|
|
83
43
|
# Check if ruleset contains movement rules for specified piece
|
@@ -88,7 +48,7 @@ module Sashite
|
|
88
48
|
# @example
|
89
49
|
# ruleset.piece?("C:K") # => true
|
90
50
|
def piece?(piece)
|
91
|
-
data.key?(piece)
|
51
|
+
@data.key?(piece)
|
92
52
|
end
|
93
53
|
|
94
54
|
# Return all piece identifiers in ruleset
|
@@ -98,151 +58,7 @@ module Sashite
|
|
98
58
|
# @example
|
99
59
|
# ruleset.pieces # => ["C:K", "C:Q", "C:P", ...]
|
100
60
|
def pieces
|
101
|
-
data.keys
|
102
|
-
end
|
103
|
-
|
104
|
-
# Convert ruleset to hash representation
|
105
|
-
#
|
106
|
-
# @return [Hash] GGN data structure
|
107
|
-
#
|
108
|
-
# @example
|
109
|
-
# ruleset.to_h # => { "C:K" => { "e1" => { "e2" => [...] } } }
|
110
|
-
def to_h
|
111
|
-
data
|
112
|
-
end
|
113
|
-
|
114
|
-
private
|
115
|
-
|
116
|
-
# Validate GGN data structure
|
117
|
-
#
|
118
|
-
# @param data [Hash] Data to validate
|
119
|
-
# @raise [ArgumentError] If structure is invalid
|
120
|
-
# @return [void]
|
121
|
-
def validate_structure!(data)
|
122
|
-
raise ::ArgumentError, "GGN data must be a Hash" unless data.is_a?(::Hash)
|
123
|
-
|
124
|
-
data.each do |piece, sources|
|
125
|
-
validate_piece!(piece)
|
126
|
-
validate_sources!(sources, piece)
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
# Validate QPI piece identifier using sashite-qpi
|
131
|
-
#
|
132
|
-
# @param piece [String] Piece identifier to validate
|
133
|
-
# @raise [ArgumentError] If piece identifier is invalid
|
134
|
-
# @return [void]
|
135
|
-
def validate_piece!(piece)
|
136
|
-
raise ::ArgumentError, "Invalid piece identifier: #{piece}" unless piece.is_a?(::String)
|
137
|
-
raise ::ArgumentError, "Invalid QPI format: #{piece}" unless Qpi.valid?(piece)
|
138
|
-
end
|
139
|
-
|
140
|
-
# Validate sources hash structure
|
141
|
-
#
|
142
|
-
# @param sources [Hash] Sources hash to validate
|
143
|
-
# @param piece [String] Piece identifier (for error messages)
|
144
|
-
# @raise [ArgumentError] If sources structure is invalid
|
145
|
-
# @return [void]
|
146
|
-
def validate_sources!(sources, piece)
|
147
|
-
raise ::ArgumentError, "Sources for #{piece} must be a Hash" unless sources.is_a?(Hash)
|
148
|
-
|
149
|
-
sources.each do |source, destinations|
|
150
|
-
validate_location!(source, piece)
|
151
|
-
validate_destinations!(destinations, piece, source)
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
# Validate destinations hash structure
|
156
|
-
#
|
157
|
-
# @param destinations [Hash] Destinations hash to validate
|
158
|
-
# @param piece [String] Piece identifier (for error messages)
|
159
|
-
# @param source [String] Source location (for error messages)
|
160
|
-
# @raise [ArgumentError] If destinations structure is invalid
|
161
|
-
# @return [void]
|
162
|
-
def validate_destinations!(destinations, piece, source)
|
163
|
-
raise ::ArgumentError, "Destinations for #{piece} from #{source} must be a Hash" unless destinations.is_a?(::Hash)
|
164
|
-
|
165
|
-
destinations.each do |destination, possibilities|
|
166
|
-
validate_location!(destination, piece)
|
167
|
-
validate_possibilities!(possibilities, piece, source, destination)
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
# Validate possibilities array structure
|
172
|
-
#
|
173
|
-
# @param possibilities [Array] Possibilities array to validate
|
174
|
-
# @param piece [String] Piece identifier (for error messages)
|
175
|
-
# @param source [String] Source location (for error messages)
|
176
|
-
# @param destination [String] Destination location (for error messages)
|
177
|
-
# @raise [ArgumentError] If possibilities structure is invalid
|
178
|
-
# @return [void]
|
179
|
-
def validate_possibilities!(possibilities, piece, source, destination)
|
180
|
-
raise ::ArgumentError, "Possibilities for #{piece} #{source}→#{destination} must be an Array" unless possibilities.is_a?(::Array)
|
181
|
-
|
182
|
-
possibilities.each do |possibility|
|
183
|
-
validate_possibility!(possibility, piece, source, destination)
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
# Validate individual possibility structure using LCN and STN gems
|
188
|
-
#
|
189
|
-
# @param possibility [Hash] Possibility to validate
|
190
|
-
# @param piece [String] Piece identifier (for error messages)
|
191
|
-
# @param source [String] Source location (for error messages)
|
192
|
-
# @param destination [String] Destination location (for error messages)
|
193
|
-
# @raise [ArgumentError] If possibility structure is invalid
|
194
|
-
# @return [void]
|
195
|
-
def validate_possibility!(possibility, piece, source, destination)
|
196
|
-
raise ::ArgumentError, "Possibility for #{piece} #{source}→#{destination} must be a Hash" unless possibility.is_a?(::Hash)
|
197
|
-
raise ::ArgumentError, "Possibility must have 'must' field" unless possibility.key?("must")
|
198
|
-
raise ::ArgumentError, "Possibility must have 'deny' field" unless possibility.key?("deny")
|
199
|
-
raise ::ArgumentError, "Possibility must have 'diff' field" unless possibility.key?("diff")
|
200
|
-
|
201
|
-
validate_lcn_conditions!(possibility["must"], "must", piece, source, destination)
|
202
|
-
validate_lcn_conditions!(possibility["deny"], "deny", piece, source, destination)
|
203
|
-
validate_stn_transition!(possibility["diff"], piece, source, destination)
|
204
|
-
end
|
205
|
-
|
206
|
-
# Validate LCN conditions using sashite-lcn
|
207
|
-
#
|
208
|
-
# @param conditions [Hash] Conditions to validate
|
209
|
-
# @param field_name [String] Field name for error messages
|
210
|
-
# @param piece [String] Piece identifier (for error messages)
|
211
|
-
# @param source [String] Source location (for error messages)
|
212
|
-
# @param destination [String] Destination location (for error messages)
|
213
|
-
# @raise [ArgumentError] If conditions are invalid
|
214
|
-
# @return [void]
|
215
|
-
def validate_lcn_conditions!(conditions, field_name, piece, source, destination)
|
216
|
-
Lcn.parse(conditions)
|
217
|
-
rescue ArgumentError => e
|
218
|
-
raise ::ArgumentError, "Invalid LCN format in '#{field_name}' for #{piece} #{source}→#{destination}: #{e.message}"
|
219
|
-
end
|
220
|
-
|
221
|
-
# Validate STN transition using sashite-stn
|
222
|
-
#
|
223
|
-
# @param transition [Hash] Transition to validate
|
224
|
-
# @param piece [String] Piece identifier (for error messages)
|
225
|
-
# @param source [String] Source location (for error messages)
|
226
|
-
# @param destination [String] Destination location (for error messages)
|
227
|
-
# @raise [ArgumentError] If transition is invalid
|
228
|
-
# @return [void]
|
229
|
-
def validate_stn_transition!(transition, piece, source, destination)
|
230
|
-
Stn.parse(transition)
|
231
|
-
rescue StandardError => e
|
232
|
-
raise ::ArgumentError, "Invalid STN format in 'diff' for #{piece} #{source}→#{destination}: #{e.message}"
|
233
|
-
end
|
234
|
-
|
235
|
-
# Validate location format using CELL and HAND gems
|
236
|
-
#
|
237
|
-
# @param location [String] Location to validate
|
238
|
-
# @param piece [String] Piece identifier (for error messages)
|
239
|
-
# @raise [ArgumentError] If location format is invalid
|
240
|
-
# @return [void]
|
241
|
-
def validate_location!(location, piece)
|
242
|
-
raise ::ArgumentError, "Location for #{piece} must be a String" unless location.is_a?(::String)
|
243
|
-
|
244
|
-
valid = Cell.valid?(location) || Hand.reserve?(location)
|
245
|
-
raise ::ArgumentError, "Invalid location format: #{location}" unless valid
|
61
|
+
@data.keys
|
246
62
|
end
|
247
63
|
end
|
248
64
|
end
|
data/lib/sashite/ggn.rb
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "sashite/cell"
|
4
|
+
require "sashite/hand"
|
5
|
+
require "sashite/lcn"
|
6
|
+
require "sashite/qpi"
|
7
|
+
require "sashite/stn"
|
8
|
+
|
3
9
|
require_relative "ggn/ruleset"
|
4
10
|
|
5
11
|
module Sashite
|
@@ -14,7 +20,7 @@ module Sashite
|
|
14
20
|
#
|
15
21
|
# @param data [Hash] GGN data structure conforming to specification
|
16
22
|
# @return [Ruleset] Immutable ruleset object
|
17
|
-
# @raise [ArgumentError
|
23
|
+
# @raise [ArgumentError] If data structure is invalid
|
18
24
|
#
|
19
25
|
# @example Parse GGN data
|
20
26
|
# ruleset = Sashite::Ggn.parse({
|
@@ -34,6 +40,7 @@ module Sashite
|
|
34
40
|
# }
|
35
41
|
# })
|
36
42
|
def self.parse(data)
|
43
|
+
validate!(data)
|
37
44
|
Ruleset.new(data)
|
38
45
|
end
|
39
46
|
|
@@ -42,17 +49,169 @@ module Sashite
|
|
42
49
|
# @param data [Hash] Data structure to validate
|
43
50
|
# @return [Boolean] True if valid, false otherwise
|
44
51
|
#
|
45
|
-
# @note Rescues both ArgumentError (invalid structure) and TypeError (wrong type)
|
46
|
-
#
|
47
52
|
# @example Validate GGN data
|
48
53
|
# Sashite::Ggn.valid?(ggn_data) # => true
|
49
|
-
# Sashite::Ggn.valid?("invalid") # => false
|
50
|
-
# Sashite::Ggn.valid?(nil) # => false
|
54
|
+
# Sashite::Ggn.valid?("invalid") # => false
|
55
|
+
# Sashite::Ggn.valid?(nil) # => false
|
51
56
|
def self.valid?(data)
|
52
|
-
|
57
|
+
validate!(data)
|
53
58
|
true
|
54
|
-
rescue ::ArgumentError
|
59
|
+
rescue ::ArgumentError
|
55
60
|
false
|
56
61
|
end
|
62
|
+
|
63
|
+
# Validate GGN data structure
|
64
|
+
#
|
65
|
+
# @param data [Object] Data to validate
|
66
|
+
# @raise [ArgumentError] If structure is invalid
|
67
|
+
# @return [void]
|
68
|
+
# @api private
|
69
|
+
def self.validate!(data)
|
70
|
+
raise ::ArgumentError, "GGN data must be a Hash" unless data.is_a?(::Hash)
|
71
|
+
|
72
|
+
data.each do |piece, sources|
|
73
|
+
validate_piece!(piece)
|
74
|
+
validate_sources!(sources, piece)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
private_class_method :validate!
|
78
|
+
|
79
|
+
# Validate QPI piece identifier
|
80
|
+
#
|
81
|
+
# @param piece [String] Piece identifier to validate
|
82
|
+
# @raise [ArgumentError] If piece identifier is invalid
|
83
|
+
# @return [void]
|
84
|
+
# @api private
|
85
|
+
def self.validate_piece!(piece)
|
86
|
+
raise ::ArgumentError, "Invalid piece identifier: #{piece}" unless piece.is_a?(::String)
|
87
|
+
raise ::ArgumentError, "Invalid QPI format: #{piece}" unless Qpi.valid?(piece)
|
88
|
+
end
|
89
|
+
private_class_method :validate_piece!
|
90
|
+
|
91
|
+
# Validate sources hash structure
|
92
|
+
#
|
93
|
+
# @param sources [Hash] Sources hash to validate
|
94
|
+
# @param piece [String] Piece identifier (for error messages)
|
95
|
+
# @raise [ArgumentError] If sources structure is invalid
|
96
|
+
# @return [void]
|
97
|
+
# @api private
|
98
|
+
def self.validate_sources!(sources, piece)
|
99
|
+
raise ::ArgumentError, "Sources for #{piece} must be a Hash" unless sources.is_a?(::Hash)
|
100
|
+
|
101
|
+
sources.each do |source, destinations|
|
102
|
+
validate_location!(source, piece)
|
103
|
+
validate_destinations!(destinations, piece, source)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
private_class_method :validate_sources!
|
107
|
+
|
108
|
+
# Validate destinations hash structure
|
109
|
+
#
|
110
|
+
# @param destinations [Hash] Destinations hash to validate
|
111
|
+
# @param piece [String] Piece identifier (for error messages)
|
112
|
+
# @param source [String] Source location (for error messages)
|
113
|
+
# @raise [ArgumentError] If destinations structure is invalid
|
114
|
+
# @return [void]
|
115
|
+
# @api private
|
116
|
+
def self.validate_destinations!(destinations, piece, source)
|
117
|
+
raise ::ArgumentError, "Destinations for #{piece} from #{source} must be a Hash" unless destinations.is_a?(::Hash)
|
118
|
+
|
119
|
+
destinations.each do |destination, possibilities|
|
120
|
+
validate_location!(destination, piece)
|
121
|
+
validate_possibilities!(possibilities, piece, source, destination)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
private_class_method :validate_destinations!
|
125
|
+
|
126
|
+
# Validate possibilities array structure
|
127
|
+
#
|
128
|
+
# @param possibilities [Array] Possibilities array to validate
|
129
|
+
# @param piece [String] Piece identifier (for error messages)
|
130
|
+
# @param source [String] Source location (for error messages)
|
131
|
+
# @param destination [String] Destination location (for error messages)
|
132
|
+
# @raise [ArgumentError] If possibilities structure is invalid
|
133
|
+
# @return [void]
|
134
|
+
# @api private
|
135
|
+
def self.validate_possibilities!(possibilities, piece, source, destination)
|
136
|
+
unless possibilities.is_a?(::Array)
|
137
|
+
raise ::ArgumentError, "Possibilities for #{piece} #{source}→#{destination} must be an Array"
|
138
|
+
end
|
139
|
+
|
140
|
+
possibilities.each do |possibility|
|
141
|
+
validate_possibility!(possibility, piece, source, destination)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
private_class_method :validate_possibilities!
|
145
|
+
|
146
|
+
# Validate individual possibility structure
|
147
|
+
#
|
148
|
+
# @param possibility [Hash] Possibility to validate
|
149
|
+
# @param piece [String] Piece identifier (for error messages)
|
150
|
+
# @param source [String] Source location (for error messages)
|
151
|
+
# @param destination [String] Destination location (for error messages)
|
152
|
+
# @raise [ArgumentError] If possibility structure is invalid
|
153
|
+
# @return [void]
|
154
|
+
# @api private
|
155
|
+
def self.validate_possibility!(possibility, piece, source, destination)
|
156
|
+
unless possibility.is_a?(::Hash)
|
157
|
+
raise ::ArgumentError, "Possibility for #{piece} #{source}→#{destination} must be a Hash"
|
158
|
+
end
|
159
|
+
raise ::ArgumentError, "Possibility must have 'must' field" unless possibility.key?("must")
|
160
|
+
raise ::ArgumentError, "Possibility must have 'deny' field" unless possibility.key?("deny")
|
161
|
+
raise ::ArgumentError, "Possibility must have 'diff' field" unless possibility.key?("diff")
|
162
|
+
|
163
|
+
validate_lcn_conditions!(possibility["must"], "must", piece, source, destination)
|
164
|
+
validate_lcn_conditions!(possibility["deny"], "deny", piece, source, destination)
|
165
|
+
validate_stn_transition!(possibility["diff"], piece, source, destination)
|
166
|
+
end
|
167
|
+
private_class_method :validate_possibility!
|
168
|
+
|
169
|
+
# Validate LCN conditions
|
170
|
+
#
|
171
|
+
# @param conditions [Hash] Conditions to validate
|
172
|
+
# @param field_name [String] Field name for error messages
|
173
|
+
# @param piece [String] Piece identifier (for error messages)
|
174
|
+
# @param source [String] Source location (for error messages)
|
175
|
+
# @param destination [String] Destination location (for error messages)
|
176
|
+
# @raise [ArgumentError] If conditions are invalid
|
177
|
+
# @return [void]
|
178
|
+
# @api private
|
179
|
+
def self.validate_lcn_conditions!(conditions, field_name, piece, source, destination)
|
180
|
+
Lcn.parse(conditions)
|
181
|
+
rescue ::ArgumentError => e
|
182
|
+
raise ::ArgumentError, "Invalid LCN format in '#{field_name}' for #{piece} #{source}→#{destination}: #{e.message}"
|
183
|
+
end
|
184
|
+
private_class_method :validate_lcn_conditions!
|
185
|
+
|
186
|
+
# Validate STN transition
|
187
|
+
#
|
188
|
+
# @param transition [Hash] Transition to validate
|
189
|
+
# @param piece [String] Piece identifier (for error messages)
|
190
|
+
# @param source [String] Source location (for error messages)
|
191
|
+
# @param destination [String] Destination location (for error messages)
|
192
|
+
# @raise [ArgumentError] If transition is invalid
|
193
|
+
# @return [void]
|
194
|
+
# @api private
|
195
|
+
def self.validate_stn_transition!(transition, piece, source, destination)
|
196
|
+
Stn.parse(transition)
|
197
|
+
rescue ::StandardError => e
|
198
|
+
raise ::ArgumentError, "Invalid STN format in 'diff' for #{piece} #{source}→#{destination}: #{e.message}"
|
199
|
+
end
|
200
|
+
private_class_method :validate_stn_transition!
|
201
|
+
|
202
|
+
# Validate location format
|
203
|
+
#
|
204
|
+
# @param location [String] Location to validate
|
205
|
+
# @param piece [String] Piece identifier (for error messages)
|
206
|
+
# @raise [ArgumentError] If location format is invalid
|
207
|
+
# @return [void]
|
208
|
+
# @api private
|
209
|
+
def self.validate_location!(location, piece)
|
210
|
+
raise ::ArgumentError, "Location for #{piece} must be a String" unless location.is_a?(::String)
|
211
|
+
|
212
|
+
valid = Cell.valid?(location) || Hand.reserve?(location)
|
213
|
+
raise ::ArgumentError, "Invalid location format: #{location}" unless valid
|
214
|
+
end
|
215
|
+
private_class_method :validate_location!
|
57
216
|
end
|
58
217
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sashite-ggn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -23,34 +23,6 @@ dependencies:
|
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: '2.0'
|
26
|
-
- !ruby/object:Gem::Dependency
|
27
|
-
name: sashite-epin
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
29
|
-
requirements:
|
30
|
-
- - "~>"
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '1.1'
|
33
|
-
type: :runtime
|
34
|
-
prerelease: false
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
36
|
-
requirements:
|
37
|
-
- - "~>"
|
38
|
-
- !ruby/object:Gem::Version
|
39
|
-
version: '1.1'
|
40
|
-
- !ruby/object:Gem::Dependency
|
41
|
-
name: sashite-feen
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
43
|
-
requirements:
|
44
|
-
- - "~>"
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: '0.3'
|
47
|
-
type: :runtime
|
48
|
-
prerelease: false
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
50
|
-
requirements:
|
51
|
-
- - "~>"
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
version: '0.3'
|
54
26
|
- !ruby/object:Gem::Dependency
|
55
27
|
name: sashite-hand
|
56
28
|
requirement: !ruby/object:Gem::Requirement
|