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,299 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Core components
4
+ require_relative 'data_definitions/position'
5
+ require_relative 'game/init'
6
+ require_relative 'event_handlers/init'
7
+ require_relative 'parsers/init'
8
+
9
+ module ChessEngine
10
+ # The `Engine` is the central coordinator and interpreter of the chess game.
11
+ #
12
+ # It interprets structured input (chess notation) and translates it into concrete
13
+ # state transitions, producing `GameUpdate` objects and notifying registered listeners.
14
+ #
15
+ # The engine is responsible for a single game session at a time.
16
+ # On initialization, you must choose the notation parser the engine will use (or use the default one).
17
+ #
18
+ # After initializing an engine, you must begin a session either by:
19
+ # - starting a new game with `#new_game`.
20
+ # - loading a game from existing state with `#from_fen` or `#load_game_state`.
21
+ #
22
+ # The game can be exported at any point with `#to_fen`.
23
+ #
24
+ # Clients advance the engine through two mechanisms:
25
+ # - By playing a turn, via the `#play_turn` method.
26
+ # - By attempting a game-ending action: offering, accepting, claiming a draw, or by resigning.
27
+ # Those actions are triggered by `#offer_draw`, `#accept_draw`, `#claim_draw` and `#resign` respectively.
28
+ #
29
+ # To observe game progress, clients can:
30
+ # - Register as listeners and implement `#on_game_update(game_update)` to get turn-by-turn updates,
31
+ # as well as error messages(a `GameUpdate.failure` object) for an invalid client operation.
32
+ # - call the `#last_update` method to get the last game update status.
33
+ class Engine # rubocop:disable Metrics/ClassLength
34
+ DEFAULT_PARSER = Parsers::ERANParser
35
+
36
+ def initialize(default_parser: DEFAULT_PARSER)
37
+ raise ArgumentError, "Not a valid `Parser`: #{default_parser}" unless default_parser.respond_to?(:call)
38
+
39
+ @listeners = []
40
+ @default_parser = default_parser
41
+ end
42
+
43
+ def add_listener(listener)
44
+ @listeners << listener unless @listeners.include?(listener)
45
+ end
46
+
47
+ def remove_listener(listener) = @listeners.delete(listener)
48
+
49
+ def default_parser(parser)
50
+ raise ArgumentError, "Not a valid `Parser`: #{default_parser}" unless parser.respond_to?(:call)
51
+
52
+ @default_parser = parser
53
+ end
54
+
55
+ # Starts a new game. Resets the game's state and starts a new session.
56
+ def new_game
57
+ load_game_state(Game::State.start)
58
+ end
59
+
60
+ # Starts a new game session with the given state.
61
+ # A lower-level operation - use with caution.
62
+ def load_game_state(state, offered_draw: nil)
63
+ update_game(
64
+ state: state,
65
+ endgame_status: detect_endgame_status(state.query),
66
+ offered_draw: offered_draw,
67
+ event: nil,
68
+ session: @session.nil? ? SessionInfo.started(0) : @session.next
69
+ )
70
+ end
71
+
72
+ def from_fen(fen_str)
73
+ position = Position.from_fen(fen_str)
74
+ load_game_state(Game::State.load(position))
75
+ end
76
+
77
+ def to_fen
78
+ @state.position.to_fen
79
+ end
80
+
81
+ # The last update that was made.
82
+ # Useful for directly retrieving the most recent `GameUpdate`,
83
+ # allowing inspection of game state changes without engaging with the listener model.
84
+ attr_reader :last_update
85
+ alias status last_update
86
+
87
+ # Plays one side’s turn.
88
+ #
89
+ # Parses the given notation, interprets the corresponding event,
90
+ # updates the engine’s state, and notifies listeners with a `GameUpdate`.
91
+ #
92
+ # Returns a corresponding `GameUpdate`.
93
+ def play_turn(notation, parser = @default_parser)
94
+ result = interpret_turn(notation, parser)
95
+ result.success? ? update_game(**result.success_attributes) : notify_listeners(result)
96
+ result
97
+ end
98
+
99
+ # Registers a draw offer from the current player.
100
+ # Although FIDE technically allows offering a draw at any point,
101
+ # the engine enforces that only the current player may do so,
102
+ # which results in a simpler interface and therefore makes more sense from an engine perspective.
103
+ def offer_draw
104
+ failure = detect_general_failure || (GameUpdate.failure(:draw_offer_not_allowed) if @offered_draw)
105
+
106
+ failure ? notify_listeners(failure) : update_game(offered_draw: query.position.current_color)
107
+ end
108
+
109
+ # Accepts a pending draw offer from the opponent and ends the game.
110
+ # Does nothing if no offer exists.
111
+ def accept_draw
112
+ invalid_offer = @offered_draw.nil? || @offered_draw == query&.position&.current_color
113
+ failure = detect_general_failure || (GameUpdate.failure(:draw_accept_not_allowed) if invalid_offer)
114
+
115
+ failure ? notify_listeners(failure) : update_game(endgame_status: GameOutcome[:draw, :agreement])
116
+ end
117
+
118
+ # Claims a draw by rule (50-move rule or repetition) if eligible.
119
+ # Does nothing if the claim is invalid.
120
+ def claim_draw # rubocop:disable Metrics/CyclomaticComplexity
121
+ eligible_cause = if query&.threefold_repetition? then :threefold_repetition
122
+ elsif query&.fifty_move_rule? then :fifty_move
123
+ end
124
+ failure = detect_general_failure || (GameUpdate.failure(:draw_claim_not_allowed) unless eligible_cause)
125
+
126
+ failure ? notify_listeners(failure) : update_game(endgame_status: GameOutcome[:draw, eligible_cause])
127
+ end
128
+
129
+ # Resign and end the game immediately.
130
+ def resign
131
+ failure = detect_general_failure
132
+ winner = query&.position&.other_color # For non-failure
133
+
134
+ failure ? notify_listeners(failure) : update_game(endgame_status: GameOutcome[winner, :resignation])
135
+ end
136
+
137
+ private
138
+
139
+ # Interprets a move notation through all internal processing stages.
140
+ # If valid, advances the engine state; Returns a `GameUpdate`.
141
+ def interpret_turn(notation, parser)
142
+ failure = detect_general_failure
143
+ return failure unless failure.nil?
144
+
145
+ event = parser.call(notation, @state.query)
146
+ return GameUpdate.failure(:invalid_notation) unless event
147
+
148
+ interpret_event(event)
149
+ rescue InvariantViolationError => e
150
+ raise InternalEngineError, "The engine encountered a problem: #{e}"
151
+ end
152
+
153
+ # Executes a given event.
154
+ #
155
+ # On success:
156
+ # - Applies event to the current `Game::State`
157
+ # - Returns a `GameUpdate.success` with updated fields
158
+ #
159
+ # On failure:
160
+ # - Returns a `GameUpdate.failure(:invalid_event)`
161
+ def interpret_event(event)
162
+ result = EventHandlers.handle(event, @state.query)
163
+ return GameUpdate.failure(:invalid_event) if result.failure?
164
+
165
+ state = @state.apply_event(result.event)
166
+ GameUpdate.success(
167
+ event: result.event,
168
+ state: state,
169
+ endgame_status: detect_endgame_status(state.query),
170
+ # Clear draw offer if the opponent just moved without accepting
171
+ offered_draw: state.position.current_color == @offered_draw ? nil : @offered_draw,
172
+ session: @session.current
173
+ )
174
+ end
175
+
176
+ # Updates the engine with the provided fields:
177
+ # - updates the specified subset of instance variables related to current game session
178
+ # - notifies listeners of the update and returns it
179
+ def update_game(state: :not_provided, endgame_status: :not_provided, offered_draw: :not_provided,
180
+ session: :not_provided, event: :not_provided)
181
+ @state = state unless state == :not_provided
182
+ @endgame_status = endgame_status unless endgame_status == :not_provided
183
+ @offered_draw = offered_draw unless offered_draw == :not_provided
184
+ @session = session unless session == :not_provided
185
+ @event = event unless event == :not_provided
186
+ @last_update = GameUpdate.success(event: @event, state: @state, endgame_status: @endgame_status,
187
+ offered_draw: @offered_draw, session: @session)
188
+ notify_listeners(@last_update)
189
+ @last_update
190
+ end
191
+
192
+ def notify_listeners(game_update)
193
+ @listeners.each do |listener|
194
+ listener.on_game_update(game_update)
195
+ end
196
+ game_update
197
+ end
198
+
199
+ # Checks the given state for checkmate or automatic draw conditions.
200
+ # The provided `Game::Query` should reflect the latest position after a move,
201
+ # so this method always returns the endgame status resulting from the most recent update.
202
+ # Returns a `GameOutcome` object if the game has ended, otherwise returns nil
203
+ def detect_endgame_status(query)
204
+ return GameOutcome[query.position.other_color, :checkmate] if query.in_checkmate?
205
+
206
+ cause = if query.stalemate? then :stalemate
207
+ elsif query.insufficient_material? then :insufficient_material
208
+ elsif query.fivefold_repetition? then :fivefold_repetition
209
+ end
210
+
211
+ return nil if cause.nil?
212
+
213
+ GameOutcome[:draw, cause]
214
+ end
215
+
216
+ # General detection for invalid client action
217
+ def detect_general_failure
218
+ return GameUpdate.failure(:no_ongoing_session) unless @session
219
+ return GameUpdate.failure(:game_already_ended) if @endgame_status
220
+
221
+ nil
222
+ end
223
+
224
+ # Convenience accessor
225
+ def query = @state&.query
226
+ end
227
+
228
+ class Engine
229
+ # Represents the outcome of a change in the state of the game, like playing a turn or accepting a draw.
230
+ #
231
+ # On success, it contains:
232
+ # - The event that describes what happened.
233
+ # - The full `Game::State` and `Game::Query` for inspection.
234
+ # - The current endgame status (if any).
235
+ # - The current draw offer status.
236
+ # - Metadata about the current game session.
237
+ #
238
+ # On failure, contains only `error`, which is one of:
239
+ # - `:invalid_notation`
240
+ # - `:invalid_event`
241
+ # - `:game_already_ended`
242
+ # - `:no_ongoing_session`
243
+ # - `:draw_offer_not_allowed`
244
+ # - `:draw_accept_not_allowed`
245
+ # - `:draw_claim_not_allowed`
246
+ #
247
+ # Note: `error` may later be replaced with a structured object to support
248
+ # more detailed error handling.
249
+ #
250
+ # Typically, clients should rely on the event sequence and status fields;
251
+ # direct access to `Game::Query` is for specialized use cases.
252
+ GameUpdate = Data.define(:event, :state, :endgame_status, :offered_draw, :session, :error) do
253
+ def success? = error.nil?
254
+ def failure? = !success?
255
+
256
+ def self.success(event:, state:, endgame_status:, offered_draw:, session:)
257
+ new(event, state, endgame_status, offered_draw, session, nil)
258
+ end
259
+
260
+ def self.failure(error)
261
+ new(nil, nil, nil, nil, nil, error)
262
+ end
263
+
264
+ # A successful result has no `error` field
265
+ def success_attributes
266
+ to_h.except(:error)
267
+ end
268
+
269
+ # A failed result has only an `error` field
270
+ def failure_attributes
271
+ { error: error }
272
+ end
273
+
274
+ def game_ended? = !endgame_status.nil?
275
+ def in_check? = game_query.in_check?
276
+ def can_draw? = game_query.can_draw?
277
+
278
+ def game_query = state.query
279
+ def position = state.position
280
+ def board = position.board
281
+ def current_color = position.current_color
282
+
283
+ private_class_method :new # enforces use of factories
284
+ end
285
+
286
+ # Metadata about the current game session
287
+ SessionInfo = Data.define(:id, :new?) do
288
+ def self.ongoing(id) = new(id, false)
289
+ def self.started(id) = new(id, true)
290
+ def next = self.class.started(id + 1)
291
+ def current = self.class.ongoing(id)
292
+ end
293
+
294
+ # winner is one of: :white, :black, :draw
295
+ # cause is one of: :checkmate, :resignation, :agreement, :stalemate, :insufficient_material, :fivefold_repetition,
296
+ # :threefold_repetition, :fifty_move
297
+ GameOutcome = Data.define(:winner, :cause)
298
+ end
299
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChessEngine
4
+ # Those are errors that should be used internally by `Engine` and its components when something goes wrong.
5
+ #
6
+ # Not to be confused with errors that are expected as part of engine-client interaction,
7
+ # which are a regular part of the control flow, and returned to the listeners in the form of `GameUpdate.error`.
8
+ module Errors
9
+ # A base class for the specific invariant violations below.
10
+ # Signifies that an invariant inside of one of the engine's components, such as `Game::State`, has been violated.
11
+ class InvariantViolationError < StandardError; end
12
+
13
+ # Raised when `Game::State` receives an invalid event.
14
+ class InvalidEventError < InvariantViolationError; end
15
+
16
+ # Raised when attempting illegal manipulations on the `Board`.
17
+ # (e.g., removing a piece from an empty square)
18
+ class BoardManipulationError < InvariantViolationError; end
19
+
20
+ # Raised when attempting to use an invalid `Square`.
21
+ # Note that squares can be created invalidly by design, but using them is forbidden.
22
+ class InvalidSquareError < InvariantViolationError; end
23
+
24
+ # Signals to the client that the engine malfunctioned internally.
25
+ # While the other errors are used inside the engine for communication,
26
+ # this is the single error that clients should see.
27
+ class InternalError < StandardError; end
28
+ end
29
+
30
+ InvariantViolationError = Errors::InvariantViolationError
31
+ InvalidEventError = Errors::InvalidEventError
32
+ BoardManipulationError = Errors::BoardManipulationError
33
+ InvalidSquareError = Errors::InvalidSquareError
34
+ InternalEngineError = Errors::InternalError
35
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../data_definitions/events'
4
+
5
+ module ChessEngine
6
+ module EventHandlers
7
+ # Base class for all event handlers.
8
+ #
9
+ # Each handler validates an event against the current game state.
10
+ # It either produces a fully resolved event (suitable for application to the game state), or returns an error.
11
+ #
12
+ # Handlers are implemented as classes for convenience (shared helpers, instance context),
13
+ # but their public API is procedural:
14
+ # `.call(query, event)` runs the handler and returns an `EventResult`.
15
+ #
16
+ # Subclasses must implement `#resolve`.
17
+ class BaseEventHandler
18
+ attr_reader :query, :event
19
+
20
+ def initialize(query, event)
21
+ @query = query
22
+ @event = event
23
+ end
24
+
25
+ # Primary entry point.
26
+
27
+ def self.call(query, event)
28
+ new(query, event).call
29
+ end
30
+
31
+ # Validates and completes the event, returning an `EventResult`.
32
+ # Clients should use the class-level `.call` instead.
33
+ def call
34
+ result = resolve
35
+ return result if result.failure?
36
+
37
+ post_process(result.event)
38
+ end
39
+
40
+ private
41
+
42
+ # To be implemented by subclasses.
43
+ # Validates and resolves the given event into a full, valid event, suitable for `Game::State` application.
44
+ # Returns an `EventResult` with either the finalized event or an error.
45
+ def resolve
46
+ raise NotImplementedError
47
+ end
48
+
49
+ # Common post-processing logic, applied to all valid results.
50
+ # (e.g., flagging check or checkmate events, enforcing turn-based constraints, etc.)
51
+ def post_process(event)
52
+ return failure if next_turn_in_check?(event)
53
+
54
+ success(event)
55
+ end
56
+
57
+ def next_turn_in_check?(event)
58
+ new_query = query.state.apply_event(event).query
59
+ new_query.in_check?(position.current_color)
60
+ end
61
+
62
+ ### Helpers for subclasses
63
+
64
+ # Runs a series of resolution steps in order.
65
+ # Stops when out of steps, or when result doesn't match the specified condition.
66
+ # By default, the condition is for `result` to be successful.
67
+ # You can set custom stop conditions for specific steps using `stop_conditions`.
68
+ #
69
+ # `steps`: a series of method names on `self` to execute. Each step takes the event and returns an `EventResult`.
70
+ # `stop_conditions`: a hash where each key is a step and the value is a condition lambda.
71
+ def run_resolution_pipeline(*steps, **stop_conditions)
72
+ default_condition = lambda(&:success?)
73
+ result = success(event)
74
+ steps.each do |step|
75
+ result = send(step, result.event)
76
+ condition = stop_conditions.fetch(step, default_condition)
77
+ return result unless condition.call(result)
78
+ end
79
+ result
80
+ end
81
+
82
+ def success(event)
83
+ EventResult.success(event)
84
+ end
85
+
86
+ def failure(msg = '')
87
+ msg = "Message: #{msg}" unless msg.empty?
88
+ EventResult.failure("Invalid result for #{event.inspect}. #{msg}")
89
+ end
90
+
91
+ # Useful accessors
92
+ def board = @query.board
93
+ def position = query.position
94
+ def current_color = position.current_color
95
+ def other_color = position.other_color
96
+ end
97
+
98
+ # Represents the outcome of processing a chess event.
99
+ #
100
+ # - On success: contains the finalized event, and `error` is nil.
101
+ # - On failure: contains an error message (`error`), and `event` is nil.
102
+ EventResult = Data.define(:event, :error) do
103
+ def success? = error.nil?
104
+ def failure? = !success?
105
+
106
+ def self.success(event) = new(event, nil)
107
+ def self.failure(error) = new(nil, error)
108
+
109
+ private_class_method :new # enforces use of factories
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_event_handler'
4
+ require_relative '../data_definitions/events'
5
+ require_relative '../data_definitions/primitives/colors'
6
+ require_relative '../data_definitions/primitives/castling_data'
7
+
8
+ module ChessEngine
9
+ module EventHandlers
10
+ # Event handler for `CastlingEvent`.
11
+ class CastlingEventHandler < BaseEventHandler
12
+ private
13
+
14
+ def resolve
15
+ return failure("#{event} is not a CastlingEvent") unless event.is_a?(CastlingEvent)
16
+
17
+ run_resolution_pipeline(:resolve_color, :resolve_side)
18
+ end
19
+
20
+ def resolve_color(event)
21
+ return failure("Not a color: #{event.color}") unless event.color.nil? || Colors.valid?(event.color)
22
+ return failure("Unexpected color: #{event.color} (expected #{current_color})") if event.color == other_color
23
+
24
+ success(event.with(color: current_color))
25
+ end
26
+
27
+ def resolve_side(event)
28
+ return failure("Not a valid side: #{event.side}") unless CastlingData::SIDES.include?(event.side)
29
+
30
+ sides = position.castling_rights.sides(event.color)
31
+ return failure("No rights for side #{event.side}") unless sides.side?(event.side)
32
+
33
+ king_is_attacked = CastlingData.king_path(event.color, event.side).any? do |sq|
34
+ query.square_attacked?(sq, other_color)
35
+ end
36
+ return failure('King is under attack somewhere on the path') if king_is_attacked
37
+
38
+ path_is_clear = CastlingData.intermediate_squares(event.color, event.side).all? do |sq|
39
+ board.get(sq).nil?
40
+ end
41
+
42
+ return failure('Path between king and rook is obstructed') unless path_is_clear
43
+
44
+ success(event)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_event_handler'
4
+ require_relative '../data_definitions/events'
5
+ require_relative '../data_definitions/square'
6
+ require_relative '../data_definitions/primitives/colors'
7
+
8
+ module ChessEngine
9
+ module EventHandlers
10
+ # Event handler for `EnPassantEvent`
11
+ class EnPassantEventHandler < BaseEventHandler
12
+ private
13
+
14
+ def resolve
15
+ return failure("#{event} is not an EnPassantEvent") unless event.is_a?(EnPassantEvent)
16
+ return failure('EnPassant not available') unless en_passant_target
17
+
18
+ run_resolution_pipeline(:resolve_color, :resolve_to, :resolve_from)
19
+ end
20
+
21
+ def resolve_color(event)
22
+ return failure("Not a color: #{event.color}") unless event.color.nil? || Colors.valid?(event.color)
23
+ return failure("Unexpected color: #{event.color} (expected #{current_color})") if event.color == other_color
24
+
25
+ success(event.with(color: current_color))
26
+ end
27
+
28
+ def resolve_to(event)
29
+ return failure(":to is not a Square: #{event.to}") unless event.to.is_a?(Square)
30
+ return failure("Cannot en passant to #{event.to}") unless event.to.matches?(en_passant_target)
31
+
32
+ success(event.with(to: en_passant_target))
33
+ end
34
+
35
+ def resolve_from(event)
36
+ return failure(":from is not a Square: #{event.from}") unless event.from.nil? || event.from.is_a?(Square)
37
+
38
+ filtered_squares = determine_from(event)
39
+ return failure("#{event.from} is not a valid :from square for this move") if filtered_squares.empty?
40
+ if filtered_squares.size > 1
41
+ return failure("Disambiguation failed. :from (#{event.from}) could be either one of: #{filtered_squares.inspect}")
42
+ end
43
+
44
+ success(event.with(from: filtered_squares.first))
45
+ end
46
+
47
+ def determine_from(event)
48
+ rank_offset = event.color == :white ? -1 : 1
49
+ offsets = [[1, rank_offset], [-1, rank_offset]]
50
+ offsets.filter_map do |file_off, rank_off|
51
+ sq = en_passant_target.offset(file_off, rank_off)
52
+ sq if sq.valid? && board.get(sq) == event.piece && (event.from.nil? || event.from.matches?(sq))
53
+ end
54
+ end
55
+
56
+ def en_passant_target = position.en_passant_target
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'move_event_handler'
4
+ require_relative 'en_passant_event_handler'
5
+ require_relative 'castling_event_handler'
6
+ require_relative '../errors'
7
+ require_relative '../data_definitions/events'
8
+
9
+ module ChessEngine
10
+ # Handles chess events by dispatching them to the appropriate event handler class.
11
+ #
12
+ # Event handling workflow:
13
+ # 1. The Engine provides an event representing a move or action as interpreted from user input or UI.
14
+ # 2. The `handle` method selects the correct handler for the event type and invokes it.
15
+ # 3. The handler validates the event against the current `Game::State`, resolves ambiguities, fills in missing data,
16
+ # and returns an `EventResult`:
17
+ # - On success: returns `EventResult.success` with a fully resolved event.
18
+ # - On failure: returns `EventResult.failure` with an error message.
19
+ #
20
+ # Entry point: `handle(event, query)`
21
+ # Raises `InvalidEventError` if no handler exists for the event type.
22
+ module EventHandlers
23
+ # Individual handlers
24
+ HANDLER_MAP = {
25
+ MovePieceEvent => MoveEventHandler,
26
+ EnPassantEvent => EnPassantEventHandler,
27
+ CastlingEvent => CastlingEventHandler
28
+ }.freeze
29
+
30
+ module_function
31
+
32
+ # Get the appropriate event handler and process the event
33
+ def handle(event, query)
34
+ handler_class = HANDLER_MAP.fetch(event.class) do
35
+ raise InvalidEventError, "no handler for #{event}"
36
+ end
37
+
38
+ handler_class.call(query, event)
39
+ end
40
+ end
41
+ end