sashite-ggn 0.9.1 → 0.10.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 +238 -353
 - data/lib/sashite/ggn/ruleset/source/destination/engine.rb +184 -56
 - data/lib/sashite/ggn/ruleset/source/destination.rb +1 -0
 - data/lib/sashite/ggn/ruleset/source.rb +1 -0
 - data/lib/sashite/ggn/ruleset.rb +1 -0
 - data/lib/sashite/ggn.rb +5 -28
 - metadata +3 -18
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: e88e8f3d556981f4bc4484589aedb85101963ac262f5cc81a4c17337418b1994
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 6aedb3c95ed76a094f80c22efaa94d5fd0a3291f25219796b96eb09abbbf02ef
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 12d4995c1bbc905190ebef8fd054ef615d9edfa1578bd629e375455cacf5794225c25ceb1cf9820d1d71e827caf5152e10583bae7ded85439639bb0d21eafe64
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 63450b84e3b335bcf1236ab91da9203bfdf24db0ccd01cb8f2aef11cda5683aa8fdeb8a27e528d5277f3ac7d232bc529cc15bc9080000a35641ba348504529c9
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -5,31 +5,13 @@ 
     | 
|
| 
       5 
5 
     | 
    
         
             
            
         
     | 
| 
       6 
6 
     | 
    
         
             
            [](https://github.com/sashite/ggn.rb/raw/main/LICENSE.md)
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
     | 
    
         
            -
            > **GGN** (General Gameplay Notation) implementation for Ruby —  
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
            ---
         
     | 
| 
      
 8 
     | 
    
         
            +
            > **GGN** (General Gameplay Notation) implementation for Ruby — evaluates **movement possibilities** in abstract strategy board games.
         
     | 
| 
       11 
9 
     | 
    
         | 
| 
       12 
10 
     | 
    
         
             
            ## What is GGN?
         
     | 
| 
       13 
11 
     | 
    
         | 
| 
       14 
     | 
    
         
            -
            GGN (General Gameplay Notation) is a rule-agnostic format for describing **pseudo-legal moves** in abstract strategy board games. GGN serves as a **movement possibility oracle**: given a  
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
       16 
     | 
    
         
            -
            This gem implements the [GGN Specification v1.0.0](https://sashite.dev/specs/ggn/1.0.0/), providing complete movement possibility evaluation with environmental constraint checking.
         
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
     | 
    
         
            -
            ### Core Philosophy
         
     | 
| 
       19 
     | 
    
         
            -
             
     | 
| 
       20 
     | 
    
         
            -
            GGN answers the fundamental question:
         
     | 
| 
       21 
     | 
    
         
            -
             
     | 
| 
       22 
     | 
    
         
            -
            > **Can this piece, currently at this location, reach that location?**
         
     | 
| 
       23 
     | 
    
         
            -
             
     | 
| 
       24 
     | 
    
         
            -
            It encodes:
         
     | 
| 
       25 
     | 
    
         
            -
            - **Which piece** (via QPI format)
         
     | 
| 
       26 
     | 
    
         
            -
            - **From where** (source location using CELL or HAND)
         
     | 
| 
       27 
     | 
    
         
            -
            - **To where** (destination location using CELL or HAND)
         
     | 
| 
       28 
     | 
    
         
            -
            - **Which environmental pre-conditions** must hold (`must`)
         
     | 
| 
       29 
     | 
    
         
            -
            - **Which environmental pre-conditions** must not hold (`deny`)
         
     | 
| 
       30 
     | 
    
         
            -
            - **What changes occur** if executed (`diff` in STN format)
         
     | 
| 
      
 12 
     | 
    
         
            +
            GGN (General Gameplay Notation) is a rule-agnostic format for describing **pseudo-legal moves** in abstract strategy board games. GGN serves as a **movement possibility oracle**: given a piece at a source location and a desired destination, it determines if the movement is feasible based on environmental pre-conditions.
         
     | 
| 
       31 
13 
     | 
    
         | 
| 
       32 
     | 
    
         
            -
             
     | 
| 
      
 14 
     | 
    
         
            +
            This gem implements the [GGN Specification v1.0.0](https://sashite.dev/specs/ggn/1.0.0/).
         
     | 
| 
       33 
15 
     | 
    
         | 
| 
       34 
16 
     | 
    
         
             
            ## Installation
         
     | 
| 
       35 
17 
     | 
    
         | 
| 
         @@ -44,22 +26,6 @@ Or install manually: 
     | 
|
| 
       44 
26 
     | 
    
         
             
            gem install sashite-ggn
         
     | 
| 
       45 
27 
     | 
    
         
             
            ```
         
     | 
| 
       46 
28 
     | 
    
         | 
| 
       47 
     | 
    
         
            -
            ---
         
     | 
| 
       48 
     | 
    
         
            -
             
     | 
| 
       49 
     | 
    
         
            -
            ## Dependencies
         
     | 
| 
       50 
     | 
    
         
            -
             
     | 
| 
       51 
     | 
    
         
            -
            GGN builds upon foundational Sashité specifications:
         
     | 
| 
       52 
     | 
    
         
            -
             
     | 
| 
       53 
     | 
    
         
            -
            ```ruby
         
     | 
| 
       54 
     | 
    
         
            -
            gem "sashite-cell"  # Coordinate Encoding for Layered Locations
         
     | 
| 
       55 
     | 
    
         
            -
            gem "sashite-hand"  # Hold And Notation Designator
         
     | 
| 
       56 
     | 
    
         
            -
            gem "sashite-lcn"   # Location Condition Notation
         
     | 
| 
       57 
     | 
    
         
            -
            gem "sashite-qpi"   # Qualified Piece Identifier
         
     | 
| 
       58 
     | 
    
         
            -
            gem "sashite-stn"   # State Transition Notation
         
     | 
| 
       59 
     | 
    
         
            -
            ```
         
     | 
| 
       60 
     | 
    
         
            -
             
     | 
| 
       61 
     | 
    
         
            -
            ---
         
     | 
| 
       62 
     | 
    
         
            -
             
     | 
| 
       63 
29 
     | 
    
         
             
            ## Quick Start
         
     | 
| 
       64 
30 
     | 
    
         | 
| 
       65 
31 
     | 
    
         
             
            ```ruby
         
     | 
| 
         @@ -67,423 +33,322 @@ require "sashite/ggn" 
     | 
|
| 
       67 
33 
     | 
    
         | 
| 
       68 
34 
     | 
    
         
             
            # Define GGN data structure
         
     | 
| 
       69 
35 
     | 
    
         
             
            ggn_data = {
         
     | 
| 
       70 
     | 
    
         
            -
              "C:P" => {
         
     | 
| 
       71 
     | 
    
         
            -
                "e2" => {
         
     | 
| 
       72 
     | 
    
         
            -
                  "e4" => [
         
     | 
| 
      
 36 
     | 
    
         
            +
              "C:P" => {                              # Chess pawn
         
     | 
| 
      
 37 
     | 
    
         
            +
                "e2" => {                             # From e2
         
     | 
| 
      
 38 
     | 
    
         
            +
                  "e4" => [                           # To e4
         
     | 
| 
       73 
39 
     | 
    
         
             
                    {
         
     | 
| 
       74 
     | 
    
         
            -
                      "must" => {  
     | 
| 
       75 
     | 
    
         
            -
             
     | 
| 
       76 
     | 
    
         
            -
             
     | 
| 
       77 
     | 
    
         
            -
             
     | 
| 
       78 
     | 
    
         
            -
             
     | 
| 
       79 
     | 
    
         
            -
                      }
         
     | 
| 
      
 40 
     | 
    
         
            +
                      "must" => {                     # Required conditions
         
     | 
| 
      
 41 
     | 
    
         
            +
                        "e3" => "empty",
         
     | 
| 
      
 42 
     | 
    
         
            +
                        "e4" => "empty"
         
     | 
| 
      
 43 
     | 
    
         
            +
                      },
         
     | 
| 
      
 44 
     | 
    
         
            +
                      "deny" => {}                    # Forbidden conditions
         
     | 
| 
       80 
45 
     | 
    
         
             
                    }
         
     | 
| 
       81 
46 
     | 
    
         
             
                  ]
         
     | 
| 
       82 
47 
     | 
    
         
             
                }
         
     | 
| 
       83 
48 
     | 
    
         
             
              }
         
     | 
| 
       84 
49 
     | 
    
         
             
            }
         
     | 
| 
       85 
50 
     | 
    
         | 
| 
       86 
     | 
    
         
            -
            # Validate GGN structure
         
     | 
| 
       87 
     | 
    
         
            -
            Sashite::Ggn.valid?(ggn_data) # => true
         
     | 
| 
       88 
     | 
    
         
            -
             
     | 
| 
       89 
51 
     | 
    
         
             
            # Parse into ruleset
         
     | 
| 
       90 
52 
     | 
    
         
             
            ruleset = Sashite::Ggn.parse(ggn_data)
         
     | 
| 
       91 
53 
     | 
    
         | 
| 
       92 
     | 
    
         
            -
            # Query movement  
     | 
| 
       93 
     | 
    
         
            -
            source = ruleset.select("C:P")
         
     | 
| 
       94 
     | 
    
         
            -
            destination = source.from("e2")
         
     | 
| 
       95 
     | 
    
         
            -
            engine = destination.to("e4")
         
     | 
| 
       96 
     | 
    
         
            -
             
     | 
| 
       97 
     | 
    
         
            -
            # Evaluate against position
         
     | 
| 
      
 54 
     | 
    
         
            +
            # Query movement through method chaining
         
     | 
| 
       98 
55 
     | 
    
         
             
            active_side = :first
         
     | 
| 
       99 
     | 
    
         
            -
            squares = {
         
     | 
| 
       100 
     | 
    
         
            -
              "e2" => "C:P",
         
     | 
| 
       101 
     | 
    
         
            -
              "e3" => nil,
         
     | 
| 
       102 
     | 
    
         
            -
              "e4" => nil
         
     | 
| 
       103 
     | 
    
         
            -
            }
         
     | 
| 
       104 
     | 
    
         
            -
             
     | 
| 
       105 
     | 
    
         
            -
            transitions = engine.where(active_side, squares)
         
     | 
| 
       106 
     | 
    
         
            -
            transitions.any? # => true
         
     | 
| 
       107 
     | 
    
         
            -
            ```
         
     | 
| 
       108 
     | 
    
         
            -
             
     | 
| 
       109 
     | 
    
         
            -
            ---
         
     | 
| 
       110 
     | 
    
         
            -
             
     | 
| 
       111 
     | 
    
         
            -
            ## API Reference
         
     | 
| 
       112 
     | 
    
         
            -
             
     | 
| 
       113 
     | 
    
         
            -
            ### Module Functions
         
     | 
| 
       114 
     | 
    
         
            -
             
     | 
| 
       115 
     | 
    
         
            -
            #### `Sashite::Ggn.parse(data) → Ruleset`
         
     | 
| 
      
 56 
     | 
    
         
            +
            squares = { "e2" => "C:P", "e3" => nil, "e4" => nil }
         
     | 
| 
       116 
57 
     | 
    
         | 
| 
       117 
     | 
    
         
            -
             
     | 
| 
      
 58 
     | 
    
         
            +
            possibilities = ruleset
         
     | 
| 
      
 59 
     | 
    
         
            +
              .select("C:P")        # Select piece type
         
     | 
| 
      
 60 
     | 
    
         
            +
              .from("e2")           # From source location
         
     | 
| 
      
 61 
     | 
    
         
            +
              .to("e4")             # To destination location
         
     | 
| 
      
 62 
     | 
    
         
            +
              .where(active_side, squares)  # Evaluate conditions
         
     | 
| 
       118 
63 
     | 
    
         | 
| 
       119 
     | 
    
         
            -
             
     | 
| 
       120 
     | 
    
         
            -
            ruleset = Sashite::Ggn.parse(ggn_data)
         
     | 
| 
      
 64 
     | 
    
         
            +
            possibilities.any?      # => true (movement is possible)
         
     | 
| 
       121 
65 
     | 
    
         
             
            ```
         
     | 
| 
       122 
66 
     | 
    
         | 
| 
       123 
     | 
    
         
            -
             
     | 
| 
       124 
     | 
    
         
            -
            - `data` (Hash): GGN data structure conforming to specification
         
     | 
| 
       125 
     | 
    
         
            -
             
     | 
| 
       126 
     | 
    
         
            -
            **Returns:** `Ruleset` — Immutable ruleset object
         
     | 
| 
       127 
     | 
    
         
            -
             
     | 
| 
       128 
     | 
    
         
            -
            **Raises:** `ArgumentError` — If data structure is invalid
         
     | 
| 
       129 
     | 
    
         
            -
             
     | 
| 
       130 
     | 
    
         
            -
            ---
         
     | 
| 
      
 67 
     | 
    
         
            +
            ## Core Concepts
         
     | 
| 
       131 
68 
     | 
    
         | 
| 
       132 
     | 
    
         
            -
             
     | 
| 
      
 69 
     | 
    
         
            +
            ### Navigation Structure
         
     | 
| 
       133 
70 
     | 
    
         | 
| 
       134 
     | 
    
         
            -
             
     | 
| 
      
 71 
     | 
    
         
            +
            GGN uses a hierarchical structure that naturally maps to method chaining:
         
     | 
| 
       135 
72 
     | 
    
         | 
| 
       136 
     | 
    
         
            -
            ```ruby
         
     | 
| 
       137 
     | 
    
         
            -
            Sashite::Ggn.valid?(ggn_data) # => true
         
     | 
| 
       138 
73 
     | 
    
         
             
            ```
         
     | 
| 
       139 
     | 
    
         
            -
             
     | 
| 
       140 
     | 
    
         
            -
            **Parameters:**
         
     | 
| 
       141 
     | 
    
         
            -
            - `data` (Hash): Data structure to validate
         
     | 
| 
       142 
     | 
    
         
            -
             
     | 
| 
       143 
     | 
    
         
            -
            **Returns:** `Boolean` — True if valid, false otherwise
         
     | 
| 
       144 
     | 
    
         
            -
             
     | 
| 
       145 
     | 
    
         
            -
            ---
         
     | 
| 
       146 
     | 
    
         
            -
             
     | 
| 
       147 
     | 
    
         
            -
            ### `Sashite::Ggn::Ruleset` Class
         
     | 
| 
       148 
     | 
    
         
            -
             
     | 
| 
       149 
     | 
    
         
            -
            Immutable container for GGN movement rules.
         
     | 
| 
       150 
     | 
    
         
            -
             
     | 
| 
       151 
     | 
    
         
            -
            #### `#select(piece) → Source`
         
     | 
| 
       152 
     | 
    
         
            -
             
     | 
| 
       153 
     | 
    
         
            -
            Selects movement rules for a specific piece type.
         
     | 
| 
       154 
     | 
    
         
            -
             
     | 
| 
       155 
     | 
    
         
            -
            ```ruby
         
     | 
| 
       156 
     | 
    
         
            -
            source = ruleset.select("C:K")
         
     | 
| 
      
 74 
     | 
    
         
            +
            Piece → Source → Destination → Possibilities
         
     | 
| 
       157 
75 
     | 
    
         
             
            ```
         
     | 
| 
       158 
76 
     | 
    
         | 
| 
       159 
     | 
    
         
            -
             
     | 
| 
       160 
     | 
    
         
            -
            - `piece` (String): QPI piece identifier
         
     | 
| 
       161 
     | 
    
         
            -
             
     | 
| 
       162 
     | 
    
         
            -
            **Returns:** `Source` — Source selector object
         
     | 
| 
       163 
     | 
    
         
            -
             
     | 
| 
       164 
     | 
    
         
            -
            **Raises:** `KeyError` — If piece not found in ruleset
         
     | 
| 
       165 
     | 
    
         
            -
             
     | 
| 
       166 
     | 
    
         
            -
            ---
         
     | 
| 
       167 
     | 
    
         
            -
             
     | 
| 
       168 
     | 
    
         
            -
            #### `#piece?(piece) → Boolean`
         
     | 
| 
       169 
     | 
    
         
            -
             
     | 
| 
       170 
     | 
    
         
            -
            Checks if ruleset contains movement rules for specified piece.
         
     | 
| 
      
 77 
     | 
    
         
            +
            Each level provides introspection methods to explore available options:
         
     | 
| 
       171 
78 
     | 
    
         | 
| 
       172 
79 
     | 
    
         
             
            ```ruby
         
     | 
| 
       173 
     | 
    
         
            -
             
     | 
| 
       174 
     | 
    
         
            -
             
     | 
| 
       175 
     | 
    
         
            -
             
     | 
| 
       176 
     | 
    
         
            -
            **Parameters:**
         
     | 
| 
       177 
     | 
    
         
            -
            - `piece` (String): QPI piece identifier
         
     | 
| 
       178 
     | 
    
         
            -
             
     | 
| 
       179 
     | 
    
         
            -
            **Returns:** `Boolean`
         
     | 
| 
      
 80 
     | 
    
         
            +
            # Explore available pieces
         
     | 
| 
      
 81 
     | 
    
         
            +
            ruleset.pieces                    # => ["C:K", "C:Q", "C:P", ...]
         
     | 
| 
       180 
82 
     | 
    
         | 
| 
       181 
     | 
    
         
            -
             
     | 
| 
      
 83 
     | 
    
         
            +
            # Explore sources for a piece
         
     | 
| 
      
 84 
     | 
    
         
            +
            ruleset.select("C:P").sources     # => ["a2", "b2", "c2", ...]
         
     | 
| 
       182 
85 
     | 
    
         | 
| 
       183 
     | 
    
         
            -
             
     | 
| 
      
 86 
     | 
    
         
            +
            # Explore destinations from a source
         
     | 
| 
      
 87 
     | 
    
         
            +
            ruleset.select("C:P").from("e2").destinations  # => ["e3", "e4"]
         
     | 
| 
       184 
88 
     | 
    
         | 
| 
       185 
     | 
    
         
            -
             
     | 
| 
       186 
     | 
    
         
            -
             
     | 
| 
       187 
     | 
    
         
            -
             
     | 
| 
       188 
     | 
    
         
            -
            ruleset. 
     | 
| 
      
 89 
     | 
    
         
            +
            # Check existence at any level
         
     | 
| 
      
 90 
     | 
    
         
            +
            ruleset.piece?("C:K")                          # => true
         
     | 
| 
      
 91 
     | 
    
         
            +
            ruleset.select("C:K").source?("e1")             # => true
         
     | 
| 
      
 92 
     | 
    
         
            +
            ruleset.select("C:K").from("e1").destination?("e2")  # => true
         
     | 
| 
       189 
93 
     | 
    
         
             
            ```
         
     | 
| 
       190 
94 
     | 
    
         | 
| 
       191 
     | 
    
         
            -
             
     | 
| 
      
 95 
     | 
    
         
            +
            ### Condition Evaluation
         
     | 
| 
       192 
96 
     | 
    
         | 
| 
       193 
     | 
    
         
            -
             
     | 
| 
       194 
     | 
    
         
            -
             
     | 
| 
       195 
     | 
    
         
            -
            ### `Sashite::Ggn::Ruleset::Source` Class
         
     | 
| 
       196 
     | 
    
         
            -
             
     | 
| 
       197 
     | 
    
         
            -
            Represents movement possibilities for a piece type.
         
     | 
| 
       198 
     | 
    
         
            -
             
     | 
| 
       199 
     | 
    
         
            -
            #### `#from(source) → Destination`
         
     | 
| 
       200 
     | 
    
         
            -
             
     | 
| 
       201 
     | 
    
         
            -
            Specifies the source location for the piece.
         
     | 
| 
      
 97 
     | 
    
         
            +
            The `where` method evaluates movement possibilities against the current board state:
         
     | 
| 
       202 
98 
     | 
    
         | 
| 
       203 
99 
     | 
    
         
             
            ```ruby
         
     | 
| 
       204 
     | 
    
         
            -
             
     | 
| 
       205 
     | 
    
         
            -
             
     | 
| 
       206 
     | 
    
         
            -
             
     | 
| 
       207 
     | 
    
         
            -
            **Parameters:**
         
     | 
| 
       208 
     | 
    
         
            -
            - `source` (String): Source location (CELL coordinate or HAND "*")
         
     | 
| 
       209 
     | 
    
         
            -
             
     | 
| 
       210 
     | 
    
         
            -
            **Returns:** `Destination` — Destination selector object
         
     | 
| 
      
 100 
     | 
    
         
            +
            # Returns array of matching possibilities (may be empty)
         
     | 
| 
      
 101 
     | 
    
         
            +
            possibilities = engine.where(active_side, squares)
         
     | 
| 
       211 
102 
     | 
    
         | 
| 
       212 
     | 
    
         
            -
             
     | 
| 
      
 103 
     | 
    
         
            +
            # Each possibility is a Hash containing the original GGN data
         
     | 
| 
      
 104 
     | 
    
         
            +
            # that satisfied the conditions
         
     | 
| 
      
 105 
     | 
    
         
            +
            possibility = possibilities.first
         
     | 
| 
      
 106 
     | 
    
         
            +
            # => { "must" => {...}, "deny" => {...} }
         
     | 
| 
      
 107 
     | 
    
         
            +
            ```
         
     | 
| 
       213 
108 
     | 
    
         | 
| 
       214 
     | 
    
         
            -
             
     | 
| 
      
 109 
     | 
    
         
            +
            **Key points:**
         
     | 
| 
      
 110 
     | 
    
         
            +
            - `active_side` (Symbol): `:first` or `:second` - determines enemy evaluation
         
     | 
| 
      
 111 
     | 
    
         
            +
            - `squares` (Hash): Board state where keys are CELL coordinates, values are QPI identifiers or `nil`
         
     | 
| 
      
 112 
     | 
    
         
            +
            - Returns an array of possibilities that match the conditions
         
     | 
| 
       215 
113 
     | 
    
         | 
| 
       216 
     | 
    
         
            -
             
     | 
| 
      
 114 
     | 
    
         
            +
            ## API Reference
         
     | 
| 
       217 
115 
     | 
    
         | 
| 
       218 
     | 
    
         
            -
             
     | 
| 
      
 116 
     | 
    
         
            +
            ### Module Methods
         
     | 
| 
       219 
117 
     | 
    
         | 
| 
       220 
118 
     | 
    
         
             
            ```ruby
         
     | 
| 
       221 
     | 
    
         
            -
             
     | 
| 
       222 
     | 
    
         
            -
             
     | 
| 
       223 
     | 
    
         
            -
             
     | 
| 
       224 
     | 
    
         
            -
            **Returns:** `Array<String>` — Source locations
         
     | 
| 
       225 
     | 
    
         
            -
             
     | 
| 
       226 
     | 
    
         
            -
            ---
         
     | 
| 
      
 119 
     | 
    
         
            +
            # Parse GGN data into a ruleset
         
     | 
| 
      
 120 
     | 
    
         
            +
            ruleset = Sashite::Ggn.parse(data)
         
     | 
| 
       227 
121 
     | 
    
         | 
| 
       228 
     | 
    
         
            -
             
     | 
| 
      
 122 
     | 
    
         
            +
            # Validate GGN data structure
         
     | 
| 
      
 123 
     | 
    
         
            +
            Sashite::Ggn.valid?(data)  # => true/false
         
     | 
| 
      
 124 
     | 
    
         
            +
            ```
         
     | 
| 
       229 
125 
     | 
    
         | 
| 
       230 
     | 
    
         
            -
             
     | 
| 
      
 126 
     | 
    
         
            +
            ### Ruleset Class
         
     | 
| 
       231 
127 
     | 
    
         | 
| 
       232 
128 
     | 
    
         
             
            ```ruby
         
     | 
| 
       233 
     | 
    
         
            -
             
     | 
| 
       234 
     | 
    
         
            -
             
     | 
| 
      
 129 
     | 
    
         
            +
            # Select piece movement rules
         
     | 
| 
      
 130 
     | 
    
         
            +
            source = ruleset.select("C:K")
         
     | 
| 
       235 
131 
     | 
    
         | 
| 
       236 
     | 
    
         
            -
             
     | 
| 
       237 
     | 
    
         
            -
             
     | 
| 
      
 132 
     | 
    
         
            +
            # Check if piece exists
         
     | 
| 
      
 133 
     | 
    
         
            +
            ruleset.piece?("C:K")  # => true/false
         
     | 
| 
       238 
134 
     | 
    
         | 
| 
       239 
     | 
    
         
            -
             
     | 
| 
      
 135 
     | 
    
         
            +
            # List all pieces
         
     | 
| 
      
 136 
     | 
    
         
            +
            ruleset.pieces  # => ["C:K", "C:Q", ...]
         
     | 
| 
      
 137 
     | 
    
         
            +
            ```
         
     | 
| 
       240 
138 
     | 
    
         | 
| 
       241 
     | 
    
         
            -
             
     | 
| 
      
 139 
     | 
    
         
            +
            ### Source Class
         
     | 
| 
       242 
140 
     | 
    
         | 
| 
       243 
     | 
    
         
            -
             
     | 
| 
      
 141 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 142 
     | 
    
         
            +
            # Select source location
         
     | 
| 
      
 143 
     | 
    
         
            +
            destination = source.from("e1")
         
     | 
| 
       244 
144 
     | 
    
         | 
| 
       245 
     | 
    
         
            -
             
     | 
| 
      
 145 
     | 
    
         
            +
            # Check if source exists
         
     | 
| 
      
 146 
     | 
    
         
            +
            source.source?("e1")  # => true/false
         
     | 
| 
       246 
147 
     | 
    
         | 
| 
       247 
     | 
    
         
            -
             
     | 
| 
      
 148 
     | 
    
         
            +
            # List all sources
         
     | 
| 
      
 149 
     | 
    
         
            +
            source.sources  # => ["e1", "d1", ...]
         
     | 
| 
      
 150 
     | 
    
         
            +
            ```
         
     | 
| 
       248 
151 
     | 
    
         | 
| 
       249 
     | 
    
         
            -
             
     | 
| 
      
 152 
     | 
    
         
            +
            ### Destination Class
         
     | 
| 
       250 
153 
     | 
    
         | 
| 
       251 
154 
     | 
    
         
             
            ```ruby
         
     | 
| 
      
 155 
     | 
    
         
            +
            # Select destination location
         
     | 
| 
       252 
156 
     | 
    
         
             
            engine = destination.to("e2")
         
     | 
| 
       253 
     | 
    
         
            -
            ```
         
     | 
| 
       254 
     | 
    
         
            -
             
     | 
| 
       255 
     | 
    
         
            -
            **Parameters:**
         
     | 
| 
       256 
     | 
    
         
            -
            - `destination` (String): Destination location (CELL coordinate or HAND "*")
         
     | 
| 
       257 
     | 
    
         
            -
             
     | 
| 
       258 
     | 
    
         
            -
            **Returns:** `Engine` — Movement evaluation engine
         
     | 
| 
       259 
     | 
    
         
            -
             
     | 
| 
       260 
     | 
    
         
            -
            **Raises:** `KeyError` — If destination not found from this source
         
     | 
| 
       261 
157 
     | 
    
         | 
| 
       262 
     | 
    
         
            -
             
     | 
| 
      
 158 
     | 
    
         
            +
            # Check if destination exists
         
     | 
| 
      
 159 
     | 
    
         
            +
            destination.destination?("e2")  # => true/false
         
     | 
| 
       263 
160 
     | 
    
         | 
| 
       264 
     | 
    
         
            -
             
     | 
| 
       265 
     | 
    
         
            -
             
     | 
| 
       266 
     | 
    
         
            -
            Returns all valid destinations from this source.
         
     | 
| 
       267 
     | 
    
         
            -
             
     | 
| 
       268 
     | 
    
         
            -
            ```ruby
         
     | 
| 
       269 
     | 
    
         
            -
            destination.destinations # => ["d1", "d2", "e2", "f2", "f1"]
         
     | 
| 
      
 161 
     | 
    
         
            +
            # List all destinations
         
     | 
| 
      
 162 
     | 
    
         
            +
            destination.destinations  # => ["d1", "d2", ...]
         
     | 
| 
       270 
163 
     | 
    
         
             
            ```
         
     | 
| 
       271 
164 
     | 
    
         | 
| 
       272 
     | 
    
         
            -
             
     | 
| 
       273 
     | 
    
         
            -
             
     | 
| 
       274 
     | 
    
         
            -
            ---
         
     | 
| 
       275 
     | 
    
         
            -
             
     | 
| 
       276 
     | 
    
         
            -
            #### `#destination?(location) → Boolean`
         
     | 
| 
       277 
     | 
    
         
            -
             
     | 
| 
       278 
     | 
    
         
            -
            Checks if location is a valid destination from this source.
         
     | 
| 
      
 165 
     | 
    
         
            +
            ### Engine Class
         
     | 
| 
       279 
166 
     | 
    
         | 
| 
       280 
167 
     | 
    
         
             
            ```ruby
         
     | 
| 
       281 
     | 
    
         
            -
             
     | 
| 
      
 168 
     | 
    
         
            +
            # Evaluate movement possibilities
         
     | 
| 
      
 169 
     | 
    
         
            +
            possibilities = engine.where(active_side, squares)
         
     | 
| 
      
 170 
     | 
    
         
            +
            # Returns array of possibility hashes that match conditions
         
     | 
| 
       282 
171 
     | 
    
         
             
            ```
         
     | 
| 
       283 
172 
     | 
    
         | 
| 
       284 
     | 
    
         
            -
             
     | 
| 
       285 
     | 
    
         
            -
            - `location` (String): Destination location
         
     | 
| 
       286 
     | 
    
         
            -
             
     | 
| 
       287 
     | 
    
         
            -
            **Returns:** `Boolean`
         
     | 
| 
       288 
     | 
    
         
            -
             
     | 
| 
       289 
     | 
    
         
            -
            ---
         
     | 
| 
       290 
     | 
    
         
            -
             
     | 
| 
       291 
     | 
    
         
            -
            ### `Sashite::Ggn::Ruleset::Source::Destination::Engine` Class
         
     | 
| 
       292 
     | 
    
         
            -
             
     | 
| 
       293 
     | 
    
         
            -
            Evaluates movement possibility under given position conditions.
         
     | 
| 
       294 
     | 
    
         
            -
             
     | 
| 
       295 
     | 
    
         
            -
            #### `#where(active_side, squares) → Array<Transition>`
         
     | 
| 
      
 173 
     | 
    
         
            +
            ## Examples
         
     | 
| 
       296 
174 
     | 
    
         | 
| 
       297 
     | 
    
         
            -
             
     | 
| 
      
 175 
     | 
    
         
            +
            ### Chess Pawn Movement
         
     | 
| 
       298 
176 
     | 
    
         | 
| 
       299 
177 
     | 
    
         
             
            ```ruby
         
     | 
| 
       300 
     | 
    
         
            -
             
     | 
| 
       301 
     | 
    
         
            -
             
     | 
| 
       302 
     | 
    
         
            -
              " 
     | 
| 
       303 
     | 
    
         
            -
             
     | 
| 
       304 
     | 
    
         
            -
             
     | 
| 
      
 178 
     | 
    
         
            +
            # Two-square advance from starting position
         
     | 
| 
      
 179 
     | 
    
         
            +
            ggn_data = {
         
     | 
| 
      
 180 
     | 
    
         
            +
              "C:P" => {
         
     | 
| 
      
 181 
     | 
    
         
            +
                "e2" => {
         
     | 
| 
      
 182 
     | 
    
         
            +
                  "e4" => [{
         
     | 
| 
      
 183 
     | 
    
         
            +
                    "must" => { "e3" => "empty", "e4" => "empty" },
         
     | 
| 
      
 184 
     | 
    
         
            +
                    "deny" => {}
         
     | 
| 
      
 185 
     | 
    
         
            +
                  }]
         
     | 
| 
      
 186 
     | 
    
         
            +
                }
         
     | 
| 
      
 187 
     | 
    
         
            +
              }
         
     | 
| 
       305 
188 
     | 
    
         
             
            }
         
     | 
| 
       306 
189 
     | 
    
         | 
| 
       307 
     | 
    
         
            -
             
     | 
| 
       308 
     | 
    
         
            -
            ```
         
     | 
| 
       309 
     | 
    
         
            -
             
     | 
| 
       310 
     | 
    
         
            -
            **Parameters:**
         
     | 
| 
       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
         
     | 
| 
       313 
     | 
    
         
            -
             
     | 
| 
       314 
     | 
    
         
            -
            **Returns:** `Array<Sashite::Stn::Transition>` — Valid state transitions (may be empty)
         
     | 
| 
      
 190 
     | 
    
         
            +
            ruleset = Sashite::Ggn.parse(ggn_data)
         
     | 
| 
       315 
191 
     | 
    
         | 
| 
       316 
     | 
    
         
            -
             
     | 
| 
      
 192 
     | 
    
         
            +
            # Valid: path is clear
         
     | 
| 
      
 193 
     | 
    
         
            +
            squares = { "e2" => "C:P", "e3" => nil, "e4" => nil }
         
     | 
| 
      
 194 
     | 
    
         
            +
            possibilities = ruleset.select("C:P").from("e2").to("e4").where(:first, squares)
         
     | 
| 
      
 195 
     | 
    
         
            +
            possibilities.any?  # => true
         
     | 
| 
       317 
196 
     | 
    
         | 
| 
       318 
     | 
    
         
            -
             
     | 
| 
      
 197 
     | 
    
         
            +
            # Invalid: e3 is blocked
         
     | 
| 
      
 198 
     | 
    
         
            +
            squares = { "e2" => "C:P", "e3" => "c:p", "e4" => nil }
         
     | 
| 
      
 199 
     | 
    
         
            +
            possibilities = ruleset.select("C:P").from("e2").to("e4").where(:first, squares)
         
     | 
| 
      
 200 
     | 
    
         
            +
            possibilities.any?  # => false
         
     | 
| 
      
 201 
     | 
    
         
            +
            ```
         
     | 
| 
       319 
202 
     | 
    
         | 
| 
       320 
     | 
    
         
            -
            ###  
     | 
| 
      
 203 
     | 
    
         
            +
            ### Pawn Capture
         
     | 
| 
       321 
204 
     | 
    
         | 
| 
       322 
205 
     | 
    
         
             
            ```ruby
         
     | 
| 
       323 
     | 
    
         
            -
             
     | 
| 
       324 
     | 
    
         
            -
             
     | 
| 
       325 
     | 
    
         
            -
             
     | 
| 
       326 
     | 
    
         
            -
             
     | 
| 
       327 
     | 
    
         
            -
             
     | 
| 
       328 
     | 
    
         
            -
             
     | 
| 
       329 
     | 
    
         
            -
             
     | 
| 
       330 
     | 
    
         
            -
             
     | 
| 
       331 
     | 
    
         
            -
                    }
         
     | 
| 
       332 
     | 
    
         
            -
                  ]
         
     | 
| 
      
 206 
     | 
    
         
            +
            # Diagonal capture
         
     | 
| 
      
 207 
     | 
    
         
            +
            ggn_data = {
         
     | 
| 
      
 208 
     | 
    
         
            +
              "C:P" => {
         
     | 
| 
      
 209 
     | 
    
         
            +
                "e4" => {
         
     | 
| 
      
 210 
     | 
    
         
            +
                  "d5" => [{
         
     | 
| 
      
 211 
     | 
    
         
            +
                    "must" => { "d5" => "enemy" },
         
     | 
| 
      
 212 
     | 
    
         
            +
                    "deny" => {}
         
     | 
| 
      
 213 
     | 
    
         
            +
                  }]
         
     | 
| 
       333 
214 
     | 
    
         
             
                }
         
     | 
| 
       334 
215 
     | 
    
         
             
              }
         
     | 
| 
       335 
216 
     | 
    
         
             
            }
         
     | 
| 
       336 
     | 
    
         
            -
            ```
         
     | 
| 
       337 
217 
     | 
    
         | 
| 
       338 
     | 
    
         
            -
             
     | 
| 
       339 
     | 
    
         
            -
             
     | 
| 
       340 
     | 
    
         
            -
            | Field | Type | Description |
         
     | 
| 
       341 
     | 
    
         
            -
            |-------|------|-------------|
         
     | 
| 
       342 
     | 
    
         
            -
            | **Piece** | String (QPI) | Piece identifier (e.g., `"C:K"`, `"s:+p"`) |
         
     | 
| 
       343 
     | 
    
         
            -
            | **Source** | String (CELL/HAND) | Origin location (e.g., `"e2"`, `"*"`) |
         
     | 
| 
       344 
     | 
    
         
            -
            | **Destination** | String (CELL/HAND) | Target location (e.g., `"e4"`, `"*"`) |
         
     | 
| 
       345 
     | 
    
         
            -
            | **must** | Hash (LCN) | Pre-conditions that must be satisfied |
         
     | 
| 
       346 
     | 
    
         
            -
            | **deny** | Hash (LCN) | Pre-conditions that must not be satisfied |
         
     | 
| 
       347 
     | 
    
         
            -
            | **diff** | Hash (STN) | State transition specification |
         
     | 
| 
       348 
     | 
    
         
            -
             
     | 
| 
       349 
     | 
    
         
            -
            **Note (normative)**: To preserve GGN's board-reachability scope, entries where **`source="*"` and `destination="*"`** (direct **HAND→HAND**) are **forbidden** by the specification.
         
     | 
| 
      
 218 
     | 
    
         
            +
            ruleset = Sashite::Ggn.parse(ggn_data)
         
     | 
| 
       350 
219 
     | 
    
         | 
| 
       351 
     | 
    
         
            -
             
     | 
| 
      
 220 
     | 
    
         
            +
            # Valid: enemy piece on d5
         
     | 
| 
      
 221 
     | 
    
         
            +
            squares = { "e4" => "C:P", "d5" => "c:p" }
         
     | 
| 
      
 222 
     | 
    
         
            +
            possibilities = ruleset.select("C:P").from("e4").to("d5").where(:first, squares)
         
     | 
| 
      
 223 
     | 
    
         
            +
            possibilities.any?  # => true
         
     | 
| 
       352 
224 
     | 
    
         | 
| 
       353 
     | 
    
         
            -
             
     | 
| 
      
 225 
     | 
    
         
            +
            # Invalid: friendly piece on d5
         
     | 
| 
      
 226 
     | 
    
         
            +
            squares = { "e4" => "C:P", "d5" => "C:N" }
         
     | 
| 
      
 227 
     | 
    
         
            +
            possibilities = ruleset.select("C:P").from("e4").to("d5").where(:first, squares)
         
     | 
| 
      
 228 
     | 
    
         
            +
            possibilities.any?  # => false
         
     | 
| 
      
 229 
     | 
    
         
            +
            ```
         
     | 
| 
       354 
230 
     | 
    
         | 
| 
       355 
     | 
    
         
            -
            ###  
     | 
| 
      
 231 
     | 
    
         
            +
            ### Castling
         
     | 
| 
       356 
232 
     | 
    
         | 
| 
       357 
233 
     | 
    
         
             
            ```ruby
         
     | 
| 
       358 
     | 
    
         
            -
            #  
     | 
| 
       359 
     | 
    
         
            -
             
     | 
| 
       360 
     | 
    
         
            -
             
     | 
| 
       361 
     | 
    
         
            -
             
     | 
| 
       362 
     | 
    
         
            -
             
     | 
| 
       363 
     | 
    
         
            -
             
     | 
| 
      
 234 
     | 
    
         
            +
            # King-side castling
         
     | 
| 
      
 235 
     | 
    
         
            +
            ggn_data = {
         
     | 
| 
      
 236 
     | 
    
         
            +
              "C:K" => {
         
     | 
| 
      
 237 
     | 
    
         
            +
                "e1" => {
         
     | 
| 
      
 238 
     | 
    
         
            +
                  "g1" => [{
         
     | 
| 
      
 239 
     | 
    
         
            +
                    "must" => {
         
     | 
| 
      
 240 
     | 
    
         
            +
                      "f1" => "empty",
         
     | 
| 
      
 241 
     | 
    
         
            +
                      "g1" => "empty",
         
     | 
| 
      
 242 
     | 
    
         
            +
                      "h1" => "C:+R"     # Rook with castling rights
         
     | 
| 
      
 243 
     | 
    
         
            +
                    },
         
     | 
| 
      
 244 
     | 
    
         
            +
                    "deny" => {}
         
     | 
| 
      
 245 
     | 
    
         
            +
                  }]
         
     | 
| 
      
 246 
     | 
    
         
            +
                }
         
     | 
| 
      
 247 
     | 
    
         
            +
              }
         
     | 
| 
       364 
248 
     | 
    
         
             
            }
         
     | 
| 
       365 
249 
     | 
    
         | 
| 
       366 
     | 
    
         
            -
             
     | 
| 
       367 
     | 
    
         
            -
              .select("C:P")
         
     | 
| 
       368 
     | 
    
         
            -
              .from("e2")
         
     | 
| 
       369 
     | 
    
         
            -
              .to("e4")
         
     | 
| 
       370 
     | 
    
         
            -
              .where(active_side, squares)
         
     | 
| 
      
 250 
     | 
    
         
            +
            ruleset = Sashite::Ggn.parse(ggn_data)
         
     | 
| 
       371 
251 
     | 
    
         | 
| 
       372 
     | 
    
         
            -
             
     | 
| 
       373 
     | 
    
         
            -
             
     | 
| 
      
 252 
     | 
    
         
            +
            # Valid: all conditions met
         
     | 
| 
      
 253 
     | 
    
         
            +
            squares = {
         
     | 
| 
      
 254 
     | 
    
         
            +
              "e1" => "C:+K",
         
     | 
| 
      
 255 
     | 
    
         
            +
              "f1" => nil,
         
     | 
| 
      
 256 
     | 
    
         
            +
              "g1" => nil,
         
     | 
| 
      
 257 
     | 
    
         
            +
              "h1" => "C:+R"
         
     | 
| 
      
 258 
     | 
    
         
            +
            }
         
     | 
| 
      
 259 
     | 
    
         
            +
            possibilities = ruleset.select("C:K").from("e1").to("g1").where(:first, squares)
         
     | 
| 
      
 260 
     | 
    
         
            +
            possibilities.any?  # => true
         
     | 
| 
       374 
261 
     | 
    
         
             
            ```
         
     | 
| 
       375 
262 
     | 
    
         | 
| 
       376 
     | 
    
         
            -
            ###  
     | 
| 
      
 263 
     | 
    
         
            +
            ### Shogi Drop
         
     | 
| 
       377 
264 
     | 
    
         | 
| 
       378 
265 
     | 
    
         
             
            ```ruby
         
     | 
| 
       379 
     | 
    
         
            -
            #  
     | 
| 
       380 
     | 
    
         
            -
             
     | 
| 
       381 
     | 
    
         
            -
             
     | 
| 
       382 
     | 
    
         
            -
             
     | 
| 
       383 
     | 
    
         
            -
             
     | 
| 
       384 
     | 
    
         
            -
             
     | 
| 
       385 
     | 
    
         
            -
            #  
     | 
| 
       386 
     | 
    
         
            -
             
     | 
| 
       387 
     | 
    
         
            -
             
     | 
| 
       388 
     | 
    
         
            -
             
     | 
| 
       389 
     | 
    
         
            -
             
     | 
| 
       390 
     | 
    
         
            -
             
     | 
| 
       391 
     | 
    
         
            -
             
     | 
| 
       392 
     | 
    
         
            -
             
     | 
| 
       393 
     | 
    
         
            -
             
     | 
| 
       394 
     | 
    
         
            -
                squares[cell] = piece&.to_s
         
     | 
| 
       395 
     | 
    
         
            -
              end
         
     | 
| 
       396 
     | 
    
         
            -
            end
         
     | 
| 
       397 
     | 
    
         
            -
             
     | 
| 
       398 
     | 
    
         
            -
            # Use with GGN
         
     | 
| 
       399 
     | 
    
         
            -
            transitions = engine.where(active_side, squares)
         
     | 
| 
       400 
     | 
    
         
            -
            ```
         
     | 
| 
      
 266 
     | 
    
         
            +
            # Pawn drop with file restriction
         
     | 
| 
      
 267 
     | 
    
         
            +
            ggn_data = {
         
     | 
| 
      
 268 
     | 
    
         
            +
              "S:P" => {
         
     | 
| 
      
 269 
     | 
    
         
            +
                "*" => {              # From hand
         
     | 
| 
      
 270 
     | 
    
         
            +
                  "e4" => [{
         
     | 
| 
      
 271 
     | 
    
         
            +
                    "must" => { "e4" => "empty" },
         
     | 
| 
      
 272 
     | 
    
         
            +
                    "deny" => {       # No friendly pawn on same file
         
     | 
| 
      
 273 
     | 
    
         
            +
                      "e1" => "S:P", "e2" => "S:P", "e3" => "S:P",
         
     | 
| 
      
 274 
     | 
    
         
            +
                      "e5" => "S:P", "e6" => "S:P", "e7" => "S:P",
         
     | 
| 
      
 275 
     | 
    
         
            +
                      "e8" => "S:P", "e9" => "S:P"
         
     | 
| 
      
 276 
     | 
    
         
            +
                    }
         
     | 
| 
      
 277 
     | 
    
         
            +
                  }]
         
     | 
| 
      
 278 
     | 
    
         
            +
                }
         
     | 
| 
      
 279 
     | 
    
         
            +
              }
         
     | 
| 
      
 280 
     | 
    
         
            +
            }
         
     | 
| 
       401 
281 
     | 
    
         | 
| 
       402 
     | 
    
         
            -
             
     | 
| 
      
 282 
     | 
    
         
            +
            ruleset = Sashite::Ggn.parse(ggn_data)
         
     | 
| 
       403 
283 
     | 
    
         | 
| 
       404 
     | 
    
         
            -
             
     | 
| 
       405 
     | 
    
         
            -
            # Check capture possibility
         
     | 
| 
       406 
     | 
    
         
            -
            active_side = :first
         
     | 
| 
      
 284 
     | 
    
         
            +
            # Valid: no pawn on e-file
         
     | 
| 
       407 
285 
     | 
    
         
             
            squares = {
         
     | 
| 
       408 
     | 
    
         
            -
              " 
     | 
| 
       409 
     | 
    
         
            -
              " 
     | 
| 
       410 
     | 
    
         
            -
              "f5" => "c:p"   # Black pawn (enemy)
         
     | 
| 
      
 286 
     | 
    
         
            +
              "e1" => nil, "e2" => nil, "e3" => nil, "e4" => nil,
         
     | 
| 
      
 287 
     | 
    
         
            +
              "e5" => nil, "e6" => nil, "e7" => nil, "e8" => nil, "e9" => nil
         
     | 
| 
       411 
288 
     | 
    
         
             
            }
         
     | 
| 
      
 289 
     | 
    
         
            +
            possibilities = ruleset.select("S:P").from("*").to("e4").where(:first, squares)
         
     | 
| 
      
 290 
     | 
    
         
            +
            possibilities.any?  # => true
         
     | 
| 
       412 
291 
     | 
    
         | 
| 
       413 
     | 
    
         
            -
            #  
     | 
| 
       414 
     | 
    
         
            -
             
     | 
| 
       415 
     | 
    
         
            -
             
     | 
| 
       416 
     | 
    
         
            -
             
     | 
| 
       417 
     | 
    
         
            -
            transitions.any? # => true if capture is allowed
         
     | 
| 
      
 292 
     | 
    
         
            +
            # Invalid: pawn already on e5
         
     | 
| 
      
 293 
     | 
    
         
            +
            squares["e5"] = "S:P"
         
     | 
| 
      
 294 
     | 
    
         
            +
            possibilities = ruleset.select("S:P").from("*").to("e4").where(:first, squares)
         
     | 
| 
      
 295 
     | 
    
         
            +
            possibilities.any?  # => false
         
     | 
| 
       418 
296 
     | 
    
         
             
            ```
         
     | 
| 
       419 
297 
     | 
    
         | 
| 
       420 
     | 
    
         
            -
            ###  
     | 
| 
      
 298 
     | 
    
         
            +
            ### En Passant
         
     | 
| 
       421 
299 
     | 
    
         | 
| 
       422 
300 
     | 
    
         
             
            ```ruby
         
     | 
| 
       423 
     | 
    
         
            -
            #  
     | 
| 
       424 
     | 
    
         
            -
             
     | 
| 
       425 
     | 
    
         
            -
             
     | 
| 
       426 
     | 
    
         
            -
             
     | 
| 
       427 
     | 
    
         
            -
             
     | 
| 
       428 
     | 
    
         
            -
             
     | 
| 
       429 
     | 
    
         
            -
             
     | 
| 
       430 
     | 
    
         
            -
            #  
     | 
| 
       431 
     | 
    
         
            -
             
     | 
| 
       432 
     | 
    
         
            -
             
     | 
| 
       433 
     | 
    
         
            -
             
     | 
| 
       434 
     | 
    
         
            -
             
     | 
| 
       435 
     | 
    
         
            -
             
     | 
| 
       436 
     | 
    
         
            -
             
     | 
| 
       437 
     | 
    
         
            -
            ```ruby
         
     | 
| 
       438 
     | 
    
         
            -
            # List all pieces
         
     | 
| 
       439 
     | 
    
         
            -
            ruleset.pieces # => ["C:K", "C:Q", "C:R", ...]
         
     | 
| 
      
 301 
     | 
    
         
            +
            # En passant capture
         
     | 
| 
      
 302 
     | 
    
         
            +
            ggn_data = {
         
     | 
| 
      
 303 
     | 
    
         
            +
              "C:P" => {
         
     | 
| 
      
 304 
     | 
    
         
            +
                "e5" => {
         
     | 
| 
      
 305 
     | 
    
         
            +
                  "f6" => [{
         
     | 
| 
      
 306 
     | 
    
         
            +
                    "must" => {
         
     | 
| 
      
 307 
     | 
    
         
            +
                      "f6" => "empty",
         
     | 
| 
      
 308 
     | 
    
         
            +
                      "f5" => "c:-p"    # Enemy pawn vulnerable to en passant
         
     | 
| 
      
 309 
     | 
    
         
            +
                    },
         
     | 
| 
      
 310 
     | 
    
         
            +
                    "deny" => {}
         
     | 
| 
      
 311 
     | 
    
         
            +
                  }]
         
     | 
| 
      
 312 
     | 
    
         
            +
                }
         
     | 
| 
      
 313 
     | 
    
         
            +
              }
         
     | 
| 
      
 314 
     | 
    
         
            +
            }
         
     | 
| 
       440 
315 
     | 
    
         | 
| 
       441 
     | 
    
         
            -
             
     | 
| 
       442 
     | 
    
         
            -
            source.sources # => ["e1", "d1", "f1", ...]
         
     | 
| 
      
 316 
     | 
    
         
            +
            ruleset = Sashite::Ggn.parse(ggn_data)
         
     | 
| 
       443 
317 
     | 
    
         | 
| 
       444 
     | 
    
         
            -
             
     | 
| 
       445 
     | 
    
         
            -
             
     | 
| 
      
 318 
     | 
    
         
            +
            squares = {
         
     | 
| 
      
 319 
     | 
    
         
            +
              "e5" => "C:P",
         
     | 
| 
      
 320 
     | 
    
         
            +
              "f5" => "c:-p",
         
     | 
| 
      
 321 
     | 
    
         
            +
              "f6" => nil
         
     | 
| 
      
 322 
     | 
    
         
            +
            }
         
     | 
| 
      
 323 
     | 
    
         
            +
            possibilities = ruleset.select("C:P").from("e5").to("f6").where(:first, squares)
         
     | 
| 
      
 324 
     | 
    
         
            +
            possibilities.any?  # => true
         
     | 
| 
       446 
325 
     | 
    
         
             
            ```
         
     | 
| 
       447 
326 
     | 
    
         | 
| 
       448 
     | 
    
         
            -
            ---
         
     | 
| 
       449 
     | 
    
         
            -
             
     | 
| 
       450 
     | 
    
         
            -
            ## Design Properties
         
     | 
| 
       451 
     | 
    
         
            -
             
     | 
| 
       452 
     | 
    
         
            -
            - **Functional**: Pure functions with no side effects
         
     | 
| 
       453 
     | 
    
         
            -
            - **Immutable**: All data structures frozen and unchangeable
         
     | 
| 
       454 
     | 
    
         
            -
            - **Composable**: Clean method chaining for natural query flow
         
     | 
| 
       455 
     | 
    
         
            -
            - **Minimal API**: Only exposes what's necessary
         
     | 
| 
       456 
     | 
    
         
            -
            - **Type-safe**: Strict validation of all inputs
         
     | 
| 
       457 
     | 
    
         
            -
            - **Lightweight**: Minimal dependencies, no unnecessary parsing
         
     | 
| 
       458 
     | 
    
         
            -
            - **Spec-compliant**: Strictly follows GGN v1.0.0 specification
         
     | 
| 
       459 
     | 
    
         
            -
             
     | 
| 
       460 
     | 
    
         
            -
            ---
         
     | 
| 
       461 
     | 
    
         
            -
             
     | 
| 
       462 
327 
     | 
    
         
             
            ## Error Handling
         
     | 
| 
       463 
328 
     | 
    
         | 
| 
       464 
329 
     | 
    
         
             
            ```ruby
         
     | 
| 
       465 
     | 
    
         
            -
            #  
     | 
| 
      
 330 
     | 
    
         
            +
            # Missing piece
         
     | 
| 
       466 
331 
     | 
    
         
             
            begin
         
     | 
| 
       467 
     | 
    
         
            -
               
     | 
| 
      
 332 
     | 
    
         
            +
              ruleset.select("X:Y")
         
     | 
| 
       468 
333 
     | 
    
         
             
            rescue KeyError => e
         
     | 
| 
       469 
     | 
    
         
            -
              puts "Piece not found:  
     | 
| 
      
 334 
     | 
    
         
            +
              puts e.message  # => "Piece not found: X:Y"
         
     | 
| 
       470 
335 
     | 
    
         
             
            end
         
     | 
| 
       471 
336 
     | 
    
         | 
| 
       472 
     | 
    
         
            -
            #  
     | 
| 
      
 337 
     | 
    
         
            +
            # Missing source
         
     | 
| 
       473 
338 
     | 
    
         
             
            begin
         
     | 
| 
       474 
     | 
    
         
            -
               
     | 
| 
      
 339 
     | 
    
         
            +
              ruleset.select("C:K").from("z9")
         
     | 
| 
       475 
340 
     | 
    
         
             
            rescue KeyError => e
         
     | 
| 
       476 
     | 
    
         
            -
              puts "Source not found:  
     | 
| 
      
 341 
     | 
    
         
            +
              puts e.message  # => "Source not found: z9"
         
     | 
| 
       477 
342 
     | 
    
         
             
            end
         
     | 
| 
       478 
343 
     | 
    
         | 
| 
       479 
     | 
    
         
            -
            #  
     | 
| 
      
 344 
     | 
    
         
            +
            # Invalid GGN data
         
     | 
| 
       480 
345 
     | 
    
         
             
            begin
         
     | 
| 
       481 
     | 
    
         
            -
               
     | 
| 
       482 
     | 
    
         
            -
            rescue  
     | 
| 
       483 
     | 
    
         
            -
              puts " 
     | 
| 
      
 346 
     | 
    
         
            +
              Sashite::Ggn.parse({ "invalid" => "data" })
         
     | 
| 
      
 347 
     | 
    
         
            +
            rescue ArgumentError => e
         
     | 
| 
      
 348 
     | 
    
         
            +
              puts e.message  # => "Invalid QPI format: invalid"
         
     | 
| 
       484 
349 
     | 
    
         
             
            end
         
     | 
| 
       485 
350 
     | 
    
         | 
| 
       486 
     | 
    
         
            -
            # Safe validation 
     | 
| 
      
 351 
     | 
    
         
            +
            # Safe validation
         
     | 
| 
       487 
352 
     | 
    
         
             
            if Sashite::Ggn.valid?(data)
         
     | 
| 
       488 
353 
     | 
    
         
             
              ruleset = Sashite::Ggn.parse(data)
         
     | 
| 
       489 
354 
     | 
    
         
             
            else
         
     | 
| 
         @@ -491,25 +356,45 @@ else 
     | 
|
| 
       491 
356 
     | 
    
         
             
            end
         
     | 
| 
       492 
357 
     | 
    
         
             
            ```
         
     | 
| 
       493 
358 
     | 
    
         | 
| 
       494 
     | 
    
         
            -
             
     | 
| 
      
 359 
     | 
    
         
            +
            ## GGN Format Restrictions
         
     | 
| 
      
 360 
     | 
    
         
            +
             
     | 
| 
      
 361 
     | 
    
         
            +
            ### HAND→HAND Prohibition
         
     | 
| 
      
 362 
     | 
    
         
            +
             
     | 
| 
      
 363 
     | 
    
         
            +
            Direct movements from hand to hand (`source="*"` and `destination="*"`) are **forbidden** by the specification:
         
     | 
| 
      
 364 
     | 
    
         
            +
             
     | 
| 
      
 365 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 366 
     | 
    
         
            +
            # This will raise an error
         
     | 
| 
      
 367 
     | 
    
         
            +
            invalid_ggn = {
         
     | 
| 
      
 368 
     | 
    
         
            +
              "S:P" => {
         
     | 
| 
      
 369 
     | 
    
         
            +
                "*" => {
         
     | 
| 
      
 370 
     | 
    
         
            +
                  "*" => [{ "must" => {}, "deny" => {} }]  # FORBIDDEN!
         
     | 
| 
      
 371 
     | 
    
         
            +
                }
         
     | 
| 
      
 372 
     | 
    
         
            +
              }
         
     | 
| 
      
 373 
     | 
    
         
            +
            }
         
     | 
| 
      
 374 
     | 
    
         
            +
             
     | 
| 
      
 375 
     | 
    
         
            +
            Sashite::Ggn.valid?(invalid_ggn)  # => false
         
     | 
| 
      
 376 
     | 
    
         
            +
            Sashite::Ggn.parse(invalid_ggn)   # => ArgumentError
         
     | 
| 
      
 377 
     | 
    
         
            +
            ```
         
     | 
| 
      
 378 
     | 
    
         
            +
             
     | 
| 
      
 379 
     | 
    
         
            +
            ## Dependencies
         
     | 
| 
       495 
380 
     | 
    
         | 
| 
       496 
     | 
    
         
            -
             
     | 
| 
      
 381 
     | 
    
         
            +
            This gem depends on other Sashité specifications:
         
     | 
| 
       497 
382 
     | 
    
         | 
| 
       498 
     | 
    
         
            -
            -  
     | 
| 
       499 
     | 
    
         
            -
            -  
     | 
| 
       500 
     | 
    
         
            -
            -  
     | 
| 
       501 
     | 
    
         
            -
            -  
     | 
| 
       502 
     | 
    
         
            -
            - [QPI v1.0.0](https://sashite.dev/specs/qpi/1.0.0/) — Piece identification
         
     | 
| 
       503 
     | 
    
         
            -
            - [STN v1.0.0](https://sashite.dev/specs/stn/1.0.0/) — State transitions
         
     | 
| 
      
 383 
     | 
    
         
            +
            - `sashite-cell` - Coordinate encoding (e.g., `"e4"`)
         
     | 
| 
      
 384 
     | 
    
         
            +
            - `sashite-hand` - Reserve notation (`"*"`)
         
     | 
| 
      
 385 
     | 
    
         
            +
            - `sashite-lcn` - Location conditions (e.g., `"empty"`, `"enemy"`)
         
     | 
| 
      
 386 
     | 
    
         
            +
            - `sashite-qpi` - Piece identification (e.g., `"C:K"`)
         
     | 
| 
       504 
387 
     | 
    
         | 
| 
       505 
     | 
    
         
            -
             
     | 
| 
      
 388 
     | 
    
         
            +
            ## Resources
         
     | 
| 
      
 389 
     | 
    
         
            +
             
     | 
| 
      
 390 
     | 
    
         
            +
            - [GGN Specification v1.0.0](https://sashite.dev/specs/ggn/1.0.0/)
         
     | 
| 
      
 391 
     | 
    
         
            +
            - [API Documentation](https://rubydoc.info/github/sashite/ggn.rb/main)
         
     | 
| 
      
 392 
     | 
    
         
            +
            - [GitHub Repository](https://github.com/sashite/ggn.rb)
         
     | 
| 
       506 
393 
     | 
    
         | 
| 
       507 
394 
     | 
    
         
             
            ## License
         
     | 
| 
       508 
395 
     | 
    
         | 
| 
       509 
396 
     | 
    
         
             
            Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
         
     | 
| 
       510 
397 
     | 
    
         | 
| 
       511 
     | 
    
         
            -
            ---
         
     | 
| 
       512 
     | 
    
         
            -
             
     | 
| 
       513 
398 
     | 
    
         
             
            ## About
         
     | 
| 
       514 
399 
     | 
    
         | 
| 
       515 
400 
     | 
    
         
             
            Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
         
     | 
| 
         @@ -1,115 +1,243 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
            require "sashite 
     | 
| 
       4 
     | 
    
         
            -
            require "sashite 
     | 
| 
       5 
     | 
    
         
            -
            require "sashite/stn"
         
     | 
| 
      
 3 
     | 
    
         
            +
            require "sashite-lcn"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "sashite-qpi"
         
     | 
| 
       6 
5 
     | 
    
         | 
| 
       7 
6 
     | 
    
         
             
            module Sashite
         
     | 
| 
       8 
7 
     | 
    
         
             
              module Ggn
         
     | 
| 
       9 
8 
     | 
    
         
             
                class Ruleset
         
     | 
| 
       10 
9 
     | 
    
         
             
                  class Source
         
     | 
| 
       11 
10 
     | 
    
         
             
                    class Destination
         
     | 
| 
       12 
     | 
    
         
            -
                      #  
     | 
| 
      
 11 
     | 
    
         
            +
                      # Movement possibility evaluator
         
     | 
| 
      
 12 
     | 
    
         
            +
                      #
         
     | 
| 
      
 13 
     | 
    
         
            +
                      # Evaluates whether movements are possible based on environmental
         
     | 
| 
      
 14 
     | 
    
         
            +
                      # pre-conditions as defined in the GGN specification v1.0.0.
         
     | 
| 
      
 15 
     | 
    
         
            +
                      #
         
     | 
| 
      
 16 
     | 
    
         
            +
                      # The Engine acts as the final stage in the GGN navigation chain,
         
     | 
| 
      
 17 
     | 
    
         
            +
                      # determining which movement possibilities from the GGN data structure
         
     | 
| 
      
 18 
     | 
    
         
            +
                      # are valid given the current board state.
         
     | 
| 
       13 
19 
     | 
    
         
             
                      #
         
     | 
| 
       14 
20 
     | 
    
         
             
                      # @see https://sashite.dev/specs/ggn/1.0.0/
         
     | 
| 
       15 
21 
     | 
    
         
             
                      class Engine
         
     | 
| 
       16 
     | 
    
         
            -
                        # Create a new Engine
         
     | 
| 
      
 22 
     | 
    
         
            +
                        # Create a new Engine with movement possibilities
         
     | 
| 
      
 23 
     | 
    
         
            +
                        #
         
     | 
| 
      
 24 
     | 
    
         
            +
                        # @note This constructor is typically called internally through the
         
     | 
| 
      
 25 
     | 
    
         
            +
                        #   navigation chain: ruleset.select(piece).from(source).to(destination)
         
     | 
| 
      
 26 
     | 
    
         
            +
                        #
         
     | 
| 
      
 27 
     | 
    
         
            +
                        # @param possibilities [Array<Hash>] Array of movement possibility
         
     | 
| 
      
 28 
     | 
    
         
            +
                        #   objects from the GGN data structure. Each possibility must contain
         
     | 
| 
      
 29 
     | 
    
         
            +
                        #   "must" and "deny" fields with LCN-formatted conditions.
         
     | 
| 
       17 
30 
     | 
    
         
             
                        #
         
     | 
| 
       18 
     | 
    
         
            -
                        # @ 
     | 
| 
      
 31 
     | 
    
         
            +
                        # @example Structure of a possibility
         
     | 
| 
      
 32 
     | 
    
         
            +
                        #   {
         
     | 
| 
      
 33 
     | 
    
         
            +
                        #     "must" => { "e3" => "empty", "e4" => "empty" },
         
     | 
| 
      
 34 
     | 
    
         
            +
                        #     "deny" => { "f3" => "enemy" }
         
     | 
| 
      
 35 
     | 
    
         
            +
                        #   }
         
     | 
| 
       19 
36 
     | 
    
         
             
                        def initialize(*possibilities)
         
     | 
| 
       20 
     | 
    
         
            -
                          @possibilities = possibilities
         
     | 
| 
      
 37 
     | 
    
         
            +
                          @possibilities = validate_and_freeze(possibilities)
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
       21 
39 
     | 
    
         
             
                          freeze
         
     | 
| 
       22 
40 
     | 
    
         
             
                        end
         
     | 
| 
       23 
41 
     | 
    
         | 
| 
       24 
     | 
    
         
            -
                        # Evaluate movement  
     | 
| 
      
 42 
     | 
    
         
            +
                        # Evaluate which movement possibilities match the current position
         
     | 
| 
       25 
43 
     | 
    
         
             
                        #
         
     | 
| 
       26 
     | 
    
         
            -
                        #  
     | 
| 
       27 
     | 
    
         
            -
                        #  
     | 
| 
       28 
     | 
    
         
            -
                        # 
     | 
| 
       29 
     | 
    
         
            -
                        # 
     | 
| 
      
 44 
     | 
    
         
            +
                        # Returns the subset of movement possibilities whose pre-conditions
         
     | 
| 
      
 45 
     | 
    
         
            +
                        # are satisfied by the current board state. This is the core evaluation
         
     | 
| 
      
 46 
     | 
    
         
            +
                        # method that determines if a movement is pseudo-legal.
         
     | 
| 
      
 47 
     | 
    
         
            +
                        #
         
     | 
| 
      
 48 
     | 
    
         
            +
                        # Each possibility is evaluated independently with the following logic:
         
     | 
| 
      
 49 
     | 
    
         
            +
                        # - All "must" conditions must be satisfied (AND logic)
         
     | 
| 
      
 50 
     | 
    
         
            +
                        # - No "deny" conditions can be satisfied (NOR logic)
         
     | 
| 
      
 51 
     | 
    
         
            +
                        #
         
     | 
| 
      
 52 
     | 
    
         
            +
                        # The "enemy" keyword in conditions is evaluated from the active
         
     | 
| 
      
 53 
     | 
    
         
            +
                        # player's perspective, following the LCN specification's standard
         
     | 
| 
      
 54 
     | 
    
         
            +
                        # interpretation.
         
     | 
| 
      
 55 
     | 
    
         
            +
                        #
         
     | 
| 
      
 56 
     | 
    
         
            +
                        # @param active_side [Symbol] Active player side (:first or :second).
         
     | 
| 
      
 57 
     | 
    
         
            +
                        #   This determines which pieces are considered "enemy" when evaluating
         
     | 
| 
      
 58 
     | 
    
         
            +
                        #   the "enemy" keyword in conditions.
         
     | 
| 
      
 59 
     | 
    
         
            +
                        # @param squares [Hash{String => String, nil}] Current board state mapping
         
     | 
| 
      
 60 
     | 
    
         
            +
                        #   CELL coordinates to QPI piece identifiers. Use nil for empty squares.
         
     | 
| 
      
 61 
     | 
    
         
            +
                        #   Only squares referenced in conditions need to be included.
         
     | 
| 
      
 62 
     | 
    
         
            +
                        #
         
     | 
| 
      
 63 
     | 
    
         
            +
                        # @return [Array<Hash>] Subset of movement possibilities that satisfy their
         
     | 
| 
      
 64 
     | 
    
         
            +
                        #   pre-conditions. Each returned Hash is the original possibility from the
         
     | 
| 
      
 65 
     | 
    
         
            +
                        #   GGN data, containing at minimum "must" and "deny" fields.
         
     | 
| 
      
 66 
     | 
    
         
            +
                        #   Returns an empty array if no possibilities match.
         
     | 
| 
      
 67 
     | 
    
         
            +
                        #
         
     | 
| 
      
 68 
     | 
    
         
            +
                        # @raise [ArgumentError] if active_side is not :first or :second
         
     | 
| 
      
 69 
     | 
    
         
            +
                        #
         
     | 
| 
      
 70 
     | 
    
         
            +
                        # @example Chess pawn two-square advance
         
     | 
| 
      
 71 
     | 
    
         
            +
                        #   active_side = :first
         
     | 
| 
      
 72 
     | 
    
         
            +
                        #   squares = {
         
     | 
| 
      
 73 
     | 
    
         
            +
                        #     "e2" => "C:P",  # White pawn on starting square
         
     | 
| 
      
 74 
     | 
    
         
            +
                        #     "e3" => nil,    # Path must be clear
         
     | 
| 
      
 75 
     | 
    
         
            +
                        #     "e4" => nil     # Destination must be empty
         
     | 
| 
      
 76 
     | 
    
         
            +
                        #   }
         
     | 
| 
      
 77 
     | 
    
         
            +
                        #   possibilities = engine.where(active_side, squares)
         
     | 
| 
      
 78 
     | 
    
         
            +
                        #   # => [{"must" => {"e3" => "empty", "e4" => "empty"}, "deny" => {}}]
         
     | 
| 
       30 
79 
     | 
    
         
             
                        #
         
     | 
| 
       31 
     | 
    
         
            -
                        # @example
         
     | 
| 
      
 80 
     | 
    
         
            +
                        # @example Capture evaluation with enemy keyword
         
     | 
| 
       32 
81 
     | 
    
         
             
                        #   active_side = :first
         
     | 
| 
       33 
82 
     | 
    
         
             
                        #   squares = {
         
     | 
| 
       34 
     | 
    
         
            -
                        #     " 
     | 
| 
       35 
     | 
    
         
            -
                        #     " 
     | 
| 
       36 
     | 
    
         
            -
                        #     "e4" => nil
         
     | 
| 
      
 83 
     | 
    
         
            +
                        #     "e4" => "C:P",  # White pawn
         
     | 
| 
      
 84 
     | 
    
         
            +
                        #     "d5" => "c:p"   # Black pawn (enemy from white's perspective)
         
     | 
| 
       37 
85 
     | 
    
         
             
                        #   }
         
     | 
| 
       38 
     | 
    
         
            -
                        #    
     | 
| 
      
 86 
     | 
    
         
            +
                        #   possibilities = engine.where(active_side, squares)
         
     | 
| 
      
 87 
     | 
    
         
            +
                        #   # => [{"must" => {"d5" => "enemy"}, "deny" => {}}]
         
     | 
| 
      
 88 
     | 
    
         
            +
                        #
         
     | 
| 
      
 89 
     | 
    
         
            +
                        # @example No matching possibilities (blocked path)
         
     | 
| 
      
 90 
     | 
    
         
            +
                        #   squares = { "e2" => "C:P", "e3" => "c:p", "e4" => nil }
         
     | 
| 
      
 91 
     | 
    
         
            +
                        #   possibilities = engine.where(active_side, squares)
         
     | 
| 
      
 92 
     | 
    
         
            +
                        #   # => []
         
     | 
| 
       39 
93 
     | 
    
         
             
                        def where(active_side, squares)
         
     | 
| 
      
 94 
     | 
    
         
            +
                          validate_active_side!(active_side)
         
     | 
| 
      
 95 
     | 
    
         
            +
                          validate_squares!(squares)
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
       40 
97 
     | 
    
         
             
                          @possibilities.select do |possibility|
         
     | 
| 
       41 
     | 
    
         
            -
                             
     | 
| 
       42 
     | 
    
         
            -
                              satisfies_deny?(possibility["deny"], active_side, squares)
         
     | 
| 
       43 
     | 
    
         
            -
                          end.map do |possibility|
         
     | 
| 
       44 
     | 
    
         
            -
                            Stn.parse(possibility["diff"])
         
     | 
| 
      
 98 
     | 
    
         
            +
                            satisfies_conditions?(possibility, active_side, squares)
         
     | 
| 
       45 
99 
     | 
    
         
             
                          end
         
     | 
| 
       46 
100 
     | 
    
         
             
                        end
         
     | 
| 
       47 
101 
     | 
    
         | 
| 
       48 
102 
     | 
    
         
             
                        private
         
     | 
| 
       49 
103 
     | 
    
         | 
| 
      
 104 
     | 
    
         
            +
                        # Validate and freeze the possibilities array
         
     | 
| 
      
 105 
     | 
    
         
            +
                        #
         
     | 
| 
      
 106 
     | 
    
         
            +
                        # @param possibilities [Array<Hash>] Possibilities to validate
         
     | 
| 
      
 107 
     | 
    
         
            +
                        # @return [Array<Hash>] Frozen array of validated possibilities
         
     | 
| 
      
 108 
     | 
    
         
            +
                        # @raise [ArgumentError] if possibilities structure is invalid
         
     | 
| 
      
 109 
     | 
    
         
            +
                        def validate_and_freeze(possibilities)
         
     | 
| 
      
 110 
     | 
    
         
            +
                          raise ::ArgumentError, "Possibilities must be an Array" unless possibilities.is_a?(::Array)
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
                          possibilities.each do |possibility|
         
     | 
| 
      
 113 
     | 
    
         
            +
                            raise ::ArgumentError, "Each possibility must be a Hash" unless possibility.is_a?(::Hash)
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                            unless possibility.key?("must") && possibility.key?("deny")
         
     | 
| 
      
 116 
     | 
    
         
            +
                              raise ::ArgumentError, "Possibility must have 'must' and 'deny' fields"
         
     | 
| 
      
 117 
     | 
    
         
            +
                            end
         
     | 
| 
      
 118 
     | 
    
         
            +
                          end
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
                          possibilities.freeze
         
     | 
| 
      
 121 
     | 
    
         
            +
                        end
         
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
      
 123 
     | 
    
         
            +
                        # Validate the active_side parameter
         
     | 
| 
      
 124 
     | 
    
         
            +
                        #
         
     | 
| 
      
 125 
     | 
    
         
            +
                        # @param active_side [Symbol] Side to validate
         
     | 
| 
      
 126 
     | 
    
         
            +
                        # @raise [ArgumentError] if side is invalid
         
     | 
| 
      
 127 
     | 
    
         
            +
                        def validate_active_side!(active_side)
         
     | 
| 
      
 128 
     | 
    
         
            +
                          return if %i[first second].include?(active_side)
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                          raise ::ArgumentError, "active_side must be :first or :second, got: #{active_side.inspect}"
         
     | 
| 
      
 131 
     | 
    
         
            +
                        end
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                        # Validate the squares parameter
         
     | 
| 
      
 134 
     | 
    
         
            +
                        #
         
     | 
| 
      
 135 
     | 
    
         
            +
                        # @param squares [Hash] Squares to validate
         
     | 
| 
      
 136 
     | 
    
         
            +
                        # @raise [ArgumentError] if squares is not a Hash
         
     | 
| 
      
 137 
     | 
    
         
            +
                        def validate_squares!(squares)
         
     | 
| 
      
 138 
     | 
    
         
            +
                          return if squares.is_a?(Hash)
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
                          raise ::ArgumentError, "squares must be a Hash, got: #{squares.class}"
         
     | 
| 
      
 141 
     | 
    
         
            +
                        end
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
                        # Check if a possibility's conditions are satisfied
         
     | 
| 
      
 144 
     | 
    
         
            +
                        #
         
     | 
| 
      
 145 
     | 
    
         
            +
                        # @param possibility [Hash] Movement possibility with "must" and "deny"
         
     | 
| 
      
 146 
     | 
    
         
            +
                        # @param active_side [Symbol] Active player side
         
     | 
| 
      
 147 
     | 
    
         
            +
                        # @param squares [Hash] Board state
         
     | 
| 
      
 148 
     | 
    
         
            +
                        # @return [Boolean] true if all conditions are satisfied
         
     | 
| 
      
 149 
     | 
    
         
            +
                        def satisfies_conditions?(possibility, active_side, squares)
         
     | 
| 
      
 150 
     | 
    
         
            +
                          must_conditions = possibility.fetch("must", {})
         
     | 
| 
      
 151 
     | 
    
         
            +
                          deny_conditions = possibility.fetch("deny", {})
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                          satisfies_must?(must_conditions, active_side, squares) &&
         
     | 
| 
      
 154 
     | 
    
         
            +
                            satisfies_deny?(deny_conditions, active_side, squares)
         
     | 
| 
      
 155 
     | 
    
         
            +
                        end
         
     | 
| 
      
 156 
     | 
    
         
            +
             
     | 
| 
       50 
157 
     | 
    
         
             
                        # Check if all 'must' conditions are satisfied
         
     | 
| 
       51 
158 
     | 
    
         
             
                        #
         
     | 
| 
       52 
     | 
    
         
            -
                        # @param conditions [Hash] LCN conditions
         
     | 
| 
      
 159 
     | 
    
         
            +
                        # @param conditions [Hash] LCN conditions that must be true
         
     | 
| 
       53 
160 
     | 
    
         
             
                        # @param active_side [Symbol] Active player side
         
     | 
| 
       54 
161 
     | 
    
         
             
                        # @param squares [Hash] Board state
         
     | 
| 
       55 
     | 
    
         
            -
                        # @return [Boolean]
         
     | 
| 
      
 162 
     | 
    
         
            +
                        # @return [Boolean] true if all conditions are met
         
     | 
| 
       56 
163 
     | 
    
         
             
                        def satisfies_must?(conditions, active_side, squares)
         
     | 
| 
       57 
     | 
    
         
            -
                          return true if conditions.empty?
         
     | 
| 
      
 164 
     | 
    
         
            +
                          return true if conditions.nil? || conditions.empty?
         
     | 
| 
       58 
165 
     | 
    
         | 
| 
       59 
     | 
    
         
            -
                           
     | 
| 
       60 
     | 
    
         
            -
             
     | 
| 
       61 
     | 
    
         
            -
                          lcn_conditions.locations.all? do |location|
         
     | 
| 
       62 
     | 
    
         
            -
                            expected_state = lcn_conditions[location]
         
     | 
| 
       63 
     | 
    
         
            -
                            check_condition(location.to_s, expected_state, active_side, squares)
         
     | 
| 
       64 
     | 
    
         
            -
                          end
         
     | 
| 
      
 166 
     | 
    
         
            +
                          evaluate_lcn_conditions(conditions, active_side, squares, :all?)
         
     | 
| 
       65 
167 
     | 
    
         
             
                        end
         
     | 
| 
       66 
168 
     | 
    
         | 
| 
       67 
     | 
    
         
            -
                        # Check if  
     | 
| 
      
 169 
     | 
    
         
            +
                        # Check if no 'deny' conditions are satisfied
         
     | 
| 
       68 
170 
     | 
    
         
             
                        #
         
     | 
| 
       69 
     | 
    
         
            -
                        # @param conditions [Hash] LCN conditions
         
     | 
| 
      
 171 
     | 
    
         
            +
                        # @param conditions [Hash] LCN conditions that must be false
         
     | 
| 
       70 
172 
     | 
    
         
             
                        # @param active_side [Symbol] Active player side
         
     | 
| 
       71 
173 
     | 
    
         
             
                        # @param squares [Hash] Board state
         
     | 
| 
       72 
     | 
    
         
            -
                        # @return [Boolean]
         
     | 
| 
      
 174 
     | 
    
         
            +
                        # @return [Boolean] true if none of the conditions are met
         
     | 
| 
       73 
175 
     | 
    
         
             
                        def satisfies_deny?(conditions, active_side, squares)
         
     | 
| 
       74 
     | 
    
         
            -
                          return true if conditions.empty?
         
     | 
| 
      
 176 
     | 
    
         
            +
                          return true if conditions.nil? || conditions.empty?
         
     | 
| 
      
 177 
     | 
    
         
            +
             
     | 
| 
      
 178 
     | 
    
         
            +
                          evaluate_lcn_conditions(conditions, active_side, squares, :none?)
         
     | 
| 
      
 179 
     | 
    
         
            +
                        end
         
     | 
| 
       75 
180 
     | 
    
         | 
| 
       76 
     | 
    
         
            -
             
     | 
| 
      
 181 
     | 
    
         
            +
                        # Evaluate LCN conditions using specified logic
         
     | 
| 
      
 182 
     | 
    
         
            +
                        #
         
     | 
| 
      
 183 
     | 
    
         
            +
                        # @param conditions [Hash] LCN conditions to evaluate
         
     | 
| 
      
 184 
     | 
    
         
            +
                        # @param active_side [Symbol] Active player side
         
     | 
| 
      
 185 
     | 
    
         
            +
                        # @param squares [Hash] Board state
         
     | 
| 
      
 186 
     | 
    
         
            +
                        # @param logic_method [Symbol] :all? or :none? for AND/NOR logic
         
     | 
| 
      
 187 
     | 
    
         
            +
                        # @return [Boolean] Result of condition evaluation
         
     | 
| 
      
 188 
     | 
    
         
            +
                        def evaluate_lcn_conditions(conditions, active_side, squares, logic_method)
         
     | 
| 
      
 189 
     | 
    
         
            +
                          # Parse conditions through LCN for validation
         
     | 
| 
      
 190 
     | 
    
         
            +
                          lcn = ::Sashite::Lcn.parse(conditions)
         
     | 
| 
       77 
191 
     | 
    
         | 
| 
       78 
     | 
    
         
            -
                           
     | 
| 
       79 
     | 
    
         
            -
             
     | 
| 
       80 
     | 
    
         
            -
                             
     | 
| 
      
 192 
     | 
    
         
            +
                          # Evaluate each location condition using the specified logic
         
     | 
| 
      
 193 
     | 
    
         
            +
                          lcn.locations.public_send(logic_method) do |location|
         
     | 
| 
      
 194 
     | 
    
         
            +
                            expected_state = lcn[location]
         
     | 
| 
      
 195 
     | 
    
         
            +
                            location_matches?(location.to_s, expected_state, active_side, squares)
         
     | 
| 
       81 
196 
     | 
    
         
             
                          end
         
     | 
| 
       82 
197 
     | 
    
         
             
                        end
         
     | 
| 
       83 
198 
     | 
    
         | 
| 
       84 
     | 
    
         
            -
                        # Check if a location  
     | 
| 
      
 199 
     | 
    
         
            +
                        # Check if a specific location matches expected state
         
     | 
| 
       85 
200 
     | 
    
         
             
                        #
         
     | 
| 
       86 
     | 
    
         
            -
                        #  
     | 
| 
       87 
     | 
    
         
            -
                        #  
     | 
| 
       88 
     | 
    
         
            -
                        #  
     | 
| 
      
 201 
     | 
    
         
            +
                        # Evaluates a single location condition against the board state.
         
     | 
| 
      
 202 
     | 
    
         
            +
                        # Handles the three types of LCN state values:
         
     | 
| 
      
 203 
     | 
    
         
            +
                        # - "empty": location must be unoccupied
         
     | 
| 
      
 204 
     | 
    
         
            +
                        # - "enemy": location must contain an opponent's piece
         
     | 
| 
      
 205 
     | 
    
         
            +
                        # - QPI identifier: location must contain exactly this piece
         
     | 
| 
      
 206 
     | 
    
         
            +
                        #
         
     | 
| 
      
 207 
     | 
    
         
            +
                        # @param location [String] CELL coordinate to check
         
     | 
| 
      
 208 
     | 
    
         
            +
                        # @param expected_state [String] Expected state value from LCN
         
     | 
| 
      
 209 
     | 
    
         
            +
                        # @param active_side [Symbol] Active player side for enemy evaluation
         
     | 
| 
       89 
210 
     | 
    
         
             
                        # @param squares [Hash] Board state
         
     | 
| 
       90 
     | 
    
         
            -
                        # @return [Boolean]
         
     | 
| 
       91 
     | 
    
         
            -
                        def  
     | 
| 
       92 
     | 
    
         
            -
                           
     | 
| 
      
 211 
     | 
    
         
            +
                        # @return [Boolean] true if location matches expected state
         
     | 
| 
      
 212 
     | 
    
         
            +
                        def location_matches?(location, expected_state, active_side, squares)
         
     | 
| 
      
 213 
     | 
    
         
            +
                          actual_value = squares[location]
         
     | 
| 
       93 
214 
     | 
    
         | 
| 
       94 
215 
     | 
    
         
             
                          case expected_state
         
     | 
| 
       95 
216 
     | 
    
         
             
                          when "empty"
         
     | 
| 
       96 
     | 
    
         
            -
                             
     | 
| 
      
 217 
     | 
    
         
            +
                            # Location must be unoccupied
         
     | 
| 
      
 218 
     | 
    
         
            +
                            actual_value.nil?
         
     | 
| 
       97 
219 
     | 
    
         
             
                          when "enemy"
         
     | 
| 
       98 
     | 
    
         
            -
                             
     | 
| 
      
 220 
     | 
    
         
            +
                            # Location must contain opponent's piece
         
     | 
| 
      
 221 
     | 
    
         
            +
                            # nil check prevents false positives on empty squares
         
     | 
| 
      
 222 
     | 
    
         
            +
                            !actual_value.nil? && enemy_piece?(actual_value, active_side)
         
     | 
| 
       99 
223 
     | 
    
         
             
                          else
         
     | 
| 
       100 
     | 
    
         
            -
                            #  
     | 
| 
       101 
     | 
    
         
            -
                             
     | 
| 
      
 224 
     | 
    
         
            +
                            # Direct QPI comparison for specific piece requirement
         
     | 
| 
      
 225 
     | 
    
         
            +
                            actual_value == expected_state
         
     | 
| 
       102 
226 
     | 
    
         
             
                          end
         
     | 
| 
       103 
227 
     | 
    
         
             
                        end
         
     | 
| 
       104 
228 
     | 
    
         | 
| 
       105 
     | 
    
         
            -
                        #  
     | 
| 
      
 229 
     | 
    
         
            +
                        # Determine if a piece belongs to the opponent
         
     | 
| 
       106 
230 
     | 
    
         
             
                        #
         
     | 
| 
       107 
     | 
    
         
            -
                        #  
     | 
| 
       108 
     | 
    
         
            -
                        #  
     | 
| 
       109 
     | 
    
         
            -
                        #  
     | 
| 
       110 
     | 
    
         
            -
                         
     | 
| 
       111 
     | 
    
         
            -
             
     | 
| 
       112 
     | 
    
         
            -
             
     | 
| 
      
 231 
     | 
    
         
            +
                        # Uses QPI parsing to extract the piece's side and compares it
         
     | 
| 
      
 232 
     | 
    
         
            +
                        # with the active player's side. A piece is considered enemy if
         
     | 
| 
      
 233 
     | 
    
         
            +
                        # its side differs from the active player's side.
         
     | 
| 
      
 234 
     | 
    
         
            +
                        #
         
     | 
| 
      
 235 
     | 
    
         
            +
                        # @param qpi_identifier [String] QPI piece identifier (e.g., "C:K", "c:p")
         
     | 
| 
      
 236 
     | 
    
         
            +
                        # @param active_side [Symbol] Active player side (:first or :second)
         
     | 
| 
      
 237 
     | 
    
         
            +
                        # @return [Boolean] true if piece belongs to opponent
         
     | 
| 
      
 238 
     | 
    
         
            +
                        def enemy_piece?(qpi_identifier, active_side)
         
     | 
| 
      
 239 
     | 
    
         
            +
                          piece = ::Sashite::Qpi.parse(qpi_identifier)
         
     | 
| 
      
 240 
     | 
    
         
            +
                          piece.side != active_side
         
     | 
| 
       113 
241 
     | 
    
         
             
                        end
         
     | 
| 
       114 
242 
     | 
    
         
             
                      end
         
     | 
| 
       115 
243 
     | 
    
         
             
                    end
         
     | 
    
        data/lib/sashite/ggn/ruleset.rb
    CHANGED
    
    
    
        data/lib/sashite/ggn.rb
    CHANGED
    
    | 
         @@ -4,7 +4,6 @@ require "sashite/cell" 
     | 
|
| 
       4 
4 
     | 
    
         
             
            require "sashite/hand"
         
     | 
| 
       5 
5 
     | 
    
         
             
            require "sashite/lcn"
         
     | 
| 
       6 
6 
     | 
    
         
             
            require "sashite/qpi"
         
     | 
| 
       7 
     | 
    
         
            -
            require "sashite/stn"
         
     | 
| 
       8 
7 
     | 
    
         | 
| 
       9 
8 
     | 
    
         
             
            require_relative "ggn/ruleset"
         
     | 
| 
       10 
9 
     | 
    
         | 
| 
         @@ -29,11 +28,7 @@ module Sashite 
     | 
|
| 
       29 
28 
     | 
    
         
             
                #         "e4" => [
         
     | 
| 
       30 
29 
     | 
    
         
             
                #           {
         
     | 
| 
       31 
30 
     | 
    
         
             
                #             "must" => { "e3" => "empty", "e4" => "empty" },
         
     | 
| 
       32 
     | 
    
         
            -
                #             "deny" => {} 
     | 
| 
       33 
     | 
    
         
            -
                #             "diff" => {
         
     | 
| 
       34 
     | 
    
         
            -
                #               "board" => { "e2" => nil, "e4" => "C:P" },
         
     | 
| 
       35 
     | 
    
         
            -
                #               "toggle" => true
         
     | 
| 
       36 
     | 
    
         
            -
                #             }
         
     | 
| 
      
 31 
     | 
    
         
            +
                #             "deny" => {}
         
     | 
| 
       37 
32 
     | 
    
         
             
                #           }
         
     | 
| 
       38 
33 
     | 
    
         
             
                #         ]
         
     | 
| 
       39 
34 
     | 
    
         
             
                #       }
         
     | 
| 
         @@ -84,7 +79,7 @@ module Sashite 
     | 
|
| 
       84 
79 
     | 
    
         
             
                # @api private
         
     | 
| 
       85 
80 
     | 
    
         
             
                def self.validate_piece!(piece)
         
     | 
| 
       86 
81 
     | 
    
         
             
                  raise ::ArgumentError, "Invalid piece identifier: #{piece}" unless piece.is_a?(::String)
         
     | 
| 
       87 
     | 
    
         
            -
                  raise ::ArgumentError, "Invalid QPI format: #{piece}" unless Qpi.valid?(piece)
         
     | 
| 
      
 82 
     | 
    
         
            +
                  raise ::ArgumentError, "Invalid QPI format: #{piece}" unless ::Sashite::Qpi.valid?(piece)
         
     | 
| 
       88 
83 
     | 
    
         
             
                end
         
     | 
| 
       89 
84 
     | 
    
         
             
                private_class_method :validate_piece!
         
     | 
| 
       90 
85 
     | 
    
         | 
| 
         @@ -132,7 +127,7 @@ module Sashite 
     | 
|
| 
       132 
127 
     | 
    
         
             
                # @return [void]
         
     | 
| 
       133 
128 
     | 
    
         
             
                # @api private
         
     | 
| 
       134 
129 
     | 
    
         
             
                def self.validate_hand_to_hand!(source, destination)
         
     | 
| 
       135 
     | 
    
         
            -
                  return unless Hand.reserve?(source) && Hand.reserve?(destination)
         
     | 
| 
      
 130 
     | 
    
         
            +
                  return unless ::Sashite::Hand.reserve?(source) && ::Sashite::Hand.reserve?(destination)
         
     | 
| 
       136 
131 
     | 
    
         | 
| 
       137 
132 
     | 
    
         
             
                  raise ::ArgumentError, "Invalid HAND→HAND movement: source and destination cannot both be '*' (forbidden by GGN specification)"
         
     | 
| 
       138 
133 
     | 
    
         
             
                end
         
     | 
| 
         @@ -173,11 +168,9 @@ module Sashite 
     | 
|
| 
       173 
168 
     | 
    
         
             
                  end
         
     | 
| 
       174 
169 
     | 
    
         
             
                  raise ::ArgumentError, "Possibility must have 'must' field" unless possibility.key?("must")
         
     | 
| 
       175 
170 
     | 
    
         
             
                  raise ::ArgumentError, "Possibility must have 'deny' field" unless possibility.key?("deny")
         
     | 
| 
       176 
     | 
    
         
            -
                  raise ::ArgumentError, "Possibility must have 'diff' field" unless possibility.key?("diff")
         
     | 
| 
       177 
171 
     | 
    
         | 
| 
       178 
172 
     | 
    
         
             
                  validate_lcn_conditions!(possibility["must"], "must", piece, source, destination)
         
     | 
| 
       179 
173 
     | 
    
         
             
                  validate_lcn_conditions!(possibility["deny"], "deny", piece, source, destination)
         
     | 
| 
       180 
     | 
    
         
            -
                  validate_stn_transition!(possibility["diff"], piece, source, destination)
         
     | 
| 
       181 
174 
     | 
    
         
             
                end
         
     | 
| 
       182 
175 
     | 
    
         
             
                private_class_method :validate_possibility!
         
     | 
| 
       183 
176 
     | 
    
         | 
| 
         @@ -192,28 +185,12 @@ module Sashite 
     | 
|
| 
       192 
185 
     | 
    
         
             
                # @return [void]
         
     | 
| 
       193 
186 
     | 
    
         
             
                # @api private
         
     | 
| 
       194 
187 
     | 
    
         
             
                def self.validate_lcn_conditions!(conditions, field_name, piece, source, destination)
         
     | 
| 
       195 
     | 
    
         
            -
                  Lcn.parse(conditions)
         
     | 
| 
      
 188 
     | 
    
         
            +
                  ::Sashite::Lcn.parse(conditions)
         
     | 
| 
       196 
189 
     | 
    
         
             
                rescue ::ArgumentError => e
         
     | 
| 
       197 
190 
     | 
    
         
             
                  raise ::ArgumentError, "Invalid LCN format in '#{field_name}' for #{piece} #{source}→#{destination}: #{e.message}"
         
     | 
| 
       198 
191 
     | 
    
         
             
                end
         
     | 
| 
       199 
192 
     | 
    
         
             
                private_class_method :validate_lcn_conditions!
         
     | 
| 
       200 
193 
     | 
    
         | 
| 
       201 
     | 
    
         
            -
                # Validate STN transition
         
     | 
| 
       202 
     | 
    
         
            -
                #
         
     | 
| 
       203 
     | 
    
         
            -
                # @param transition [Hash] Transition to validate
         
     | 
| 
       204 
     | 
    
         
            -
                # @param piece [String] Piece identifier (for error messages)
         
     | 
| 
       205 
     | 
    
         
            -
                # @param source [String] Source location (for error messages)
         
     | 
| 
       206 
     | 
    
         
            -
                # @param destination [String] Destination location (for error messages)
         
     | 
| 
       207 
     | 
    
         
            -
                # @raise [ArgumentError] If transition is invalid
         
     | 
| 
       208 
     | 
    
         
            -
                # @return [void]
         
     | 
| 
       209 
     | 
    
         
            -
                # @api private
         
     | 
| 
       210 
     | 
    
         
            -
                def self.validate_stn_transition!(transition, piece, source, destination)
         
     | 
| 
       211 
     | 
    
         
            -
                  Stn.parse(transition)
         
     | 
| 
       212 
     | 
    
         
            -
                rescue ::StandardError => e
         
     | 
| 
       213 
     | 
    
         
            -
                  raise ::ArgumentError, "Invalid STN format in 'diff' for #{piece} #{source}→#{destination}: #{e.message}"
         
     | 
| 
       214 
     | 
    
         
            -
                end
         
     | 
| 
       215 
     | 
    
         
            -
                private_class_method :validate_stn_transition!
         
     | 
| 
       216 
     | 
    
         
            -
             
     | 
| 
       217 
194 
     | 
    
         
             
                # Validate location format
         
     | 
| 
       218 
195 
     | 
    
         
             
                #
         
     | 
| 
       219 
196 
     | 
    
         
             
                # @param location [String] Location to validate
         
     | 
| 
         @@ -224,7 +201,7 @@ module Sashite 
     | 
|
| 
       224 
201 
     | 
    
         
             
                def self.validate_location!(location, piece)
         
     | 
| 
       225 
202 
     | 
    
         
             
                  raise ::ArgumentError, "Location for #{piece} must be a String" unless location.is_a?(::String)
         
     | 
| 
       226 
203 
     | 
    
         | 
| 
       227 
     | 
    
         
            -
                  valid = Cell.valid?(location) || Hand.reserve?(location)
         
     | 
| 
      
 204 
     | 
    
         
            +
                  valid = ::Sashite::Cell.valid?(location) || ::Sashite::Hand.reserve?(location)
         
     | 
| 
       228 
205 
     | 
    
         
             
                  raise ::ArgumentError, "Invalid location format: #{location}" unless valid
         
     | 
| 
       229 
206 
     | 
    
         
             
                end
         
     | 
| 
       230 
207 
     | 
    
         
             
                private_class_method :validate_location!
         
     | 
    
        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.10.0
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Cyril Kato
         
     | 
| 
         @@ -65,26 +65,11 @@ dependencies: 
     | 
|
| 
       65 
65 
     | 
    
         
             
                - - "~>"
         
     | 
| 
       66 
66 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       67 
67 
     | 
    
         
             
                    version: '1.0'
         
     | 
| 
       68 
     | 
    
         
            -
            - !ruby/object:Gem::Dependency
         
     | 
| 
       69 
     | 
    
         
            -
              name: sashite-stn
         
     | 
| 
       70 
     | 
    
         
            -
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
       71 
     | 
    
         
            -
                requirements:
         
     | 
| 
       72 
     | 
    
         
            -
                - - "~>"
         
     | 
| 
       73 
     | 
    
         
            -
                  - !ruby/object:Gem::Version
         
     | 
| 
       74 
     | 
    
         
            -
                    version: '1.0'
         
     | 
| 
       75 
     | 
    
         
            -
              type: :runtime
         
     | 
| 
       76 
     | 
    
         
            -
              prerelease: false
         
     | 
| 
       77 
     | 
    
         
            -
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
       78 
     | 
    
         
            -
                requirements:
         
     | 
| 
       79 
     | 
    
         
            -
                - - "~>"
         
     | 
| 
       80 
     | 
    
         
            -
                  - !ruby/object:Gem::Version
         
     | 
| 
       81 
     | 
    
         
            -
                    version: '1.0'
         
     | 
| 
       82 
68 
     | 
    
         
             
            description: A pure functional Ruby implementation of the General Gameplay Notation
         
     | 
| 
       83 
69 
     | 
    
         
             
              (GGN) specification v1.0.0. Provides a movement possibility oracle for evaluating
         
     | 
| 
       84 
70 
     | 
    
         
             
              pseudo-legal moves in abstract strategy board games. Features include hierarchical
         
     | 
| 
       85 
71 
     | 
    
         
             
              move navigation (piece → source → destination → transitions), pre-condition evaluation
         
     | 
| 
       86 
     | 
    
         
            -
              (must/deny) 
     | 
| 
       87 
     | 
    
         
            -
              Xiangqi, and custom variants.
         
     | 
| 
      
 72 
     | 
    
         
            +
              (must/deny). Works with Chess, Shogi, Xiangqi, and custom variants.
         
     | 
| 
       88 
73 
     | 
    
         
             
            email: contact@cyril.email
         
     | 
| 
       89 
74 
     | 
    
         
             
            executables: []
         
     | 
| 
       90 
75 
     | 
    
         
             
            extensions: []
         
     | 
| 
         @@ -122,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement 
     | 
|
| 
       122 
107 
     | 
    
         
             
                - !ruby/object:Gem::Version
         
     | 
| 
       123 
108 
     | 
    
         
             
                  version: '0'
         
     | 
| 
       124 
109 
     | 
    
         
             
            requirements: []
         
     | 
| 
       125 
     | 
    
         
            -
            rubygems_version: 3. 
     | 
| 
      
 110 
     | 
    
         
            +
            rubygems_version: 3.6.9
         
     | 
| 
       126 
111 
     | 
    
         
             
            specification_version: 4
         
     | 
| 
       127 
112 
     | 
    
         
             
            summary: General Gameplay Notation (GGN) - movement possibilities for board games
         
     | 
| 
       128 
113 
     | 
    
         
             
            test_files: []
         
     |