sashite-ggn 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/sashite-ggn.rb CHANGED
@@ -1,126 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Sashité - Abstract Strategy Board Games Notation Library
4
- #
5
- # This library provides a comprehensive implementation of the General Gameplay Notation (GGN)
6
- # specification, which is a rule-agnostic, JSON-based format for describing pseudo-legal
7
- # moves in abstract strategy board games.
8
- #
9
- # GGN focuses exclusively on board-to-board transformations: pieces moving, capturing,
10
- # or transforming on the game board. Hand management, drops, and captures-to-hand are
11
- # outside the scope of this specification.
12
- #
13
- # GGN works alongside other Sashité specifications:
14
- # - GAN (General Actor Notation): Unique piece identifiers
15
- # - FEEN (Forsyth-Edwards Enhanced Notation): Board position representation
16
- # - PMN (Portable Move Notation): Move sequence representation
17
- #
18
- # @author Sashité <https://sashite.com/>
19
- # @version 1.0.0
20
- # @see https://sashite.dev/documents/ggn/1.0.0/ GGN Specification
21
- # @see https://github.com/sashite/ggn.rb Official Ruby implementation
22
- #
23
- # @example Basic usage with a chess pawn double move
24
- # # Load GGN data from file
25
- # require "sashite/ggn"
26
- #
27
- # piece_data = Sashite::Ggn.load_file("chess_moves.json")
28
- # engine = piece_data.select("CHESS:P").from("e2").to("e4")
29
- #
30
- # # Check if the move is valid given current board state
31
- # board_state = {
32
- # "e2" => "CHESS:P", # White pawn on e2
33
- # "e3" => nil, # Empty square
34
- # "e4" => nil # Empty square
35
- # }
36
- #
37
- # transitions = engine.where(board_state, "CHESS")
38
- #
39
- # if transitions.any?
40
- # transition = transitions.first
41
- # puts "Move is valid!"
42
- # puts "Board changes: #{transition.diff}"
43
- # # => { "e2" => nil, "e4" => "CHESS:P" }
44
- # else
45
- # puts "Move is not valid under current conditions"
46
- # end
47
- #
48
- # @example Piece promotion with multiple variants
49
- # # Chess pawn promotion offers multiple choices
50
- # piece_data = Sashite::Ggn.load_file("chess_moves.json")
51
- # engine = piece_data.select("CHESS:P").from("e7").to("e8")
52
- #
53
- # # Board with pawn ready to promote
54
- # board_state = {
55
- # "e7" => "CHESS:P", # White pawn on 7th rank
56
- # "e8" => nil # Empty promotion square
57
- # }
58
- #
59
- # transitions = engine.where(board_state, "CHESS")
60
- #
61
- # transitions.each_with_index do |transition, i|
62
- # promoted_piece = transition.diff["e8"]
63
- # puts "Promotion choice #{i + 1}: #{promoted_piece}"
64
- # end
65
- # # Output: CHESS:Q, CHESS:R, CHESS:B, CHESS:N
66
- #
67
- # @example Complex multi-square moves like castling
68
- # # Castling involves both king and rook movement
69
- # piece_data = Sashite::Ggn.load_file("chess_moves.json")
70
- # engine = piece_data.select("CHESS:K").from("e1").to("g1")
71
- #
72
- # # Board state allowing kingside castling
73
- # board_state = {
74
- # "e1" => "CHESS:K", # King on starting square
75
- # "f1" => nil, # Empty square
76
- # "g1" => nil, # Empty destination
77
- # "h1" => "CHESS:R" # Rook on starting square
78
- # }
79
- #
80
- # transitions = engine.where(board_state, "CHESS")
81
- #
82
- # if transitions.any?
83
- # transition = transitions.first
84
- # puts "Castling is possible!"
85
- # puts "Final position: #{transition.diff}"
86
- # # => { "e1" => nil, "f1" => "CHESS:R", "g1" => "CHESS:K", "h1" => nil }
87
- # end
88
- #
89
- # @example Loading GGN data from different sources
90
- # # From file
91
- # piece_data = Sashite::Ggn.load_file("moves.json")
92
- #
93
- # # From JSON string
94
- # json_string = '{"CHESS:K": {"e1": {"e2": [{"perform": {"e1": null, "e2": "CHESS:K"}}]}}}'
95
- # piece_data = Sashite::Ggn.load_string(json_string)
96
- #
97
- # # From Hash
98
- # ggn_hash = { "CHESS:K" => { "e1" => { "e2" => [{ "perform" => { "e1" => nil, "e2" => "CHESS:K" } }] } } }
99
- # piece_data = Sashite::Ggn.load_hash(ggn_hash)
100
- #
101
- # @example Generating all possible moves
102
- # # Get all pseudo-legal moves for the current position
103
- # board_state = {
104
- # "e1" => "CHESS:K", "d1" => "CHESS:Q", "a1" => "CHESS:R",
105
- # "e2" => "CHESS:P", "d2" => "CHESS:P"
106
- # }
3
+ require_relative "sashite/ggn"
4
+
5
+ # Sashité namespace for board game notation libraries
107
6
  #
108
- # all_moves = piece_data.pseudo_legal_transitions(board_state, "CHESS")
7
+ # Sashité provides a collection of libraries for representing and manipulating
8
+ # board game concepts according to the Sashité Protocol specifications.
109
9
  #
110
- # all_moves.each do |actor, origin, target, transitions|
111
- # puts "#{actor}: #{origin} #{target} (#{transitions.size} variants)"
112
- # end
10
+ # @see https://sashite.dev/protocol/ Sashité Protocol
11
+ # @see https://sashite.dev/specs/ Sashité Specifications
12
+ # @author Sashité
113
13
  module Sashite
114
- # Base namespace for all Sashité notation libraries.
115
- #
116
- # Sashité provides a comprehensive suite of specifications and implementations
117
- # for representing abstract strategy board games in a rule-agnostic manner.
118
- # This allows for unified game engines, cross-game analysis, and hybrid
119
- # game variants.
120
- #
121
- # @see https://sashite.com/ Official Sashité website
122
- # @see https://sashite.dev/ Developer documentation and specifications
123
14
  end
124
-
125
- # Load the main GGN implementation
126
- require_relative "sashite/ggn"
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.7.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -10,26 +10,81 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: json_schemer
13
+ name: sashite-cell
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 2.4.0
18
+ version: '2.0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 2.4.0
26
- description: A Ruby implementation of the General Gameplay Notation (GGN) specification.
27
- GGN is a rule-agnostic, JSON-based format for describing pseudo-legal board-to-board
28
- transformations in abstract strategy board games. This library provides parsing,
29
- validation, and evaluation capabilities for GGN documents, focusing exclusively
30
- on piece movements, captures, and transformations on the game board. Features flexible
31
- validation system for both safety and performance. Supports Chess, Shogi, Xiangqi,
32
- and custom variants without hand management or piece drops.
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: sashite-hand
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: sashite-lcn
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.1'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: sashite-qpi
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
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
+ description: A pure functional Ruby implementation of the General Gameplay Notation
83
+ (GGN) specification v1.0.0. Provides a movement possibility oracle for evaluating
84
+ pseudo-legal moves in abstract strategy board games. Features include hierarchical
85
+ move navigation (piece → source → destination → transitions), pre-condition evaluation
86
+ (must/deny), and state transition support via STN format. Works with Chess, Shogi,
87
+ Xiangqi, and custom variants.
33
88
  email: contact@cyril.email
34
89
  executables: []
35
90
  extensions: []
@@ -39,14 +94,10 @@ files:
39
94
  - README.md
40
95
  - lib/sashite-ggn.rb
41
96
  - lib/sashite/ggn.rb
42
- - lib/sashite/ggn/move_validator.rb
43
97
  - lib/sashite/ggn/ruleset.rb
44
98
  - lib/sashite/ggn/ruleset/source.rb
45
99
  - lib/sashite/ggn/ruleset/source/destination.rb
46
100
  - lib/sashite/ggn/ruleset/source/destination/engine.rb
47
- - lib/sashite/ggn/ruleset/source/destination/engine/transition.rb
48
- - lib/sashite/ggn/schema.rb
49
- - lib/sashite/ggn/validation_error.rb
50
101
  homepage: https://github.com/sashite/ggn.rb
51
102
  licenses:
52
103
  - MIT
@@ -55,11 +106,8 @@ metadata:
55
106
  documentation_uri: https://rubydoc.info/github/sashite/ggn.rb/main
56
107
  homepage_uri: https://github.com/sashite/ggn.rb
57
108
  source_code_uri: https://github.com/sashite/ggn.rb
58
- specification_uri: https://sashite.dev/documents/ggn/1.0.0/
109
+ specification_uri: https://sashite.dev/specs/ggn/1.0.0/
59
110
  rubygems_mfa_required: 'true'
60
- keywords: board-game, chess, game, gameplay, json, makruk, notation, performance,
61
- pseudo-legal-move, rule-agnostic, serialization, shogi, strategy, validation,
62
- xiangqi
63
111
  rdoc_options: []
64
112
  require_paths:
65
113
  - lib
@@ -76,5 +124,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
124
  requirements: []
77
125
  rubygems_version: 3.6.9
78
126
  specification_version: 4
79
- summary: General Gameplay Notation (GGN) library for board-to-board game transformations
127
+ summary: General Gameplay Notation (GGN) - movement possibilities for board games
80
128
  test_files: []
@@ -1,208 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module Ggn
5
- # Centralized module for move condition validation.
6
- # Contains shared logic for validating piece ownership and board positions
7
- # in GGN move evaluation.
8
- #
9
- # This module focuses exclusively on board-based validation since GGN
10
- # only handles board-to-board transformations. All methods work with
11
- # pieces on the board and use GAN (General Actor Notation) identifiers.
12
- module MoveValidator
13
- # Separator in GAN (General Actor Notation) identifiers.
14
- # Used to split game identifiers from piece identifiers.
15
- #
16
- # @example GAN format
17
- # "CHESS:K" # game: "CHESS", piece: "K"
18
- # "shogi:+p" # game: "shogi", piece: "+p"
19
- GAN_SEPARATOR = ":"
20
-
21
- private
22
-
23
- # Checks if the correct piece is present at the origin square on the board.
24
- #
25
- # This method validates that the expected piece is actually present at the
26
- # specified origin square, which is a fundamental requirement for any move.
27
- #
28
- # @param actor [String] GAN identifier of the piece
29
- # @param origin [String] Origin square
30
- # @param board_state [Hash] Current board state
31
- #
32
- # @return [Boolean] true if the piece is at the correct position
33
- #
34
- # @example Valid piece placement
35
- # board_state = { "e1" => "CHESS:K", "e2" => "CHESS:P" }
36
- # piece_on_board_at_origin?("CHESS:K", "e1", board_state)
37
- # # => true
38
- #
39
- # @example Invalid piece placement
40
- # board_state = { "e1" => "CHESS:Q", "e2" => "CHESS:P" }
41
- # piece_on_board_at_origin?("CHESS:K", "e1", board_state)
42
- # # => false (wrong piece at e1)
43
- #
44
- # @example Empty square
45
- # board_state = { "e1" => nil, "e2" => "CHESS:P" }
46
- # piece_on_board_at_origin?("CHESS:K", "e1", board_state)
47
- # # => false (no piece at e1)
48
- def piece_on_board_at_origin?(actor, origin, board_state)
49
- return false unless valid_gan_format?(actor)
50
- return false unless origin.is_a?(String) && !origin.empty?
51
- return false unless board_state.is_a?(Hash)
52
-
53
- board_state[origin] == actor
54
- end
55
-
56
- # Checks if the piece belongs to the current player based on case matching.
57
- #
58
- # This method implements the corrected ownership logic based on FEEN specification:
59
- # - Ownership is determined by case correspondence, not exact string matching
60
- # - If active_game is uppercase, the player owns uppercase-cased pieces
61
- # - If active_game is lowercase, the player owns lowercase-cased pieces
62
- # - This allows for hybrid games where a player may control pieces from different games
63
- #
64
- # @param actor [String] GAN identifier of the piece
65
- # @param active_game [String] Current player's game identifier
66
- #
67
- # @return [Boolean] true if the piece belongs to the current player
68
- #
69
- # @example Same game, same case (typical scenario)
70
- # piece_belongs_to_current_player?("CHESS:K", "CHESS")
71
- # # => true (both uppercase)
72
- #
73
- # @example Different games, same case (hybrid scenario)
74
- # piece_belongs_to_current_player?("MAKRUK:K", "CHESS")
75
- # # => true (both uppercase, player controls both)
76
- #
77
- # @example Same game, different case
78
- # piece_belongs_to_current_player?("chess:k", "CHESS")
79
- # # => false (different players)
80
- #
81
- # @example Mixed case active_game (invalid)
82
- # piece_belongs_to_current_player?("CHESS:K", "Chess")
83
- # # => false (invalid active_game format)
84
- def piece_belongs_to_current_player?(actor, active_game)
85
- return false unless valid_gan_format?(actor)
86
- return false unless valid_game_identifier?(active_game)
87
-
88
- game_part, piece_part = actor.split(GAN_SEPARATOR, 2)
89
-
90
- # Determine player ownership based on case correspondence
91
- # If active_game is uppercase, player owns uppercase pieces
92
- # If active_game is lowercase, player owns lowercase pieces
93
- case active_game
94
- when active_game.upcase
95
- # Current player is the uppercase one
96
- game_part == game_part.upcase && piece_part.match?(/\A[-+]?[A-Z]'?\z/)
97
- when active_game.downcase
98
- # Current player is the lowercase one
99
- game_part == game_part.downcase && piece_part.match?(/\A[-+]?[a-z]'?\z/)
100
- else
101
- # active_game is neither entirely uppercase nor lowercase
102
- false
103
- end
104
- end
105
-
106
- # Validates the GAN format of an identifier.
107
- #
108
- # A valid GAN identifier must:
109
- # - Be a string containing exactly one colon separator
110
- # - Have a valid game identifier before the colon
111
- # - Have a valid piece identifier after the colon
112
- # - Maintain case consistency between game and piece parts
113
- #
114
- # @param actor [String] Identifier to validate
115
- #
116
- # @return [Boolean] true if the format is valid
117
- #
118
- # @example Valid GAN identifiers
119
- # valid_gan_format?("CHESS:K") # => true
120
- # valid_gan_format?("shogi:+p") # => true
121
- # valid_gan_format?("MAKRUK:R'") # => true
122
- #
123
- # @example Invalid GAN identifiers
124
- # valid_gan_format?("CHESS") # => false (no colon)
125
- # valid_gan_format?("chess:K") # => false (case mismatch)
126
- # valid_gan_format?("CHESS:") # => false (no piece part)
127
- def valid_gan_format?(actor)
128
- return false unless actor.is_a?(String)
129
- return false unless actor.include?(GAN_SEPARATOR)
130
-
131
- parts = actor.split(GAN_SEPARATOR, 2)
132
- return false unless parts.length == 2
133
-
134
- game_part, piece_part = parts
135
-
136
- return false unless valid_game_identifier?(game_part)
137
- return false unless valid_piece_identifier?(piece_part)
138
-
139
- # Case consistency verification between game and piece
140
- game_is_upper = game_part == game_part.upcase
141
- piece_match = piece_part.match(/\A[-+]?([A-Za-z])'?\z/)
142
- return false unless piece_match
143
-
144
- piece_char = piece_match[1]
145
- piece_is_upper = piece_char == piece_char.upcase
146
-
147
- game_is_upper == piece_is_upper
148
- end
149
-
150
- # Validates a game identifier.
151
- #
152
- # Game identifiers must be non-empty strings containing only
153
- # alphabetic characters, either all uppercase or all lowercase.
154
- # Mixed case is not allowed as it breaks the player distinction.
155
- #
156
- # @param game_id [String] Game identifier to validate
157
- #
158
- # @return [Boolean] true if the identifier is valid
159
- #
160
- # @example Valid game identifiers
161
- # valid_game_identifier?("CHESS") # => true
162
- # valid_game_identifier?("shogi") # => true
163
- # valid_game_identifier?("XIANGQI") # => true
164
- #
165
- # @example Invalid game identifiers
166
- # valid_game_identifier?("Chess") # => false (mixed case)
167
- # valid_game_identifier?("") # => false (empty)
168
- # valid_game_identifier?("CHESS1") # => false (contains digit)
169
- def valid_game_identifier?(game_id)
170
- return false unless game_id.is_a?(String)
171
- return false if game_id.empty?
172
-
173
- # Must be either entirely uppercase or entirely lowercase
174
- game_id.match?(/\A[A-Z]+\z/) || game_id.match?(/\A[a-z]+\z/)
175
- end
176
-
177
- # Validates a piece identifier (part after the colon).
178
- #
179
- # Piece identifiers follow the pattern: [optional prefix][letter][optional suffix]
180
- # Where:
181
- # - Optional prefix: + or -
182
- # - Letter: A-Z or a-z (must match game part case)
183
- # - Optional suffix: ' (apostrophe)
184
- #
185
- # @param piece_id [String] Piece identifier to validate
186
- #
187
- # @return [Boolean] true if the identifier is valid
188
- #
189
- # @example Valid piece identifiers
190
- # valid_piece_identifier?("K") # => true
191
- # valid_piece_identifier?("+p") # => true
192
- # valid_piece_identifier?("R'") # => true
193
- # valid_piece_identifier?("-Q'") # => true
194
- #
195
- # @example Invalid piece identifiers
196
- # valid_piece_identifier?("") # => false (empty)
197
- # valid_piece_identifier?("++K") # => false (double prefix)
198
- # valid_piece_identifier?("K''") # => false (double suffix)
199
- def valid_piece_identifier?(piece_id)
200
- return false unless piece_id.is_a?(String)
201
- return false if piece_id.empty?
202
-
203
- # Format: [optional prefix][letter][optional suffix]
204
- piece_id.match?(/\A[-+]?[A-Za-z]'?\z/)
205
- end
206
- end
207
- end
208
- end
@@ -1,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module Ggn
5
- class Ruleset
6
- class Source
7
- class Destination
8
- class Engine
9
- # Represents the result of a valid pseudo-legal move evaluation.
10
- #
11
- # A Transition encapsulates the changes that occur when a move is executed
12
- # on the game board. Since GGN focuses exclusively on board-to-board
13
- # transformations, a Transition only contains board state changes: pieces
14
- # moving, appearing, or disappearing on the board.
15
- #
16
- # @example Basic move (pawn advance)
17
- # transition = Transition.new("e2" => nil, "e4" => "CHESS:P")
18
- # transition.diff # => { "e2" => nil, "e4" => "CHESS:P" }
19
- #
20
- # @example Capture (piece takes enemy piece)
21
- # transition = Transition.new("d4" => nil, "e5" => "CHESS:P")
22
- # transition.diff # => { "d4" => nil, "e5" => "CHESS:P" }
23
- #
24
- # @example Complex move (castling with king and rook)
25
- # transition = Transition.new(
26
- # "e1" => nil, "f1" => "CHESS:R", "g1" => "CHESS:K", "h1" => nil
27
- # )
28
- # transition.diff # => { "e1" => nil, "f1" => "CHESS:R", "g1" => "CHESS:K", "h1" => nil }
29
- #
30
- # @example Promotion (pawn becomes queen)
31
- # transition = Transition.new("e7" => nil, "e8" => "CHESS:Q")
32
- # transition.diff # => { "e7" => nil, "e8" => "CHESS:Q" }
33
- class Transition
34
- # @return [Hash<String, String|nil>] Board state changes after the move.
35
- # Keys are square labels, values are piece identifiers or nil for empty squares.
36
- attr_reader :diff
37
-
38
- # Creates a new Transition with the specified board changes.
39
- #
40
- # @param diff [Hash] Board state changes as keyword arguments.
41
- # Keys should be square labels, values should be piece identifiers or nil.
42
- #
43
- # @example Creating a simple move transition
44
- # Transition.new("e2" => nil, "e4" => "CHESS:P")
45
- #
46
- # @example Creating a capture transition
47
- # Transition.new("d4" => nil, "e5" => "CHESS:P")
48
- #
49
- # @example Creating a complex multi-square transition (castling)
50
- # Transition.new(
51
- # "e1" => nil, # King leaves e1
52
- # "f1" => "CHESS:R", # Rook moves to f1
53
- # "g1" => "CHESS:K", # King moves to g1
54
- # "h1" => nil # Rook leaves h1
55
- # )
56
- #
57
- # @example Creating a promotion transition
58
- # Transition.new("e7" => nil, "e8" => "CHESS:Q")
59
- #
60
- # @example Creating an en passant capture
61
- # Transition.new(
62
- # "d5" => nil, # Attacking pawn leaves d5
63
- # "e5" => nil, # Captured pawn removed from e5
64
- # "e6" => "CHESS:P" # Attacking pawn lands on e6
65
- # )
66
- def initialize(**diff)
67
- @diff = diff
68
-
69
- freeze
70
- end
71
-
72
- # This class remains intentionally simple and rule-agnostic.
73
- # Any interpretation of what constitutes a "capture" or "promotion"
74
- # is left to higher-level game logic, maintaining GGN's neutrality.
75
- end
76
- end
77
- end
78
- end
79
- end
80
- end
81
- end