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 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
+ [![Version](https://img.shields.io/github/v/tag/sashite/lcn.rb?label=Version&logo=github)](https://github.com/sashite/lcn.rb/tags)
4
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/lcn.rb/main)
5
+ ![Ruby](https://github.com/sashite/lcn.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
+ [![License](https://img.shields.io/github/license/sashite/lcn.rb?label=License&logo=github)](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
@@ -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
@@ -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: []