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.
- checksums.yaml +4 -4
- data/README.md +313 -562
- data/lib/sashite/ggn/ruleset/source/destination/engine.rb +71 -326
- data/lib/sashite/ggn/ruleset/source/destination.rb +33 -85
- data/lib/sashite/ggn/ruleset/source.rb +33 -75
- data/lib/sashite/ggn/ruleset.rb +35 -439
- data/lib/sashite/ggn.rb +196 -324
- data/lib/sashite-ggn.rb +8 -120
- metadata +68 -20
- data/lib/sashite/ggn/move_validator.rb +0 -208
- data/lib/sashite/ggn/ruleset/source/destination/engine/transition.rb +0 -81
- data/lib/sashite/ggn/schema.rb +0 -171
- data/lib/sashite/ggn/validation_error.rb +0 -56
data/lib/sashite-ggn.rb
CHANGED
@@ -1,126 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
#
|
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
|
-
#
|
7
|
+
# Sashité provides a collection of libraries for representing and manipulating
|
8
|
+
# board game concepts according to the Sashité Protocol specifications.
|
109
9
|
#
|
110
|
-
#
|
111
|
-
#
|
112
|
-
#
|
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.
|
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:
|
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.
|
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.
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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/
|
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)
|
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
|