chess_engine_rb 0.1.1

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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +83 -0
  4. data/lib/chess_engine/data_definitions/README.md +10 -0
  5. data/lib/chess_engine/data_definitions/board.rb +192 -0
  6. data/lib/chess_engine/data_definitions/components/castling_rights.rb +48 -0
  7. data/lib/chess_engine/data_definitions/components/persistent_array.rb +114 -0
  8. data/lib/chess_engine/data_definitions/events.rb +174 -0
  9. data/lib/chess_engine/data_definitions/piece.rb +159 -0
  10. data/lib/chess_engine/data_definitions/position.rb +137 -0
  11. data/lib/chess_engine/data_definitions/primitives/castling_data.rb +61 -0
  12. data/lib/chess_engine/data_definitions/primitives/colors.rb +26 -0
  13. data/lib/chess_engine/data_definitions/primitives/core_notation.rb +111 -0
  14. data/lib/chess_engine/data_definitions/primitives/movement.rb +52 -0
  15. data/lib/chess_engine/data_definitions/square.rb +98 -0
  16. data/lib/chess_engine/engine.rb +299 -0
  17. data/lib/chess_engine/errors.rb +35 -0
  18. data/lib/chess_engine/event_handlers/base_event_handler.rb +112 -0
  19. data/lib/chess_engine/event_handlers/castling_event_handler.rb +48 -0
  20. data/lib/chess_engine/event_handlers/en_passant_event_handler.rb +59 -0
  21. data/lib/chess_engine/event_handlers/init.rb +41 -0
  22. data/lib/chess_engine/event_handlers/move_event_handler.rb +144 -0
  23. data/lib/chess_engine/formatters/eran_formatters.rb +71 -0
  24. data/lib/chess_engine/formatters/init.rb +19 -0
  25. data/lib/chess_engine/formatters/validation.rb +54 -0
  26. data/lib/chess_engine/game/history.rb +25 -0
  27. data/lib/chess_engine/game/init.rb +15 -0
  28. data/lib/chess_engine/game/legal_moves_helper.rb +126 -0
  29. data/lib/chess_engine/game/query.rb +168 -0
  30. data/lib/chess_engine/game/state.rb +198 -0
  31. data/lib/chess_engine/parsers/eran_parser.rb +85 -0
  32. data/lib/chess_engine/parsers/identity_parser.rb +18 -0
  33. data/lib/chess_engine/parsers/init.rb +21 -0
  34. data/lib/chess_engine_rb.rb +46 -0
  35. metadata +112 -0
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'immutable'
4
+ require_relative 'query'
5
+ require_relative 'history'
6
+ require_relative '../errors'
7
+ require_relative '../data_definitions/piece'
8
+ require_relative '../data_definitions/square'
9
+ require_relative '../data_definitions/board'
10
+ require_relative '../data_definitions/position'
11
+ require_relative '../data_definitions/events'
12
+ require_relative '../data_definitions/primitives/colors'
13
+ require_relative '../data_definitions/primitives/castling_data'
14
+
15
+ module ChessEngine
16
+ module Game
17
+ # Represents the immutable state of the game at a specific point in time.
18
+ #
19
+ # Holds all the information needed to fully describe the current state of a chess game:
20
+ # the board layout, which player's turn it is, history, castling rights, en passant target, and more.
21
+ #
22
+ # Responsibilities:
23
+ # - Answer queries about the current position (through the `Game::Query` object).
24
+ # - Produce the next `State` by applying a valid event (`#apply_event`).
25
+ #
26
+ # Internal structure:
27
+ # - position: A `Position` object representing the current game position
28
+ # - history: A `Game::History` object representing all that happened since the creation of the originating `State`.
29
+ # - query: A `Game::Query` object that provides a high-level interface for interrogating the state.
30
+ #
31
+ # The design avoids mutable state — each change produces a new `State`, leaving previous states untouched.
32
+ # This makes reasoning about the engine easier and enables features like undo and state comparison.
33
+ class State
34
+ attr_reader :query, :position, :history
35
+
36
+ # The state at the game's start
37
+ def self.start
38
+ State.new(position: Position.start, history: History.start)
39
+ end
40
+
41
+ # Load a new gamestate from position. Suitable for FEN.
42
+ def self.load(position)
43
+ history = History.start.with(start_position: position)
44
+ State.new(position: position, history: history)
45
+ end
46
+
47
+ # Make a new `State` from an existing one.
48
+ # Good for computations requiring forking the state.
49
+ def with(position: @position, history: @history)
50
+ State.new(position: position, history: history)
51
+ end
52
+
53
+ # Low-level initialization, loads all fields. Use with caution.
54
+ def initialize(position: Position.start, history: History.start)
55
+ unless position.is_a?(Position) && history.is_a?(History)
56
+ raise ArgumentError,
57
+ "One or more invalid arguments: #{position}, #{history}"
58
+ end
59
+
60
+ @position = position
61
+ @history = history
62
+ @query = Query.new(@position, @history)
63
+ end
64
+
65
+ # Process an event to produce the next `State`
66
+ # Assumes the event is valid and complete.
67
+ def apply_event(event)
68
+ raise ArgumentError unless event.is_a?(Events::BaseEvent)
69
+
70
+ State.new(
71
+ position: advance_position(event),
72
+ history: advance_history(event)
73
+ )
74
+ end
75
+
76
+ private
77
+
78
+ def advance_history(event)
79
+ signatures = history.position_signatures
80
+ signature_count = signatures.fetch(@position.signature, 0)
81
+ new_signatures = signatures.put(@position.signature, signature_count + 1)
82
+
83
+ history.with(moves: history.moves.add(event), position_signatures: new_signatures)
84
+ end
85
+
86
+ def advance_position(event)
87
+ Position.new(
88
+ board: advance_board(event),
89
+ current_color: @position.other_color,
90
+ en_passant_target: compute_en_passant(event),
91
+ castling_rights: compute_castling_rights(event),
92
+ halfmove_clock: compute_halfmove_clock(event),
93
+ fullmove_number: compute_fullmove_number
94
+ )
95
+ rescue InvalidEventError; raise
96
+ rescue InvariantViolationError => e
97
+ raise InvalidEventError,
98
+ "Invariant violation during event application: #{e.class} - #{e.message}\nEvent: #{event}"
99
+ end
100
+
101
+ def advance_board(event)
102
+ board = @position.board
103
+ case event
104
+ in MovePieceEvent
105
+ advance_with_move_piece_event(board, event)
106
+ in EnPassantEvent => e
107
+ board.remove(e.captured.square).move(e.from, e.to)
108
+ in CastlingEvent => e
109
+ board.move(e.king_from, e.king_to).move(e.rook_from, e.rook_to)
110
+ else
111
+ raise InvalidEventError, "Unhandled event type: #{event.class}"
112
+ end
113
+ end
114
+
115
+ def advance_with_move_piece_event(board, event)
116
+ final_piece = event.promote_to ? Piece.new(event.piece.color, event.promote_to) : event.piece
117
+
118
+ # capture if applicable
119
+ board = board.remove(event.captured.square) if event.captured
120
+ # move the piece
121
+ board.remove(event.from).insert(final_piece, event.to)
122
+ end
123
+
124
+ def compute_en_passant(event)
125
+ # Get the last move and ensure it was a pawn moving two steps forward
126
+ return unless event.is_a?(MovePieceEvent) && event.piece.type == :pawn &&
127
+ event.from.distance(event.to) == [0, 2]
128
+
129
+ # Return the square passed over
130
+ sq = Square[event.from.file, (event.from.rank + event.to.rank) / 2]
131
+ raise InvariantViolationError, "Invalid en passant target: #{sq}" unless sq.valid?
132
+
133
+ sq
134
+ end
135
+
136
+ def compute_castling_rights(event)
137
+ current_color_sides = castling_sides_for_current_color(event)
138
+ other_color_sides = castling_sides_for_other_color(event)
139
+ @position.castling_rights.with(@position.current_color => current_color_sides,
140
+ @position.other_color => other_color_sides)
141
+ end
142
+
143
+ # Helpers for computing castling rights
144
+ def castling_sides_for_current_color(event)
145
+ color = @position.current_color
146
+ sides = @position.castling_rights.sides color
147
+
148
+ case event
149
+ in MovePieceEvent => e
150
+ if e.piece.type == :king
151
+ sides.with(kingside: false, queenside: false)
152
+ elsif e.from == CastlingData.rook_from(color, :kingside)
153
+ sides.with(kingside: false)
154
+ elsif e.from == CastlingData.rook_from(color, :queenside)
155
+ sides.with(queenside: false)
156
+ else
157
+ sides
158
+ end
159
+ in CastlingEvent
160
+ sides.with(kingside: false, queenside: false)
161
+ else
162
+ sides
163
+ end
164
+ end
165
+
166
+ def castling_sides_for_other_color(event)
167
+ color = @position.other_color
168
+ sides = @position.castling_rights.sides color
169
+
170
+ return sides unless event.is_a?(MovePieceEvent)
171
+
172
+ captured = event.captured
173
+ return sides if captured.nil? || captured.piece.type != :rook
174
+
175
+ case captured.square
176
+ when CastlingData.rook_from(color, :kingside)
177
+ sides.with(kingside: false)
178
+ when CastlingData.rook_from(color, :queenside)
179
+ sides.with(queenside: false)
180
+ else
181
+ sides
182
+ end
183
+ end
184
+
185
+ def compute_halfmove_clock(event)
186
+ reset_clock = (event.is_a?(MovePieceEvent) && (event.piece.type == :pawn || event.captured)) ||
187
+ event.is_a?(EnPassantEvent)
188
+
189
+ reset_clock ? 0 : @position.halfmove_clock + 1
190
+ end
191
+
192
+ def compute_fullmove_number
193
+ increment = @position.current_color == :black ? 1 : 0
194
+ @position.fullmove_number + increment
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../data_definitions/events'
4
+ require_relative '../data_definitions/primitives/core_notation'
5
+
6
+ module ChessEngine
7
+ module Parsers
8
+ # A parser for ERAN, a notation made specifically for the engine.
9
+ # See the docs for more details.
10
+ class ERANParser
11
+ SQR = /[a-h][1-8]/i
12
+ MOVEMENT = /(?<from>#{SQR})((?<silent>-)|(?<capture>x))(?<to>#{SQR})/i
13
+
14
+ PIECE = /pawn|rook|bishop|knight|queen|king|[prbnqk]/i
15
+ PROMOTION = /queen|rook|bishop|knight|[qrbn]/i
16
+
17
+ REGULAR_MOVE = /
18
+ (?<piece>#{PIECE})\s+
19
+ (?<movement>#{MOVEMENT})
20
+ (?:\s+(?:->|>)(?<promotion>#{PROMOTION}))?
21
+ /x
22
+ SPECIAL_MOVE = /
23
+ (?:
24
+ (?<en_passant>ep|en-passant) |
25
+ (?<kingside>ck|castling-kingside) |
26
+ (?<queenside>cq|castling-queenside)
27
+ )/ix
28
+
29
+ MOVE = /
30
+ \A\s*
31
+ (?:
32
+ (?<regular>#{REGULAR_MOVE}) |
33
+ (?<special>#{SPECIAL_MOVE})
34
+ )
35
+ \s*\Z
36
+ /ixo
37
+
38
+ class << self
39
+ def call(notation, _query = nil)
40
+ match = MOVE.match(notation)
41
+ return unless match
42
+
43
+ return parse_special_move(match) if match[:special]
44
+
45
+ parse_regular_move(match)
46
+ end
47
+
48
+ private
49
+
50
+ def parse_special_move(match)
51
+ if match[:en_passant]
52
+ EnPassantEvent[nil, nil, nil]
53
+ elsif match[:kingside]
54
+ CastlingEvent[nil, :kingside]
55
+ elsif match[:queenside]
56
+ CastlingEvent[nil, :queenside]
57
+ end
58
+ end
59
+
60
+ def parse_regular_move(match)
61
+ piece = Piece[nil, str_to_piece_type(match[:piece])]
62
+ from = CoreNotation.str_to_square(match[:from].downcase)
63
+ to = CoreNotation.str_to_square(match[:to].downcase)
64
+
65
+ event = MovePieceEvent[piece, from, to]
66
+ event = event.capture if match[:capture]
67
+ if (promotion = match[:promotion])
68
+ event = event.promote(str_to_piece_type(promotion))
69
+ end
70
+
71
+ event
72
+ end
73
+
74
+ def str_to_piece_type(str)
75
+ str = str.downcase
76
+ if Piece::TYPES.include?(str.to_sym)
77
+ str.to_sym
78
+ else
79
+ CoreNotation.str_to_piece(str).type
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../data_definitions/events'
4
+
5
+ module ChessEngine
6
+ module Parsers
7
+ # A parser that returns the input event unchanged.
8
+ # Used primarily for testing, as it bypasses actual notation parsing
9
+ # and assumes the input is already valid.
10
+ class IdentityParser
11
+ def self.call(notation, _game_query)
12
+ return nil unless notation.is_a?(Events::BaseEvent)
13
+
14
+ notation
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'eran_parser'
4
+ require_relative 'identity_parser'
5
+
6
+ module ChessEngine
7
+ # Namespace for all chess notation parsers.
8
+ #
9
+ # Parsers convert notation input into syntactically correct `Events::BaseEvent` objects.
10
+ # The output must accurately reflect the provided notation, but need not ensure
11
+ # that the move is *legally valid* within the game — legality is verified later
12
+ # by the engine.
13
+ #
14
+ # Each parser should implement `.call(notation, game_query)`, returning a syntactically valid event
15
+ # for the given notation, or `nil` if the notation cannot be parsed.
16
+ #
17
+ # A syntactically correct event is a `Events::BaseEvent` for which every provided field is of the expected type,
18
+ # or `nil`.
19
+ module Parsers
20
+ end
21
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ lib_dir = Pathname.new(__FILE__).dirname.expand_path
5
+
6
+ $LOAD_PATH.unshift(lib_dir.to_s) unless $LOAD_PATH.include?(lib_dir.to_s)
7
+
8
+ # Main namespace for the engine.
9
+ # Contains all core logic, data definitions, parsers, and more.
10
+ module ChessEngine
11
+ # --- Main components ---
12
+ # Main class
13
+ autoload :Engine, 'chess_engine/engine'
14
+
15
+ # components
16
+ autoload :EventHandlers, 'chess_engine/event_handlers/init'
17
+ autoload :Game, 'chess_engine/game/init'
18
+ autoload :Parsers, 'chess_engine/parsers/init'
19
+ autoload :Formatters, 'chess_engine/formatters/init'
20
+
21
+ # --- Data Definitions ---
22
+ autoload :Piece, 'chess_engine/data_definitions/piece'
23
+ autoload :Square, 'chess_engine/data_definitions/square'
24
+ autoload :Board, 'chess_engine/data_definitions/board'
25
+
26
+ autoload :Position, 'chess_engine/data_definitions/position'
27
+ autoload :CastlingRights, 'chess_engine/data_definitions/components/castling_rights'
28
+
29
+ # Events
30
+ autoload :Events, 'chess_engine/data_definitions/events'
31
+ autoload :MovePieceEvent, 'chess_engine/data_definitions/events'
32
+ autoload :CastlingEvent, 'chess_engine/data_definitions/events'
33
+ autoload :EnPassantEvent, 'chess_engine/data_definitions/events'
34
+
35
+ # primitive data definitions
36
+ autoload :CastlingData, 'chess_engine/data_definitions/primitives/castling_data'
37
+ autoload :Colors, 'chess_engine/data_definitions/primitives/colors'
38
+ autoload :CoreNotation, 'chess_engine/data_definitions/primitives/core_notation'
39
+
40
+ # --- Errors ---
41
+ autoload :InvariantViolationError, 'chess_engine/errors'
42
+ autoload :InvalidEventError, 'chess_engine/errors'
43
+ autoload :BoardManipulationError, 'chess_engine/errors'
44
+ autoload :InvalidSquareError, 'chess_engine/errors'
45
+ autoload :InternalEngineError, 'chess_engine/errors'
46
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chess_engine_rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - nadi726
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-12-08 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: immutable-ruby
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.2.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.2.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: wholeable
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.4'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.4'
40
+ description: |+
41
+ A modular, deterministic chess engine built around immutable objects.
42
+ Cleanly expresses chess concepts in code and designed for easy integration with any UI.
43
+
44
+ > ⚠️ Note: This is not a competitive chess engine like Stockfish.
45
+ While AI features could be added in the future, the core purpose of this project is to provide a ruby gem for cleanly representing chess in code.
46
+
47
+ email:
48
+ - 16650084+nadi726@users.noreply.github.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - LICENSE
54
+ - README.md
55
+ - lib/chess_engine/data_definitions/README.md
56
+ - lib/chess_engine/data_definitions/board.rb
57
+ - lib/chess_engine/data_definitions/components/castling_rights.rb
58
+ - lib/chess_engine/data_definitions/components/persistent_array.rb
59
+ - lib/chess_engine/data_definitions/events.rb
60
+ - lib/chess_engine/data_definitions/piece.rb
61
+ - lib/chess_engine/data_definitions/position.rb
62
+ - lib/chess_engine/data_definitions/primitives/castling_data.rb
63
+ - lib/chess_engine/data_definitions/primitives/colors.rb
64
+ - lib/chess_engine/data_definitions/primitives/core_notation.rb
65
+ - lib/chess_engine/data_definitions/primitives/movement.rb
66
+ - lib/chess_engine/data_definitions/square.rb
67
+ - lib/chess_engine/engine.rb
68
+ - lib/chess_engine/errors.rb
69
+ - lib/chess_engine/event_handlers/base_event_handler.rb
70
+ - lib/chess_engine/event_handlers/castling_event_handler.rb
71
+ - lib/chess_engine/event_handlers/en_passant_event_handler.rb
72
+ - lib/chess_engine/event_handlers/init.rb
73
+ - lib/chess_engine/event_handlers/move_event_handler.rb
74
+ - lib/chess_engine/formatters/eran_formatters.rb
75
+ - lib/chess_engine/formatters/init.rb
76
+ - lib/chess_engine/formatters/validation.rb
77
+ - lib/chess_engine/game/history.rb
78
+ - lib/chess_engine/game/init.rb
79
+ - lib/chess_engine/game/legal_moves_helper.rb
80
+ - lib/chess_engine/game/query.rb
81
+ - lib/chess_engine/game/state.rb
82
+ - lib/chess_engine/parsers/eran_parser.rb
83
+ - lib/chess_engine/parsers/identity_parser.rb
84
+ - lib/chess_engine/parsers/init.rb
85
+ - lib/chess_engine_rb.rb
86
+ homepage: https://github.com/nadi726/ruby-chess-engine
87
+ licenses:
88
+ - MIT
89
+ metadata:
90
+ homepage_uri: https://github.com/nadi726/ruby-chess-engine
91
+ source_code_uri: https://github.com/nadi726/ruby-chess-engine
92
+ rubygems_mfa_required: 'true'
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 3.4.1
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.6.2
108
+ specification_version: 4
109
+ summary: A UI-agnostic, event-driven, mostly-immutable chess engine written in pure
110
+ Ruby.
111
+ test_files: []
112
+ ...