sashite-lcn 0.1.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 +7 -0
- data/LICENSE.md +22 -0
- data/README.md +435 -0
- data/lib/sashite/lcn/conditions.rb +329 -0
- data/lib/sashite/lcn.rb +216 -0
- data/lib/sashite-lcn.rb +14 -0
- metadata +86 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 94ba5ec45b43c16436dbe3b980dada0475f14592b12163871ce40a9bd22db38e
|
|
4
|
+
data.tar.gz: b5a32943c9d2c14127b0ff24bd14d3a4cd1aba729e9e6bedc9d1db937c10e458
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5c065503d9b548066bf2915e3b28cf0ba816e37ba407463900c2cc4665aef086243721872fe40dcab3d67cb07190c5884b73b5e5daa7e0ede133e7b560957984
|
|
7
|
+
data.tar.gz: d8d4b68044bf2e118c09acc5d89fa967ffecd8efd74f307fcc6b8f528a5dce9e30102c16c1563409f34ce06f7d5f5a67bc396f6708e3cc152a15517e1e1c5f27
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2025 Sashite
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
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:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# Lcn.rb
|
|
2
|
+
|
|
3
|
+
[](https://github.com/sashite/lcn.rb/tags)
|
|
4
|
+
[](https://rubydoc.info/github/sashite/lcn.rb/main)
|
|
5
|
+

|
|
6
|
+
[](https://github.com/sashite/lcn.rb/raw/main/LICENSE.md)
|
|
7
|
+
|
|
8
|
+
> **LCN** (Location Condition Notation) implementation for the Ruby language.
|
|
9
|
+
|
|
10
|
+
## What is LCN?
|
|
11
|
+
|
|
12
|
+
LCN (Location Condition Notation) is a rule-agnostic format for describing **location conditions** in abstract strategy board games. LCN provides a standardized way to express constraints on board locations, defining what states specific locations should have.
|
|
13
|
+
|
|
14
|
+
This gem implements the [LCN Specification v1.0.0](https://sashite.dev/specs/lcn/1.0.0/) exactly, providing environmental constraint validation with CELL coordinates and state values including reserved keywords and QPI piece identifiers.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# In your Gemfile
|
|
20
|
+
gem "sashite-lcn"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or install manually:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
gem install sashite-lcn
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Dependencies
|
|
30
|
+
|
|
31
|
+
LCN builds upon two foundational primitive specifications:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
gem "sashite-cell" # Coordinate Encoding for Layered Locations
|
|
35
|
+
gem "sashite-qpi" # Qualified Piece Identifier
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### Basic Operations
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
require "sashite/lcn"
|
|
44
|
+
|
|
45
|
+
# Parse location condition data (string keys only)
|
|
46
|
+
conditions = Sashite::Lcn.parse({
|
|
47
|
+
"e4" => "empty",
|
|
48
|
+
"f5" => "enemy"
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
# Create directly using symbol keys (constructor)
|
|
52
|
+
conditions = Sashite::Lcn::Conditions.new(
|
|
53
|
+
e4: "empty",
|
|
54
|
+
f5: "enemy"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Validate condition data
|
|
58
|
+
Sashite::Lcn.valid?({ "e4" => "empty" }) # => true
|
|
59
|
+
Sashite::Lcn.valid?({ "invalid" => "empty" }) # => false
|
|
60
|
+
Sashite::Lcn.valid?({ "e4" => "unknown" }) # => false
|
|
61
|
+
|
|
62
|
+
# Access conditions using symbols
|
|
63
|
+
conditions[:e4] # => "empty"
|
|
64
|
+
conditions[:f5] # => "enemy"
|
|
65
|
+
conditions.locations # => [:e4, :f5]
|
|
66
|
+
conditions.size # => 2
|
|
67
|
+
conditions.empty? # => false
|
|
68
|
+
|
|
69
|
+
# Convert to hash with symbol keys
|
|
70
|
+
conditions.to_h # => { e4: "empty", f5: "enemy" }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### State Value Types
|
|
74
|
+
|
|
75
|
+
LCN supports two categories of state values:
|
|
76
|
+
|
|
77
|
+
#### Reserved Keywords
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# Empty location requirement
|
|
81
|
+
empty_condition = Sashite::Lcn.parse({ "e4" => "empty" })
|
|
82
|
+
empty_condition[:e4] # => "empty"
|
|
83
|
+
|
|
84
|
+
# Enemy piece requirement (context-dependent)
|
|
85
|
+
enemy_condition = Sashite::Lcn.parse({ "f5" => "enemy" })
|
|
86
|
+
enemy_condition[:f5] # => "enemy"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### QPI Piece Identifiers
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# Specific piece requirements
|
|
93
|
+
piece_conditions = Sashite::Lcn.parse({
|
|
94
|
+
"h1" => "C:+R", # Chess rook with castling rights
|
|
95
|
+
"e1" => "C:+K", # Chess king with castling rights
|
|
96
|
+
"f5" => "c:-p" # Enemy pawn vulnerable to en passant
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
# Access using symbol keys
|
|
100
|
+
piece_conditions[:h1] # => "C:+R"
|
|
101
|
+
piece_conditions[:e1] # => "C:+K"
|
|
102
|
+
piece_conditions[:f5] # => "c:-p"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Complex Conditions
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
# Path clearance for movement
|
|
109
|
+
path_clear = Sashite::Lcn::Conditions.new(
|
|
110
|
+
b2: "empty",
|
|
111
|
+
c3: "empty",
|
|
112
|
+
d4: "empty"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Castling requirements
|
|
116
|
+
castling_conditions = Sashite::Lcn::Conditions.new(
|
|
117
|
+
f1: "empty",
|
|
118
|
+
g1: "empty",
|
|
119
|
+
h1: "C:+R"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# File restriction (Shogi pawn drop)
|
|
123
|
+
file_restriction = Sashite::Lcn::Conditions.new(
|
|
124
|
+
e1: "S:P",
|
|
125
|
+
e2: "S:P",
|
|
126
|
+
e3: "S:P",
|
|
127
|
+
e4: "S:P",
|
|
128
|
+
e5: "S:P",
|
|
129
|
+
e6: "S:P",
|
|
130
|
+
e7: "S:P",
|
|
131
|
+
e8: "S:P",
|
|
132
|
+
e9: "S:P"
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Empty Conditions
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# No constraints
|
|
140
|
+
no_conditions = Sashite::Lcn::Conditions.new
|
|
141
|
+
no_conditions.empty? # => true
|
|
142
|
+
no_conditions.locations # => []
|
|
143
|
+
no_conditions.to_h # => {}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Validation and Analysis
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
conditions = Sashite::Lcn::Conditions.new(
|
|
150
|
+
e4: "empty",
|
|
151
|
+
f5: "enemy",
|
|
152
|
+
h1: "C:+R"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Basic queries
|
|
156
|
+
conditions.size # => 3
|
|
157
|
+
conditions.location?(:e4) # => true
|
|
158
|
+
conditions.location?(:g1) # => false
|
|
159
|
+
|
|
160
|
+
# State value analysis
|
|
161
|
+
conditions.keywords # => ["empty", "enemy"]
|
|
162
|
+
conditions.qpi_identifiers # => ["C:+R"]
|
|
163
|
+
conditions.keywords? # => true
|
|
164
|
+
conditions.qpi_identifiers? # => true
|
|
165
|
+
|
|
166
|
+
# Specific state checks
|
|
167
|
+
conditions.empty_locations # => [:e4]
|
|
168
|
+
conditions.enemy_locations # => [:f5]
|
|
169
|
+
conditions.piece_locations # => [:h1]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## API Reference
|
|
173
|
+
|
|
174
|
+
### Main Module Methods
|
|
175
|
+
|
|
176
|
+
- `Sashite::Lcn.parse(data)` - Parse condition data into LCN object (raises on invalid, string keys only)
|
|
177
|
+
- `Sashite::Lcn.valid?(data)` - Check if data represents valid LCN conditions
|
|
178
|
+
|
|
179
|
+
### LCN::Conditions Class
|
|
180
|
+
|
|
181
|
+
#### Creation and Parsing
|
|
182
|
+
- `Sashite::Lcn::Conditions.new(**conditions_hash)` - Create from validated hash using keyword arguments (symbol keys)
|
|
183
|
+
- `Sashite::Lcn::Conditions.parse(data)` - Parse and validate condition data (string keys only, converts to symbols internally)
|
|
184
|
+
|
|
185
|
+
#### Data Access
|
|
186
|
+
- `#to_h` - Convert to hash with symbol keys
|
|
187
|
+
- `#[](location)` - Get state value for location (symbol key)
|
|
188
|
+
- `#locations` - Get array of all location symbols
|
|
189
|
+
- `#size` - Get number of conditions
|
|
190
|
+
- `#empty?` - Check if no conditions specified
|
|
191
|
+
|
|
192
|
+
#### Validation and Queries
|
|
193
|
+
- `#location?(location)` - Check if location has condition (symbol key)
|
|
194
|
+
- `#valid_location?(location)` - Check if location uses valid CELL coordinate
|
|
195
|
+
- `#valid_state_value?(value)` - Check if state value is valid
|
|
196
|
+
|
|
197
|
+
#### State Value Analysis
|
|
198
|
+
- `#keywords` - Get array of keyword state values used
|
|
199
|
+
- `#qpi_identifiers` - Get array of QPI identifiers used
|
|
200
|
+
- `#keywords?` - Check if any keywords are used
|
|
201
|
+
- `#qpi_identifiers?` - Check if any QPI identifiers are used
|
|
202
|
+
|
|
203
|
+
#### Categorized Access
|
|
204
|
+
- `#empty_locations` - Get locations requiring empty state (array of symbols)
|
|
205
|
+
- `#enemy_locations` - Get locations requiring enemy pieces (array of symbols)
|
|
206
|
+
- `#piece_locations` - Get locations requiring specific pieces (array of symbols)
|
|
207
|
+
|
|
208
|
+
#### Iteration
|
|
209
|
+
- `#each` - Iterate over location-state pairs (location as symbol)
|
|
210
|
+
- `#each_location` - Iterate over locations only (symbols)
|
|
211
|
+
- `#each_state_value` - Iterate over state values only
|
|
212
|
+
|
|
213
|
+
## Format Specification
|
|
214
|
+
|
|
215
|
+
### Internal Ruby Representation
|
|
216
|
+
```ruby
|
|
217
|
+
# Symbol keys for Ruby idiomatic usage
|
|
218
|
+
{
|
|
219
|
+
e4: "empty",
|
|
220
|
+
f5: "enemy",
|
|
221
|
+
h1: "C:+R"
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Specification Format (JSON/external)
|
|
226
|
+
```ruby
|
|
227
|
+
# String keys as per LCN specification (required for parse method)
|
|
228
|
+
{
|
|
229
|
+
"e4" => "empty",
|
|
230
|
+
"f5" => "enemy",
|
|
231
|
+
"h1" => "C:+R"
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### State Value Types
|
|
236
|
+
```ruby
|
|
237
|
+
# Reserved keywords
|
|
238
|
+
"empty" # Location should be unoccupied
|
|
239
|
+
"enemy" # Location should contain opposing piece (context-dependent)
|
|
240
|
+
|
|
241
|
+
# QPI identifiers (examples)
|
|
242
|
+
"C:K" # Chess king, first player
|
|
243
|
+
"c:p" # Chess pawn, second player
|
|
244
|
+
"S:+P" # Shogi promoted pawn, first player
|
|
245
|
+
"x:-c" # Xiangqi diminished cannon, second player
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Validation Rules
|
|
249
|
+
|
|
250
|
+
- **Keys**: Must be valid CELL coordinates (required as strings for parse, stored as symbols internally)
|
|
251
|
+
- **Values**: Must be reserved keywords or valid QPI identifiers
|
|
252
|
+
- **Structure**: Must be a Hash (after JSON parsing if applicable)
|
|
253
|
+
- **Encoding**: String values, symbol keys internally
|
|
254
|
+
|
|
255
|
+
## Context Dependency
|
|
256
|
+
|
|
257
|
+
### Reference Piece Requirement
|
|
258
|
+
|
|
259
|
+
The `"enemy"` keyword is **context-dependent** and requires a reference piece for evaluation:
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
conditions = Sashite::Lcn::Conditions.new(f5: "enemy")
|
|
263
|
+
|
|
264
|
+
# The consuming specification must provide:
|
|
265
|
+
# 1. Reference piece identification (e.g., the moving piece)
|
|
266
|
+
# 2. Side determination logic
|
|
267
|
+
# 3. Enemy evaluation rules
|
|
268
|
+
|
|
269
|
+
# LCN validates format but doesn't interpret "enemy" semantics
|
|
270
|
+
conditions.enemy_locations # => [:f5]
|
|
271
|
+
conditions[:f5] # => "enemy"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Integration with Consuming Specifications
|
|
275
|
+
|
|
276
|
+
LCN is designed to be used by higher-level specifications:
|
|
277
|
+
|
|
278
|
+
- **GGN**: Uses LCN for movement pre-conditions with the moving piece as reference
|
|
279
|
+
- **Pattern matching**: Could use LCN with designated reference pieces
|
|
280
|
+
- **Rule validation**: Could combine multiple LCN objects with logical operators
|
|
281
|
+
|
|
282
|
+
## Common Usage Patterns
|
|
283
|
+
|
|
284
|
+
### Movement Validation
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
# Path must be clear
|
|
288
|
+
path_conditions = Sashite::Lcn::Conditions.new(
|
|
289
|
+
b2: "empty",
|
|
290
|
+
c3: "empty",
|
|
291
|
+
d4: "empty"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Destination must contain enemy
|
|
295
|
+
capture_condition = Sashite::Lcn::Conditions.new(e5: "enemy")
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Special Move Requirements
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
# Castling conditions
|
|
302
|
+
castling = Sashite::Lcn::Conditions.new(
|
|
303
|
+
f1: "empty", # King's path clear
|
|
304
|
+
g1: "empty", # King's destination clear
|
|
305
|
+
h1: "C:+R" # Rook in position with rights
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# En passant conditions
|
|
309
|
+
en_passant = Sashite::Lcn::Conditions.new(
|
|
310
|
+
f6: "empty", # Capture destination empty
|
|
311
|
+
f5: "c:-p" # Enemy pawn vulnerable
|
|
312
|
+
)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Restriction Patterns
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
# File cannot contain same piece type (Shogi pawn drop)
|
|
319
|
+
file_check = Sashite::Lcn::Conditions.new(
|
|
320
|
+
e1: "S:P",
|
|
321
|
+
e2: "S:P",
|
|
322
|
+
e3: "S:P",
|
|
323
|
+
e4: "S:P",
|
|
324
|
+
e5: "S:P",
|
|
325
|
+
e6: "S:P",
|
|
326
|
+
e7: "S:P",
|
|
327
|
+
e8: "S:P",
|
|
328
|
+
e9: "S:P"
|
|
329
|
+
)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Error Handling
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
# Invalid CELL coordinates
|
|
336
|
+
begin
|
|
337
|
+
Sashite::Lcn.parse({ "invalid" => "empty" })
|
|
338
|
+
rescue ArgumentError => e
|
|
339
|
+
puts e.message # => "Invalid CELL coordinate: invalid"
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Invalid state values
|
|
343
|
+
begin
|
|
344
|
+
Sashite::Lcn.parse({ "e4" => "unknown" })
|
|
345
|
+
rescue ArgumentError => e
|
|
346
|
+
puts e.message # => "Invalid state value: unknown"
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Invalid data structure
|
|
350
|
+
begin
|
|
351
|
+
Sashite::Lcn.parse("not a hash")
|
|
352
|
+
rescue ArgumentError => e
|
|
353
|
+
puts e.message # => "LCN data must be a Hash"
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Direct construction with keyword arguments ensures type safety
|
|
357
|
+
conditions_data = { e4: "empty", f5: "enemy" }
|
|
358
|
+
conditions = Sashite::Lcn::Conditions.new(**conditions_data)
|
|
359
|
+
# Functional approach with clear separation of construction methods
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Input Format Specification
|
|
363
|
+
|
|
364
|
+
### Parse Method - String Keys Only
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
# ✅ Valid input for parse() - string keys required
|
|
368
|
+
string_input = { "e4" => "empty", "f5" => "enemy" }
|
|
369
|
+
conditions = Sashite::Lcn.parse(string_input)
|
|
370
|
+
|
|
371
|
+
# ❌ Symbol keys not accepted by parse()
|
|
372
|
+
symbol_input = { e4: "empty", f5: "enemy" }
|
|
373
|
+
# Sashite::Lcn.parse(symbol_input) # => ArgumentError
|
|
374
|
+
|
|
375
|
+
# Internal representation always uses symbol keys
|
|
376
|
+
conditions.to_h # => { e4: "empty", f5: "enemy" }
|
|
377
|
+
conditions[:e4] # => "empty"
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Constructor - Symbol Keys Only
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
# ✅ Constructor accepts symbol keys via keyword arguments
|
|
384
|
+
conditions = Sashite::Lcn::Conditions.new(
|
|
385
|
+
e4: "empty",
|
|
386
|
+
f5: "enemy"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# This provides clear separation:
|
|
390
|
+
# - parse() for external data (JSON/string keys)
|
|
391
|
+
# - new() for internal construction (symbol keys)
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Design Rationale
|
|
395
|
+
|
|
396
|
+
This design provides clear separation between:
|
|
397
|
+
|
|
398
|
+
1. **External data parsing** (`parse()` with string keys) - matches LCN specification format
|
|
399
|
+
2. **Internal construction** (`new()` with symbol keys) - Ruby-idiomatic with keyword arguments
|
|
400
|
+
3. **Consistent internal representation** (always symbol keys) - optimal performance and developer experience
|
|
401
|
+
|
|
402
|
+
Benefits:
|
|
403
|
+
- **Reduced API complexity** - no ambiguity about which key types are accepted where
|
|
404
|
+
- **Specification compliance** - parse() strictly follows LCN format with string keys
|
|
405
|
+
- **Performance** - no key conversion logic needed in parse()
|
|
406
|
+
- **Clarity** - clear distinction between external and internal interfaces
|
|
407
|
+
|
|
408
|
+
## Design Properties
|
|
409
|
+
|
|
410
|
+
- **Rule-agnostic**: Independent of specific game mechanics
|
|
411
|
+
- **Context-neutral**: Provides format without imposing evaluation logic
|
|
412
|
+
- **Composable**: Can be combined by consuming specifications
|
|
413
|
+
- **Type-safe**: Clear distinction between keywords and piece identifiers
|
|
414
|
+
- **Ruby-idiomatic**: Symbol keys for better performance and developer experience
|
|
415
|
+
- **Specification-compliant**: String keys for external data parsing
|
|
416
|
+
- **Foundational**: Designed as building block for higher-level specifications
|
|
417
|
+
- **Immutable**: All instances frozen, transformations return new objects
|
|
418
|
+
- **Functional**: Pure functions with no side effects
|
|
419
|
+
|
|
420
|
+
## Related Specifications
|
|
421
|
+
|
|
422
|
+
- [LCN Specification v1.0.0](https://sashite.dev/specs/lcn/1.0.0/) - Complete technical specification
|
|
423
|
+
- [LCN Examples](https://sashite.dev/specs/lcn/1.0.0/examples/) - Practical implementation examples
|
|
424
|
+
- [CELL Specification v1.0.0](https://sashite.dev/specs/cell/1.0.0/) - Coordinate system component
|
|
425
|
+
- [QPI Specification v1.0.0](https://sashite.dev/specs/qpi/1.0.0/) - Piece identification component
|
|
426
|
+
- [GGN Specification v1.0.0](https://sashite.dev/specs/ggn/1.0.0/) - Consumer of LCN for movement notation
|
|
427
|
+
- [Sashité Protocol](https://sashite.dev/protocol/) - Conceptual foundation
|
|
428
|
+
|
|
429
|
+
## License
|
|
430
|
+
|
|
431
|
+
Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
|
|
432
|
+
|
|
433
|
+
## About
|
|
434
|
+
|
|
435
|
+
Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sashite/cell"
|
|
4
|
+
require "sashite/qpi"
|
|
5
|
+
|
|
6
|
+
module Sashite
|
|
7
|
+
module Lcn
|
|
8
|
+
# Represents a set of location conditions in LCN format
|
|
9
|
+
#
|
|
10
|
+
# This class provides an immutable, functional interface for working with
|
|
11
|
+
# location conditions. All instances are frozen after creation to ensure
|
|
12
|
+
# thread safety and prevent accidental mutations.
|
|
13
|
+
#
|
|
14
|
+
# Conditions are stored internally with symbol keys for Ruby idiomatic access
|
|
15
|
+
# while supporting flexible input formats (string or symbol keys).
|
|
16
|
+
#
|
|
17
|
+
# @example Creating conditions
|
|
18
|
+
# conditions = Sashite::Lcn::Conditions.new(
|
|
19
|
+
# e4: "empty",
|
|
20
|
+
# f5: "enemy",
|
|
21
|
+
# h1: "C:+R"
|
|
22
|
+
# )
|
|
23
|
+
#
|
|
24
|
+
# @example Accessing conditions
|
|
25
|
+
# conditions[:e4] # => "empty"
|
|
26
|
+
# conditions.locations # => [:e4, :f5, :h1]
|
|
27
|
+
# conditions.size # => 3
|
|
28
|
+
#
|
|
29
|
+
# @example Analyzing conditions
|
|
30
|
+
# conditions.empty_locations # => [:e4]
|
|
31
|
+
# conditions.enemy_locations # => [:f5]
|
|
32
|
+
# conditions.piece_locations # => [:h1]
|
|
33
|
+
# conditions.keywords? # => true
|
|
34
|
+
# conditions.qpi_identifiers? # => true
|
|
35
|
+
class Conditions
|
|
36
|
+
include Enumerable
|
|
37
|
+
|
|
38
|
+
# Reserved keywords from LCN specification
|
|
39
|
+
EMPTY_KEYWORD = "empty"
|
|
40
|
+
ENEMY_KEYWORD = "enemy"
|
|
41
|
+
RESERVED_KEYWORDS = [EMPTY_KEYWORD, ENEMY_KEYWORD].freeze
|
|
42
|
+
|
|
43
|
+
# Error messages
|
|
44
|
+
ERROR_INVALID_LOCATION = "Invalid CELL coordinate: %s"
|
|
45
|
+
ERROR_INVALID_STATE = "Invalid state value: %s"
|
|
46
|
+
ERROR_NOT_HASH = "Conditions data must be a Hash"
|
|
47
|
+
|
|
48
|
+
# Create a new Conditions instance
|
|
49
|
+
#
|
|
50
|
+
# @param conditions_hash [Hash] validated conditions with symbol keys
|
|
51
|
+
# @note The hash is duplicated and frozen to ensure immutability
|
|
52
|
+
#
|
|
53
|
+
# @example Direct creation with keyword arguments
|
|
54
|
+
# conditions = Sashite::Lcn::Conditions.new(
|
|
55
|
+
# e4: "empty",
|
|
56
|
+
# f5: "enemy"
|
|
57
|
+
# )
|
|
58
|
+
def initialize(**conditions_hash)
|
|
59
|
+
# Deep duplicate to ensure complete isolation
|
|
60
|
+
@conditions = conditions_hash.dup.freeze
|
|
61
|
+
freeze
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Parse and create a Conditions instance from raw data
|
|
65
|
+
#
|
|
66
|
+
# Accepts both string and symbol keys, normalizing to symbols internally.
|
|
67
|
+
# Validates all locations and state values before creating the instance.
|
|
68
|
+
#
|
|
69
|
+
# @param data [Hash] conditions data with string or symbol keys
|
|
70
|
+
# @return [Conditions] new validated instance
|
|
71
|
+
# @raise [ArgumentError] if data is invalid
|
|
72
|
+
#
|
|
73
|
+
# @example Parse with string keys
|
|
74
|
+
# conditions = Sashite::Lcn::Conditions.parse({
|
|
75
|
+
# "e4" => "empty",
|
|
76
|
+
# "f5" => "enemy"
|
|
77
|
+
# })
|
|
78
|
+
#
|
|
79
|
+
# @example Parse with symbol keys
|
|
80
|
+
# conditions = Sashite::Lcn::Conditions.parse({
|
|
81
|
+
# e4: "empty",
|
|
82
|
+
# f5: "enemy"
|
|
83
|
+
# })
|
|
84
|
+
def self.parse(data)
|
|
85
|
+
raise ArgumentError, ERROR_NOT_HASH unless data.is_a?(Hash)
|
|
86
|
+
|
|
87
|
+
validated = {}
|
|
88
|
+
|
|
89
|
+
data.each do |location, state_value|
|
|
90
|
+
location_str = location.to_s
|
|
91
|
+
|
|
92
|
+
raise ArgumentError, format(ERROR_INVALID_LOCATION, location_str) unless Cell.valid?(location_str)
|
|
93
|
+
|
|
94
|
+
raise ArgumentError, format(ERROR_INVALID_STATE, state_value) unless valid_state_value?(state_value)
|
|
95
|
+
|
|
96
|
+
validated[location_str.to_sym] = state_value
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
new(**validated)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Convert to hash with symbol keys
|
|
103
|
+
#
|
|
104
|
+
# @return [Hash] conditions as a hash with symbol keys
|
|
105
|
+
def to_h
|
|
106
|
+
@conditions.dup
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Get state value for a location
|
|
110
|
+
#
|
|
111
|
+
# @param location [Symbol, String] the location coordinate
|
|
112
|
+
# @return [String, nil] state value or nil if location not present
|
|
113
|
+
#
|
|
114
|
+
# @example
|
|
115
|
+
# conditions[:e4] # => "empty"
|
|
116
|
+
# conditions["e4"] # => "empty"
|
|
117
|
+
# conditions[:z9] # => nil
|
|
118
|
+
def [](location)
|
|
119
|
+
@conditions[location.to_sym]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Get array of all location symbols
|
|
123
|
+
#
|
|
124
|
+
# @return [Array<Symbol>] all location coordinates as symbols
|
|
125
|
+
def locations
|
|
126
|
+
@conditions.keys
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Get number of conditions
|
|
130
|
+
#
|
|
131
|
+
# @return [Integer] number of location conditions
|
|
132
|
+
def size
|
|
133
|
+
@conditions.size
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Check if no conditions are specified
|
|
137
|
+
#
|
|
138
|
+
# @return [Boolean] true if no conditions
|
|
139
|
+
def empty?
|
|
140
|
+
@conditions.empty?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Check if location has a condition
|
|
144
|
+
#
|
|
145
|
+
# @param location [Symbol, String] the location to check
|
|
146
|
+
# @return [Boolean] true if location has a condition
|
|
147
|
+
def location?(location)
|
|
148
|
+
@conditions.key?(location.to_sym)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Check if location uses valid CELL coordinate
|
|
152
|
+
#
|
|
153
|
+
# @param location [Symbol, String] the location to validate
|
|
154
|
+
# @return [Boolean] true if valid CELL coordinate
|
|
155
|
+
def valid_location?(location)
|
|
156
|
+
Cell.valid?(location.to_s)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Check if state value is valid
|
|
160
|
+
#
|
|
161
|
+
# @param value [Object] the value to validate
|
|
162
|
+
# @return [Boolean] true if valid state value
|
|
163
|
+
def valid_state_value?(value)
|
|
164
|
+
self.class.valid_state_value?(value)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Get array of keyword state values used
|
|
168
|
+
#
|
|
169
|
+
# @return [Array<String>] unique keywords used in conditions
|
|
170
|
+
#
|
|
171
|
+
# @example
|
|
172
|
+
# conditions.keywords # => ["empty", "enemy"]
|
|
173
|
+
def keywords
|
|
174
|
+
@conditions.values.select { |v| RESERVED_KEYWORDS.include?(v) }.uniq
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Get array of QPI identifiers used
|
|
178
|
+
#
|
|
179
|
+
# @return [Array<String>] unique QPI identifiers used in conditions
|
|
180
|
+
#
|
|
181
|
+
# @example
|
|
182
|
+
# conditions.qpi_identifiers # => ["C:+R", "c:p"]
|
|
183
|
+
def qpi_identifiers
|
|
184
|
+
@conditions.values.reject { |v| RESERVED_KEYWORDS.include?(v) }.uniq
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Check if any keywords are used
|
|
188
|
+
#
|
|
189
|
+
# @return [Boolean] true if any reserved keywords are present
|
|
190
|
+
def keywords?
|
|
191
|
+
@conditions.values.any? { |v| RESERVED_KEYWORDS.include?(v) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Check if any QPI identifiers are used
|
|
195
|
+
#
|
|
196
|
+
# @return [Boolean] true if any QPI identifiers are present
|
|
197
|
+
def qpi_identifiers?
|
|
198
|
+
@conditions.values.any? { |v| !RESERVED_KEYWORDS.include?(v) }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Get locations requiring empty state
|
|
202
|
+
#
|
|
203
|
+
# @return [Array<Symbol>] locations that must be empty
|
|
204
|
+
#
|
|
205
|
+
# @example
|
|
206
|
+
# conditions.empty_locations # => [:e4, :f1, :g1]
|
|
207
|
+
def empty_locations
|
|
208
|
+
@conditions.select { |_, v| v == EMPTY_KEYWORD }.keys
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Get locations requiring enemy pieces
|
|
212
|
+
#
|
|
213
|
+
# @return [Array<Symbol>] locations that must contain enemy pieces
|
|
214
|
+
#
|
|
215
|
+
# @example
|
|
216
|
+
# conditions.enemy_locations # => [:f5]
|
|
217
|
+
def enemy_locations
|
|
218
|
+
@conditions.select { |_, v| v == ENEMY_KEYWORD }.keys
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Get locations requiring specific pieces
|
|
222
|
+
#
|
|
223
|
+
# @return [Array<Symbol>] locations with QPI piece requirements
|
|
224
|
+
#
|
|
225
|
+
# @example
|
|
226
|
+
# conditions.piece_locations # => [:h1, :e1]
|
|
227
|
+
def piece_locations
|
|
228
|
+
@conditions.reject { |_, v| RESERVED_KEYWORDS.include?(v) }.keys
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Iterate over location-state pairs
|
|
232
|
+
#
|
|
233
|
+
# @yield [location, state_value] each location-state pair
|
|
234
|
+
# @yieldparam location [Symbol] location coordinate as symbol
|
|
235
|
+
# @yieldparam state_value [String] state value
|
|
236
|
+
# @return [Enumerator] if no block given
|
|
237
|
+
#
|
|
238
|
+
# @example
|
|
239
|
+
# conditions.each do |location, state|
|
|
240
|
+
# puts "#{location}: #{state}"
|
|
241
|
+
# end
|
|
242
|
+
def each(&)
|
|
243
|
+
return enum_for(:each) unless block_given?
|
|
244
|
+
|
|
245
|
+
@conditions.each(&)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Iterate over locations only
|
|
249
|
+
#
|
|
250
|
+
# @yield [location] each location
|
|
251
|
+
# @yieldparam location [Symbol] location coordinate as symbol
|
|
252
|
+
# @return [Enumerator] if no block given
|
|
253
|
+
#
|
|
254
|
+
# @example
|
|
255
|
+
# conditions.each_location do |location|
|
|
256
|
+
# puts location
|
|
257
|
+
# end
|
|
258
|
+
def each_location(&)
|
|
259
|
+
return enum_for(:each_location) unless block_given?
|
|
260
|
+
|
|
261
|
+
@conditions.each_key(&)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Iterate over state values only
|
|
265
|
+
#
|
|
266
|
+
# @yield [state_value] each state value
|
|
267
|
+
# @yieldparam state_value [String] state value
|
|
268
|
+
# @return [Enumerator] if no block given
|
|
269
|
+
#
|
|
270
|
+
# @example
|
|
271
|
+
# conditions.each_state_value do |state|
|
|
272
|
+
# puts state
|
|
273
|
+
# end
|
|
274
|
+
def each_state_value(&)
|
|
275
|
+
return enum_for(:each_state_value) unless block_given?
|
|
276
|
+
|
|
277
|
+
@conditions.each_value(&)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Check equality with another Conditions instance
|
|
281
|
+
#
|
|
282
|
+
# @param other [Object] object to compare with
|
|
283
|
+
# @return [Boolean] true if conditions are equal
|
|
284
|
+
def ==(other)
|
|
285
|
+
return false unless other.is_a?(self.class)
|
|
286
|
+
|
|
287
|
+
@conditions == other.to_h
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Alias for == to ensure proper equality in collections
|
|
291
|
+
alias eql? ==
|
|
292
|
+
|
|
293
|
+
# Generate hash code for use in Hash and Set
|
|
294
|
+
#
|
|
295
|
+
# @return [Integer] hash code
|
|
296
|
+
def hash
|
|
297
|
+
[self.class, @conditions].hash
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# String representation for debugging
|
|
301
|
+
#
|
|
302
|
+
# @return [String] human-readable representation
|
|
303
|
+
def inspect
|
|
304
|
+
"#<#{self.class.name} #{@conditions.inspect}>"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# String representation
|
|
308
|
+
#
|
|
309
|
+
# @return [String] conditions as string
|
|
310
|
+
def to_s
|
|
311
|
+
@conditions.to_s
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Check if a state value is valid (class method for internal use)
|
|
315
|
+
#
|
|
316
|
+
# @param value [Object] the value to validate
|
|
317
|
+
# @return [Boolean] true if valid state value
|
|
318
|
+
def self.valid_state_value?(value)
|
|
319
|
+
return false unless value.is_a?(String)
|
|
320
|
+
|
|
321
|
+
# Check if it's a reserved keyword
|
|
322
|
+
return true if RESERVED_KEYWORDS.include?(value)
|
|
323
|
+
|
|
324
|
+
# Otherwise, check if it's a valid QPI identifier
|
|
325
|
+
Qpi.valid?(value)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
data/lib/sashite/lcn.rb
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lcn/conditions"
|
|
4
|
+
require "sashite/cell"
|
|
5
|
+
require "sashite/qpi"
|
|
6
|
+
|
|
7
|
+
module Sashite
|
|
8
|
+
# LCN (Location Condition Notation) implementation for Ruby
|
|
9
|
+
#
|
|
10
|
+
# Provides a rule-agnostic format for describing location conditions in abstract strategy
|
|
11
|
+
# board games. LCN provides a standardized way to express constraints on board locations,
|
|
12
|
+
# defining what states specific locations should have.
|
|
13
|
+
#
|
|
14
|
+
# ## Concept
|
|
15
|
+
#
|
|
16
|
+
# LCN is a foundational specification designed to be used by other formats that require
|
|
17
|
+
# location state definitions. It expresses environmental constraints as key-value pairs
|
|
18
|
+
# where keys are CELL coordinates and values are state values.
|
|
19
|
+
#
|
|
20
|
+
# ## State Value Types
|
|
21
|
+
#
|
|
22
|
+
# LCN supports two categories of state values:
|
|
23
|
+
#
|
|
24
|
+
# ### Reserved Keywords
|
|
25
|
+
# - `"empty"`: Location should be unoccupied
|
|
26
|
+
# - `"enemy"`: Location should contain a piece from the opposing side (context-dependent)
|
|
27
|
+
#
|
|
28
|
+
# ### QPI Piece Identifiers
|
|
29
|
+
# - Exact piece requirements using QPI format (e.g., "C:K", "c:p", "S:+P")
|
|
30
|
+
#
|
|
31
|
+
# ## Context Dependency
|
|
32
|
+
#
|
|
33
|
+
# The `"enemy"` keyword is context-dependent and requires a reference piece for interpretation.
|
|
34
|
+
# The consuming specification must provide:
|
|
35
|
+
# 1. Reference piece identification (which piece determines the perspective)
|
|
36
|
+
# 2. Side determination logic (how the reference piece's side is established)
|
|
37
|
+
# 3. Enemy evaluation rules (how opposing pieces are identified)
|
|
38
|
+
#
|
|
39
|
+
# ## Format Structure
|
|
40
|
+
#
|
|
41
|
+
# LCN uses a simple JSON object structure:
|
|
42
|
+
# ```json
|
|
43
|
+
# {
|
|
44
|
+
# "<cell-coordinate>": "<state-value>"
|
|
45
|
+
# }
|
|
46
|
+
# ```
|
|
47
|
+
#
|
|
48
|
+
# Where:
|
|
49
|
+
# - Keys: CELL coordinates (string, conforming to CELL specification)
|
|
50
|
+
# - Values: State values (reserved keywords or QPI identifiers)
|
|
51
|
+
#
|
|
52
|
+
# ## Examples
|
|
53
|
+
#
|
|
54
|
+
# ### Basic Conditions
|
|
55
|
+
#
|
|
56
|
+
# # Empty location requirement
|
|
57
|
+
# conditions = Sashite::Lcn.parse({ "e4" => "empty" })
|
|
58
|
+
# conditions[:e4] # => "empty"
|
|
59
|
+
#
|
|
60
|
+
# # Enemy piece requirement (context-dependent)
|
|
61
|
+
# conditions = Sashite::Lcn.parse({ "f5" => "enemy" })
|
|
62
|
+
# conditions[:f5] # => "enemy"
|
|
63
|
+
#
|
|
64
|
+
# ### Specific Piece Requirements
|
|
65
|
+
#
|
|
66
|
+
# conditions = Sashite::Lcn.parse({
|
|
67
|
+
# "h1" => "C:+R", # Chess rook with castling rights
|
|
68
|
+
# "e1" => "C:+K", # Chess king with castling rights
|
|
69
|
+
# "f5" => "c:-p" # Enemy pawn vulnerable to en passant
|
|
70
|
+
# })
|
|
71
|
+
#
|
|
72
|
+
# ### Complex Path Conditions
|
|
73
|
+
#
|
|
74
|
+
# # Path clearance for movement
|
|
75
|
+
# path_conditions = Sashite::Lcn.parse({
|
|
76
|
+
# "b2" => "empty",
|
|
77
|
+
# "c3" => "empty",
|
|
78
|
+
# "d4" => "empty"
|
|
79
|
+
# })
|
|
80
|
+
#
|
|
81
|
+
# # Castling requirements
|
|
82
|
+
# castling = Sashite::Lcn.parse({
|
|
83
|
+
# "f1" => "empty",
|
|
84
|
+
# "g1" => "empty",
|
|
85
|
+
# "h1" => "C:+R"
|
|
86
|
+
# })
|
|
87
|
+
#
|
|
88
|
+
# ### Empty Conditions
|
|
89
|
+
#
|
|
90
|
+
# no_conditions = Sashite::Lcn.parse({})
|
|
91
|
+
# no_conditions.empty? # => true
|
|
92
|
+
#
|
|
93
|
+
# ## Design Properties
|
|
94
|
+
#
|
|
95
|
+
# - **Rule-agnostic**: Independent of specific game mechanics
|
|
96
|
+
# - **Context-neutral**: Provides format without imposing evaluation logic
|
|
97
|
+
# - **Composable**: Can be combined by consuming specifications
|
|
98
|
+
# - **Type-safe**: Clear distinction between keywords and piece identifiers
|
|
99
|
+
# - **Minimal syntax**: Clean key-value pairs without nesting
|
|
100
|
+
# - **Functional**: Pure functions with no side effects
|
|
101
|
+
# - **Immutable**: All instances are frozen for thread safety
|
|
102
|
+
#
|
|
103
|
+
# @see https://sashite.dev/specs/lcn/1.0.0/ LCN Specification v1.0.0
|
|
104
|
+
# @see https://sashite.dev/specs/lcn/1.0.0/examples/ LCN Examples
|
|
105
|
+
# @see https://sashite.dev/specs/cell/1.0.0/ CELL Specification (coordinate system)
|
|
106
|
+
# @see https://sashite.dev/specs/qpi/1.0.0/ QPI Specification (piece identifiers)
|
|
107
|
+
module Lcn
|
|
108
|
+
# Reserved keywords for common location states
|
|
109
|
+
EMPTY_KEYWORD = "empty"
|
|
110
|
+
ENEMY_KEYWORD = "enemy"
|
|
111
|
+
RESERVED_KEYWORDS = [EMPTY_KEYWORD, ENEMY_KEYWORD].freeze
|
|
112
|
+
|
|
113
|
+
# Check if data represents valid LCN conditions
|
|
114
|
+
#
|
|
115
|
+
# Validates that the data is a Hash with:
|
|
116
|
+
# - Keys: Valid CELL coordinates
|
|
117
|
+
# - Values: Reserved keywords ("empty", "enemy") or valid QPI identifiers
|
|
118
|
+
#
|
|
119
|
+
# @param data [Object] the data to validate
|
|
120
|
+
# @return [Boolean] true if valid LCN data, false otherwise
|
|
121
|
+
#
|
|
122
|
+
# @example Validate various LCN formats
|
|
123
|
+
# Sashite::Lcn.valid?({ "e4" => "empty" }) # => true
|
|
124
|
+
# Sashite::Lcn.valid?({ "f5" => "enemy" }) # => true
|
|
125
|
+
# Sashite::Lcn.valid?({ "h1" => "C:+R" }) # => true
|
|
126
|
+
# Sashite::Lcn.valid?({ "invalid" => "empty" }) # => false (invalid CELL)
|
|
127
|
+
# Sashite::Lcn.valid?({ "e4" => "unknown" }) # => false (invalid state)
|
|
128
|
+
# Sashite::Lcn.valid?("not a hash") # => false
|
|
129
|
+
# Sashite::Lcn.valid?({}) # => true (empty conditions)
|
|
130
|
+
def self.valid?(data)
|
|
131
|
+
return false unless data.is_a?(Hash)
|
|
132
|
+
|
|
133
|
+
data.all? do |location, state_value|
|
|
134
|
+
valid_location?(location) && valid_state_value?(state_value)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Parse LCN data into a Conditions object
|
|
139
|
+
#
|
|
140
|
+
# Creates a new LCN Conditions instance by parsing and validating the input data.
|
|
141
|
+
# Accepts both string and symbol keys for compatibility, but internally
|
|
142
|
+
# normalizes to symbol keys for Ruby idiomatic usage.
|
|
143
|
+
#
|
|
144
|
+
# @param data [Hash] LCN conditions data with CELL coordinates as keys
|
|
145
|
+
# @return [Lcn::Conditions] parsed conditions object with validated data
|
|
146
|
+
# @raise [ArgumentError] if the data is invalid (not a Hash, invalid keys, or invalid values)
|
|
147
|
+
#
|
|
148
|
+
# @example Parse different LCN formats
|
|
149
|
+
# # String keys (as per specification)
|
|
150
|
+
# conditions = Sashite::Lcn.parse({ "e4" => "empty", "f5" => "enemy" })
|
|
151
|
+
# conditions[:e4] # => "empty"
|
|
152
|
+
#
|
|
153
|
+
# # Symbol keys (Ruby idiomatic)
|
|
154
|
+
# conditions = Sashite::Lcn.parse({ e4: "empty", f5: "enemy" })
|
|
155
|
+
# conditions[:e4] # => "empty"
|
|
156
|
+
#
|
|
157
|
+
# # Mixed QPI and keywords
|
|
158
|
+
# conditions = Sashite::Lcn.parse({
|
|
159
|
+
# "f1" => "empty",
|
|
160
|
+
# "g1" => "empty",
|
|
161
|
+
# "h1" => "C:+R"
|
|
162
|
+
# })
|
|
163
|
+
#
|
|
164
|
+
# @example Error handling
|
|
165
|
+
# Sashite::Lcn.parse({ "invalid" => "empty" }) # => ArgumentError: Invalid CELL coordinate: invalid
|
|
166
|
+
# Sashite::Lcn.parse({ "e4" => "unknown" }) # => ArgumentError: Invalid state value: unknown
|
|
167
|
+
# Sashite::Lcn.parse("not a hash") # => ArgumentError: LCN data must be a Hash
|
|
168
|
+
def self.parse(data)
|
|
169
|
+
raise ArgumentError, "LCN data must be a Hash" unless data.is_a?(Hash)
|
|
170
|
+
|
|
171
|
+
validated_conditions = {}
|
|
172
|
+
|
|
173
|
+
data.each do |location, state_value|
|
|
174
|
+
# Convert location to string for validation, then to symbol for storage
|
|
175
|
+
location_str = location.to_s
|
|
176
|
+
|
|
177
|
+
raise ArgumentError, "Invalid CELL coordinate: #{location_str}" unless valid_location?(location_str)
|
|
178
|
+
|
|
179
|
+
raise ArgumentError, "Invalid state value: #{state_value}" unless valid_state_value?(state_value)
|
|
180
|
+
|
|
181
|
+
# Store with symbol key for Ruby idiomatic access
|
|
182
|
+
validated_conditions[location_str.to_sym] = state_value
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
Conditions.new(**validated_conditions)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
# Check if a location is a valid CELL coordinate
|
|
191
|
+
#
|
|
192
|
+
# @param location [Object] the location to validate
|
|
193
|
+
# @return [Boolean] true if valid CELL coordinate
|
|
194
|
+
def self.valid_location?(location)
|
|
195
|
+
return false unless location.is_a?(String) || location.is_a?(Symbol)
|
|
196
|
+
|
|
197
|
+
Sashite::Cell.valid?(location.to_s)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Check if a state value is valid (keyword or QPI identifier)
|
|
201
|
+
#
|
|
202
|
+
# @param state_value [Object] the state value to validate
|
|
203
|
+
# @return [Boolean] true if valid state value
|
|
204
|
+
def self.valid_state_value?(state_value)
|
|
205
|
+
return false unless state_value.is_a?(String)
|
|
206
|
+
|
|
207
|
+
# Check if it's a reserved keyword
|
|
208
|
+
return true if RESERVED_KEYWORDS.include?(state_value)
|
|
209
|
+
|
|
210
|
+
# Otherwise, check if it's a valid QPI identifier
|
|
211
|
+
Sashite::Qpi.valid?(state_value)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private_class_method :valid_location?, :valid_state_value?
|
|
215
|
+
end
|
|
216
|
+
end
|
data/lib/sashite-lcn.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "sashite/lcn"
|
|
4
|
+
|
|
5
|
+
# Sashité namespace for board game notation libraries
|
|
6
|
+
#
|
|
7
|
+
# Sashité provides a collection of libraries for representing and manipulating
|
|
8
|
+
# board game concepts according to the Sashité Protocol specifications.
|
|
9
|
+
#
|
|
10
|
+
# @see https://sashite.dev/protocol/ Sashité Protocol
|
|
11
|
+
# @see https://sashite.dev/specs/ Sashité Specifications
|
|
12
|
+
# @author Sashité
|
|
13
|
+
module Sashite
|
|
14
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sashite-lcn
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Cyril Kato
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: sashite-cell
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: sashite-qpi
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.0'
|
|
40
|
+
description: |
|
|
41
|
+
LCN (Location Condition Notation) provides a rule-agnostic format for describing location conditions
|
|
42
|
+
in abstract strategy board games. This gem implements the LCN Specification v1.0.0 with a modern
|
|
43
|
+
Ruby interface featuring immutable condition objects and functional programming principles. LCN
|
|
44
|
+
enables standardized representation of environmental constraints on board locations using reserved
|
|
45
|
+
keywords ("empty", "enemy") and QPI piece identifiers with CELL coordinate system integration.
|
|
46
|
+
Perfect for movement validation, pre-condition checking, constraint evaluation, and rule-agnostic
|
|
47
|
+
game logic requiring precise location state requirements across multiple game types and traditions.
|
|
48
|
+
email: contact@cyril.email
|
|
49
|
+
executables: []
|
|
50
|
+
extensions: []
|
|
51
|
+
extra_rdoc_files: []
|
|
52
|
+
files:
|
|
53
|
+
- LICENSE.md
|
|
54
|
+
- README.md
|
|
55
|
+
- lib/sashite-lcn.rb
|
|
56
|
+
- lib/sashite/lcn.rb
|
|
57
|
+
- lib/sashite/lcn/conditions.rb
|
|
58
|
+
homepage: https://github.com/sashite/lcn.rb
|
|
59
|
+
licenses:
|
|
60
|
+
- MIT
|
|
61
|
+
metadata:
|
|
62
|
+
bug_tracker_uri: https://github.com/sashite/lcn.rb/issues
|
|
63
|
+
documentation_uri: https://rubydoc.info/github/sashite/lcn.rb/main
|
|
64
|
+
homepage_uri: https://github.com/sashite/lcn.rb
|
|
65
|
+
source_code_uri: https://github.com/sashite/lcn.rb
|
|
66
|
+
specification_uri: https://sashite.dev/specs/lcn/1.0.0/
|
|
67
|
+
rubygems_mfa_required: 'true'
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: 3.2.0
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 3.7.1
|
|
83
|
+
specification_version: 4
|
|
84
|
+
summary: LCN (Location Condition Notation) implementation for Ruby with environmental
|
|
85
|
+
constraint validation
|
|
86
|
+
test_files: []
|