chess_cli 0.9.2

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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/.config/locale/debug_en.yml +4 -0
  3. data/.config/locale/en.yml +424 -0
  4. data/bin/chess_cli +6 -0
  5. data/lib/chess_cli.rb +11 -0
  6. data/lib/console/console.rb +202 -0
  7. data/lib/console/wait_utils.rb +25 -0
  8. data/lib/console_game/base_game.rb +74 -0
  9. data/lib/console_game/chess/board.rb +110 -0
  10. data/lib/console_game/chess/game.rb +184 -0
  11. data/lib/console_game/chess/input/algebraic_notation.rb +103 -0
  12. data/lib/console_game/chess/input/chess_input.rb +191 -0
  13. data/lib/console_game/chess/input/smith_notation.rb +38 -0
  14. data/lib/console_game/chess/launcher.rb +20 -0
  15. data/lib/console_game/chess/level.rb +276 -0
  16. data/lib/console_game/chess/logics/display.rb +182 -0
  17. data/lib/console_game/chess/logics/endgame_logic.rb +126 -0
  18. data/lib/console_game/chess/logics/logic.rb +137 -0
  19. data/lib/console_game/chess/logics/moves_simulation.rb +75 -0
  20. data/lib/console_game/chess/logics/piece_analysis.rb +76 -0
  21. data/lib/console_game/chess/logics/piece_lookup.rb +93 -0
  22. data/lib/console_game/chess/pieces/bishop.rb +18 -0
  23. data/lib/console_game/chess/pieces/chess_piece.rb +204 -0
  24. data/lib/console_game/chess/pieces/king.rb +200 -0
  25. data/lib/console_game/chess/pieces/knight.rb +46 -0
  26. data/lib/console_game/chess/pieces/pawn.rb +142 -0
  27. data/lib/console_game/chess/pieces/queen.rb +16 -0
  28. data/lib/console_game/chess/pieces/rook.rb +37 -0
  29. data/lib/console_game/chess/player/chess_computer.rb +25 -0
  30. data/lib/console_game/chess/player/chess_player.rb +211 -0
  31. data/lib/console_game/chess/utilities/chess_utils.rb +67 -0
  32. data/lib/console_game/chess/utilities/fen_export.rb +114 -0
  33. data/lib/console_game/chess/utilities/fen_import.rb +196 -0
  34. data/lib/console_game/chess/utilities/load_manager.rb +51 -0
  35. data/lib/console_game/chess/utilities/pgn_export.rb +97 -0
  36. data/lib/console_game/chess/utilities/pgn_utils.rb +134 -0
  37. data/lib/console_game/chess/utilities/player_builder.rb +74 -0
  38. data/lib/console_game/chess/utilities/session_builder.rb +48 -0
  39. data/lib/console_game/chess/version.rb +8 -0
  40. data/lib/console_game/console_menu.rb +68 -0
  41. data/lib/console_game/game_manager.rb +181 -0
  42. data/lib/console_game/input.rb +87 -0
  43. data/lib/console_game/player.rb +100 -0
  44. data/lib/console_game/user_profile.rb +65 -0
  45. data/lib/nimbus_file_utils/nimbus_file_utils.rb +194 -0
  46. data/user_data/.keep +0 -0
  47. data/user_data/dummy_user.json +124 -0
  48. data/user_data/pgn_export/.keep +0 -0
  49. metadata +147 -0
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "chess_piece"
4
+
5
+ module ConsoleGame
6
+ module Chess
7
+ # King is a sub-class of ChessPiece for the game Chess in Console Game
8
+ # @author Ancient Nimbus
9
+ class King < ChessPiece
10
+ attr_accessor :checked
11
+ attr_reader :checked_status, :castle_dirs, :castle_config, :castle_key
12
+
13
+ # @param alg_pos [Symbol] expects board position in Algebraic notation
14
+ # @param side [Symbol] specify unit side :black or :white
15
+ # @param level [Level] Chess::Level object
16
+ def initialize(alg_pos = :e1, side = :white, level: nil)
17
+ super(alg_pos, side, :k, level:)
18
+ @castle_dirs = %i[e w]
19
+ @checked = false
20
+ @checked_status = { checked:, attackers: [] }
21
+ end
22
+
23
+ # Override move
24
+ # Move the chess piece to a new valid location
25
+ # @param new_alg_pos [Symbol] expects board position in Algebraic notation, e.g., :e3
26
+ def move(new_alg_pos)
27
+ old_pos = curr_pos
28
+ disable_castling
29
+ super(new_alg_pos)
30
+
31
+ castling_event(old_pos)
32
+ end
33
+
34
+ # Override query_moves
35
+ # Query and update possible_moves
36
+ def query_moves
37
+ king_to_the_table
38
+ setup_castle
39
+ super
40
+ end
41
+
42
+ # == King specific logics ==
43
+
44
+ # Determine if the King is in a checkmate position
45
+ # @return [Boolean] true if it is a checkmate
46
+ def checkmate?
47
+ return false unless in_check?
48
+
49
+ level.simulate_next_moves(self)
50
+ allies = level.fetch_all(side).select { |ally| ally unless ally.is_a?(King) }
51
+ checked_status[:attackers].each { |attacker| return false if any_saviours?(allies, attacker) }
52
+ crown_has_fallen?
53
+ end
54
+
55
+ private
56
+
57
+ # Add king reference to level
58
+ def king_to_the_table
59
+ return unless level.kings[side].nil?
60
+
61
+ level.kings[side] = self
62
+ end
63
+
64
+ # Determine if the King can perform castling
65
+ def setup_castle
66
+ return unless can_castle?
67
+
68
+ @castle_key ||= side == :white ? %i[K Q] : %i[k q]
69
+ @castle_config ||= castle_key.zip(castle_dirs)
70
+ castle_config.each do |set|
71
+ key, dir = set
72
+ castle_dirs.delete(dir) if level.castling_states[key] == false
73
+ end
74
+ end
75
+
76
+ # Determine if the King can perform castling
77
+ # @return [Boolean]
78
+ def can_castle? = at_start && !checked
79
+
80
+ # Process castling event
81
+ # @param old_pos [Integer] previous position
82
+ def castling_event(old_pos)
83
+ distance = curr_pos - old_pos
84
+ return unless distance.abs == 2
85
+
86
+ rook_pos, @last_move = castling_config
87
+ summon_the_rook(distance.positive? ? "h#{rank}" : "a#{rank}", rook_pos)
88
+
89
+ print_castling_msg
90
+ end
91
+
92
+ # Castling config assignment
93
+ # @return [Array<Integer, String>] returns rook target position and algebraic move
94
+ def castling_config = file == "g" ? [curr_pos - 1, "O-O"] : [curr_pos + 1, "O-O-O"]
95
+
96
+ # Print castling message
97
+ def print_castling_msg = level.board.print_after_cb("level.castle", { info: })
98
+
99
+ # Search the rook
100
+ # @param rook_to_summon [String] expects algebraic notation
101
+ # @param rook_pos [Integer] expects grid position of the target rook
102
+ def summon_the_rook(rook_to_summon, rook_pos) = level.fetch_piece(rook_to_summon, bypass: true).move(rook_pos)
103
+
104
+ # disable castling
105
+ def disable_castling
106
+ return unless at_start
107
+
108
+ castle_key.each { |key| level.castling_states[key] = false }
109
+ end
110
+
111
+ # Override validate_moves
112
+ # Store all valid placement
113
+ # @param turn_data [Array] turn data from level object
114
+ # @param pos [Integer] positional value within a matrix
115
+ def validate_moves(turn_data, pos = curr_pos)
116
+ super(turn_data, pos)
117
+ @possible_moves = possible_moves - level.threats_map[opposite_of(side)]
118
+ end
119
+
120
+ # Override path
121
+ # Path via Pathfinder
122
+ # @param pos [Integer] board positional value
123
+ # @param path [Symbol] compass direction
124
+ # @param range [Symbol, Integer] movement range of the given piece or :max for furthest possible range
125
+ # @return [Array<Integer>]
126
+ def path(pos = 0, path = :e, range: 1)
127
+ range = 2 if can_castle? && castle_dirs.include?(path)
128
+ super(pos, path, range: range)
129
+ end
130
+
131
+ # == Checkmate event flow ==
132
+
133
+ # Determine if there are any saviour
134
+ # @param king_allies [Array<ChessPiece>] expects an array of King's army
135
+ # @param attacker [ChessPiece]
136
+ # @return [Boolean] true if someone can come save the King
137
+ def any_saviours?(king_allies, attacker)
138
+ attack_path = find_attacker_path(attacker)
139
+ saviours = find_saviours(king_allies, attack_path)
140
+ limit_saviours_movements(saviours, attack_path)
141
+ add_saviours(saviours)
142
+ !saviours.empty?
143
+ end
144
+
145
+ # Helper for any_saviours?, find all saviours
146
+ # @param king_allies [Array<ChessPiece>] expects an array of King's army
147
+ # @param attack_path [Array<Integer>]
148
+ # @return [Array<ChessPiece>]
149
+ def find_saviours(king_allies, attack_path)
150
+ king_allies.map { |ally| ally unless (ally.possible_moves & attack_path).empty? }.compact
151
+ end
152
+
153
+ # Helper for any_saviours?, Update and limits saviours path to attacker's path
154
+ # @param saviours [Array<ChessPiece>] expects an array of King's saviours
155
+ # @param attack_path [Array<Integer>]
156
+ def limit_saviours_movements(saviours, attack_path)
157
+ saviours.each do |ally|
158
+ ally.query_moves(attack_path)
159
+ ally.targets.compact.each_value { |pos| level.turn_data[pos].query_moves }
160
+ end
161
+ end
162
+
163
+ # Helper for any_saviours?, add saviours and King to the usable pieces if King still move
164
+ # @param saviours [Array<ChessPiece>] expects an array of King's saviours
165
+ def add_saviours(saviours)
166
+ saviours.push(self) unless possible_moves.empty?
167
+ level.usable_pieces[side] = saviours.map(&:info)
168
+ end
169
+
170
+ # Determine if there are no escape route for the King
171
+ # @return [Boolean] true if King cannot escape
172
+ def crown_has_fallen? = (possible_moves - level.threats_map[opposite_of(side)]).empty?
173
+
174
+ # == Check event flow ==
175
+
176
+ # Determine if the King is checked
177
+ # @return [Boolean] true if the king is checked
178
+ def in_check?
179
+ checked_status.transform_values! { |_| [] }
180
+ self.checked = under_threat?
181
+ checked_event if checked
182
+ checked
183
+ end
184
+
185
+ # Process the checked event
186
+ def checked_event
187
+ find_checking_pieces
188
+ sub = { type: checked_status[:attackers].map(&:name).join(", "), king_side: side.capitalize }
189
+ level.event_msgs << level.board.s("level.check", sub)
190
+ end
191
+
192
+ # Find the pieces that is checking the King
193
+ def find_checking_pieces
194
+ level.fetch_all(opposite_of(side)).select do |piece|
195
+ checked_status[:attackers] << piece if piece.targets.value?(curr_pos)
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "chess_piece"
4
+
5
+ module ConsoleGame
6
+ module Chess
7
+ # Knight is a sub-class of ChessPiece for the game Chess in Console Game
8
+ # @author Ancient Nimbus
9
+ class Knight < ChessPiece
10
+ # @param alg_pos [Symbol] expects board position in Algebraic notation
11
+ # @param side [Symbol] specify unit side :black or :white
12
+ # @param level [Level] Chess::Level object
13
+ def initialize(alg_pos = :b1, side = :white, level: nil) = super(alg_pos, side, :n, level:)
14
+
15
+ private
16
+
17
+ # Override path
18
+ # Knight Movement via pathfinder
19
+ # @param pos [Integer] board positional value
20
+ # @param path [Symbol] compass direction
21
+ # @param range [Symbol, Integer] movement range of the given piece
22
+ # @return [Array<Integer>]
23
+ def path(pos = 0, path = :e, range: 1)
24
+ dirs_keys = DIRECTIONS.keys
25
+ offset_dirs = dirs_keys.rotate(1)
26
+ offset_pos = super(pos, offset_dirs[dirs_keys.index(path)], range: range).last
27
+ return [] if offset_pos.nil?
28
+
29
+ next_pos = super(offset_pos, path, range: range).last
30
+ return [] if next_pos.nil?
31
+
32
+ valid_moves?(pos, next_pos) ? [pos, next_pos] : []
33
+ end
34
+
35
+ # Valid movement for Knight
36
+ # @param pos1 [Integer] original board positional value
37
+ # @param pos2 [Integer] new board positional value
38
+ # @return [Boolean]
39
+ def valid_moves?(pos1, pos2)
40
+ r1, c1, r2, c2 = [pos1, pos2].map { |pos| to_coord(pos) }.flatten
41
+
42
+ ((r1 - r2).abs == 2 && (c1 - c2).abs == 1) || ((r1 - r2).abs == 1 && (c1 - c2).abs == 2)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "chess_piece"
4
+
5
+ module ConsoleGame
6
+ module Chess
7
+ # Pawn is a sub-class of ChessPiece for the game Chess in Console Game
8
+ # @author Ancient Nimbus
9
+ class Pawn < ChessPiece
10
+ attr_reader :at_end
11
+
12
+ # @param alg_pos [Symbol] expects board position in Algebraic notation
13
+ # @param side [Symbol] specify unit side :black or :white
14
+ # @param level [Level] Chess::Level object
15
+ def initialize(alg_pos = :a2, side = :white, level: nil)
16
+ movements = side == :white ? %i[n ne nw] : %i[s se sw]
17
+ super(alg_pos, side, :p, movements:, level:)
18
+ self.at_start = at_rank?(%i[a2 h2], %i[a7 h7])
19
+ at_end?
20
+ end
21
+
22
+ # Override move
23
+ # Move the chess piece to a new valid location
24
+ # @param new_alg_pos [Symbol] expects board position in Algebraic notation, e.g., :e3
25
+ # @param notation [String]
26
+ def move(new_alg_pos, notation = nil)
27
+ old_pos = curr_pos
28
+ super(new_alg_pos)
29
+
30
+ en_passant_reg if (old_pos - curr_pos).abs == 16
31
+ en_passant_capture unless level.en_passant.nil?
32
+
33
+ return unless at_end?
34
+
35
+ notation = notation.nil? ? level.promote_opts : notation
36
+ promote_to(notation.to_sym)
37
+ end
38
+
39
+ private
40
+
41
+ # Handle En Passant detection event
42
+ def en_passant_reg
43
+ tiles_to_query = fetch_adjacent_tiles
44
+
45
+ ghost_pos = side == :white ? curr_pos - 8 : curr_pos + 8
46
+ level.en_passant = [self, ghost_pos] unless tiles_to_query.empty?
47
+ end
48
+
49
+ # Handle En Passant capture event
50
+ def en_passant_capture
51
+ captured_pawn, ghost_pos = level.en_passant
52
+ return unless curr_pos == ghost_pos
53
+
54
+ level.turn_data[captured_pawn.curr_pos] = ""
55
+ level.en_passant = nil
56
+ end
57
+
58
+ # Helper: Return valid adjacent tiles
59
+ def fetch_adjacent_tiles
60
+ tiles_to_query = [curr_pos - 1, curr_pos + 1].map { |pos| level.turn_data.fetch(pos) }
61
+ tiles_to_query.select { |tile| tile.is_a?(Pawn) && tile.rank == rank && tile.side != side }
62
+ end
63
+
64
+ # Perform pawn promotion
65
+ # @param notation [Symbol]
66
+ def promote_to(notation = :q)
67
+ return unless %i[q r b n].include?(notation)
68
+
69
+ class_name = ALG_REF.dig(notation, :class)
70
+ new_unit = Chess.const_get(class_name).new(curr_pos, side, level:)
71
+ level.turn_data[curr_pos] = new_unit
72
+
73
+ last_move_is_promotion(notation)
74
+ print_promo_msg(new_unit)
75
+ end
76
+
77
+ # Print Promotion message
78
+ # @param piece [ChessPiece]
79
+ def print_promo_msg(piece) = level.board.print_after_cb("level.promo", { new_name: piece.name, info: })
80
+
81
+ # Check if the pawn is at the other end of the board
82
+ def at_end? = @at_end = at_rank?(%i[a8 h8], %i[a1 h1])
83
+
84
+ # Parallel query to check if pawn is at a certain rank
85
+ # @param white_range [Array<Symbol>] `[:a2, :h2]`
86
+ # @param black_range [Array<Symbol>] `[:a7, :h7]`
87
+ # @return [Boolean]
88
+ def at_rank?(white_range = %i[a2 h2], black_range = %i[a7 h7])
89
+ w_min, w_max, b_min, b_max = [white_range, black_range].flatten.map { |alg| alg_map[alg] }
90
+ case side
91
+ when :white then return true if [*w_min..w_max].any?(curr_pos)
92
+ when :black then return true if [*b_min..b_max].any?(curr_pos)
93
+ end
94
+ false
95
+ end
96
+
97
+ # Override store_last_move
98
+ # Last move formatted as algebraic notation
99
+ # @param event [Symbol] expects the following key: :move, :capture
100
+ # @param old_pos [Integer] original position
101
+ # @return [String]
102
+ def store_last_move(event = :move, old_pos:) = super.sub("P", "")
103
+
104
+ # Add promotion notation to last move
105
+ # @param notation [Symbol]
106
+ def last_move_is_promotion(notation) = self.last_move += "=#{notation.upcase}"
107
+
108
+ # Override detect_occupied_tiles
109
+ # Detect blocked tile based on the given positions
110
+ # @param path [Symbol]
111
+ # @param turn_data [Array] board data array
112
+ # @param positions [Array] rank array
113
+ # @return [Array]
114
+ def detect_occupied_tiles(path, turn_data, positions)
115
+ new_positions = super(path, turn_data, positions)
116
+ tile_data = new_positions.empty? ? nil : turn_data[new_positions.first]
117
+ if %i[n s].include?(path)
118
+ new_positions = [] if tile_data.is_a?(ChessPiece)
119
+ targets[path] = nil
120
+ elsif tile_data.is_a?(String)
121
+ sights.push(*new_positions)
122
+ new_positions = []
123
+ end
124
+ new_positions.push(en_passant_add)
125
+ end
126
+
127
+ # Helper: add en passant capture position to possible moves
128
+ def en_passant_add = level.en_passant.nil? ? nil : level.en_passant[1]
129
+
130
+ # Override path
131
+ # Path via Pathfinder
132
+ # @param pos [Integer] board positional value
133
+ # @param path [Symbol] compass direction
134
+ # @param range [Symbol, Integer] movement range of the given piece or :max for furthest possible range
135
+ # @return [Array<Integer>]
136
+ def path(pos = 0, path = :e, range: 1)
137
+ range = 2 if at_start && %i[n s].include?(path)
138
+ super(pos, path, range: range)
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "chess_piece"
4
+
5
+ module ConsoleGame
6
+ module Chess
7
+ # Queen is a sub-class of ChessPiece for the game Chess in Console Game
8
+ # @author Ancient Nimbus
9
+ class Queen < ChessPiece
10
+ # @param alg_pos [Symbol] expects board position in Algebraic notation
11
+ # @param side [Symbol] specify unit side :black or :white
12
+ # @param level [Level] Chess::Level object
13
+ def initialize(alg_pos = :d1, side = :white, level: nil) = super(alg_pos, side, :q, range: :max, level:)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "chess_piece"
4
+
5
+ module ConsoleGame
6
+ module Chess
7
+ # Rook is a sub-class of ChessPiece for the game Chess in Console Game
8
+ # @author Ancient Nimbus
9
+ class Rook < ChessPiece
10
+ # @param alg_pos [Symbol] expects board position in Algebraic notation
11
+ # @param side [Symbol] specify unit side :black or :white
12
+ # @param level [Level] Chess::Level object
13
+ def initialize(alg_pos = :a1, side = :white, level: nil)
14
+ super(alg_pos, side, :r, movements: %i[n e s w], range: :max, level:)
15
+ end
16
+
17
+ # Move the chess piece to a new valid location
18
+ # @param new_alg_pos [Symbol, Integer] expects board position in Algebraic notation, e.g., :e3
19
+ def move(new_alg_pos)
20
+ disable_castling
21
+ super
22
+ end
23
+
24
+ private
25
+
26
+ # disable castling
27
+ def disable_castling
28
+ return unless at_start
29
+
30
+ kingside = file == "h"
31
+ query = kingside ? :k : :q
32
+ query = query.upcase if side == :white
33
+ level.castling_states[query] = false
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "chess_player"
4
+
5
+ module ConsoleGame
6
+ module Chess
7
+ # Chess computer player class
8
+ class ChessComputer < ChessPlayer
9
+ def initialize(name = "", controller = nil, color = nil, m_history: [])
10
+ super
11
+ end
12
+
13
+ # Process player action
14
+ # Computer player's move
15
+ def play_turn
16
+ selection = level.usable_pieces[side].sample
17
+
18
+ assign_piece(selection)
19
+ target = piece_at_hand.possible_moves.to_a.sample
20
+
21
+ move_piece(target)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../player"
4
+ require_relative "../utilities/chess_utils"
5
+
6
+ module ConsoleGame
7
+ module Chess
8
+ # ChessPlayer is the Player object for the game Chess and it is a subclass of Player.
9
+ class ChessPlayer < Player
10
+ include ChessUtils
11
+ # @!attribute [w] piece_at_hand
12
+ # @return [ChessPiece]
13
+ attr_accessor :side, :piece_at_hand
14
+ # @!attribute [r] controller
15
+ # @return [ChessInput]
16
+ attr_reader :level, :board, :controller, :moves_history, :session_id, :cmd_usage_cp
17
+
18
+ # @param name [String]
19
+ # @param controller [ChessInput]
20
+ # @param color [Symbol] :black or :white
21
+ # @param m_history [Array<String>]
22
+ def initialize(name = "", controller = nil, color = nil, m_history: [])
23
+ super(name, controller)
24
+ @side = color
25
+ # @type [ChessPiece, nil]
26
+ @piece_at_hand = nil
27
+ @moves_history = m_history
28
+
29
+ store_cmd_usage
30
+ end
31
+
32
+ # Override: Initialise player save data
33
+ def init_data
34
+ @data = Hash.new do |hash, key|
35
+ hash[key] =
36
+ { event: nil, site: nil, date: nil, round: nil, white: nil, black: nil, result: nil, mode: nil, moves: {},
37
+ white_moves: [], black_moves: [], fens: [] }
38
+ end
39
+ end
40
+
41
+ # Register session data
42
+ # @param id [Integer] session id
43
+ # @param [Hash] metadata fields for PGN style data
44
+ # @see SessionBuilder #build_session
45
+ # @return [Hash] session data
46
+ def register_session(id, **metadata)
47
+ @session_id = id
48
+ metadata.each { |k, v| write_metadata(k, v) }
49
+ data[id]
50
+ end
51
+
52
+ # Store active level
53
+ # @param active_level [Level]
54
+ def link_level(active_level)
55
+ return if level == active_level
56
+
57
+ @level = active_level
58
+ @board = active_level.board
59
+ end
60
+
61
+ # == Game Logic ==
62
+
63
+ # Play a turn in chess as a human player, input action is handled by ChessInput
64
+ # Placeholder method for ChessComputer
65
+ def play_turn; end
66
+
67
+ # Preview a move, display the moves indictor
68
+ # Prompt player to enter move value when preview mode is used
69
+ # @param curr_alg_pos [String] algebraic position
70
+ # @return [Boolean] true if the operation is a success
71
+ def preview_move(curr_alg_pos)
72
+ return false unless assign_piece(curr_alg_pos)
73
+
74
+ level.event_msgs << board.s("level.preview", move_msg_bundle)
75
+ board.print_turn(level.event_msgs)
76
+
77
+ # Second prompt to complete the turn
78
+ controller.make_a_move(self)
79
+ true
80
+ end
81
+
82
+ # Chain with #preview_move, enables player make a move after previewing possible moves
83
+ # @param new_alg_pos [String] algebraic position
84
+ # @return [Boolean] true if the operation is a success
85
+ def move_piece(new_alg_pos)
86
+ piece_at_hand.move(new_alg_pos)
87
+ return false unless piece_at_hand.moved
88
+
89
+ msg = piece_at_hand.last_move.include?("x") ? "capture" : "move"
90
+ level.event_msgs << board.s("level.#{msg}", move_msg_bundle.merge(atk_msg_bundle))
91
+
92
+ turn_end
93
+ end
94
+
95
+ # Assign a piece and make a move on the same prompt
96
+ # @param curr_alg_pos [String] algebraic position
97
+ # @param new_alg_pos [String] algebraic position
98
+ # @return [Boolean] true if the operation is a success
99
+ def direct_move(curr_alg_pos, new_alg_pos)
100
+ assign_piece(curr_alg_pos) && move_piece(new_alg_pos)
101
+ end
102
+
103
+ # Pawn specific: Promote the pawn when it reaches the other end of the board
104
+ # @param curr_alg_pos [String] algebraic position
105
+ # @param new_alg_pos [String] algebraic position
106
+ # @param notation [Symbol] algebraic notation
107
+ # @return [Boolean] true if the operation is a success
108
+ def direct_promote(curr_alg_pos, new_alg_pos, notation)
109
+ return false unless assign_piece(curr_alg_pos) && piece_at_hand.is_a?(Pawn)
110
+
111
+ piece_at_hand.move(new_alg_pos, notation)
112
+ return false unless piece_at_hand.moved
113
+
114
+ turn_end
115
+ end
116
+
117
+ # Pawn specific: Present a list of option when player can promote a pawn
118
+ def indirect_promote
119
+ piece_at_hand.query_moves
120
+ level.refresh
121
+ level.event_msgs << board.s("level.promo_opt")
122
+ controller.promote_a_pawn
123
+ end
124
+
125
+ # Fetch and move
126
+ # @param side [Symbol]
127
+ # @param type [Symbol]
128
+ # @param target [String]
129
+ def fetch_and_move(side, type, target, file_rank = nil)
130
+ piece = level.reverse_lookup(side, type, target, file_rank)
131
+
132
+ return board.print_after_cb("level.err.notation") if invalid_assignment?(piece)
133
+
134
+ store_active_piece(piece)
135
+ move_piece(target)
136
+ end
137
+
138
+ # Invalid input
139
+ def invalid_input(input)
140
+ return false unless cmd_usage_cp != controller.command_usage.slice(:alg, :smith)
141
+
142
+ store_cmd_usage
143
+ board.print_after_cb("cmd.input.done", { input: })
144
+ false
145
+ end
146
+
147
+ private
148
+
149
+ # Fetch and copy command usage data from input
150
+ def store_cmd_usage = @cmd_usage_cp = controller.command_usage.slice(:alg, :smith)
151
+
152
+ # Move action message bundle for Paint
153
+ # @return [Hash]
154
+ def move_msg_bundle = { player: name, name: [piece_at_hand.name, board.type_hl],
155
+ info: [piece_at_hand.info, board.alg_pos_hl] }
156
+
157
+ # Capture action message bundle for Paint
158
+ # @return [Hash]
159
+ def atk_msg_bundle = { side: opposite_of(side).capitalize,
160
+ def: [piece_at_hand.captured.last&.name, board.type_hl] }
161
+
162
+ # Handling piece assignment
163
+ # @param alg_pos [String] algebraic notation
164
+ # @return [ChessPiece]
165
+ def assign_piece(alg_pos)
166
+ put_piece_down
167
+ piece = level.fetch_piece(alg_pos, bypass: false)
168
+
169
+ return board.print_after_cb("level.err.notation") if invalid_assignment?(piece)
170
+
171
+ store_active_piece(piece)
172
+ end
173
+
174
+ # Assignment validation
175
+ # @param piece [nil, ChessPiece]
176
+ # @return [Boolean]
177
+ def invalid_assignment?(piece) = piece.nil? || level.simulate_next_moves(piece).empty?
178
+
179
+ # Unassign active piece
180
+ def put_piece_down
181
+ level.active_piece = nil
182
+ self.piece_at_hand = nil
183
+ end
184
+
185
+ # store active piece
186
+ # @param piece [ChessPiece]
187
+ # @param current_level [Level]
188
+ # @return [ChessPiece]
189
+ def store_active_piece(piece, current_level = level)
190
+ self.piece_at_hand = piece
191
+ current_level.active_piece = piece_at_hand
192
+ level.update_board_state
193
+ level.game_end_check
194
+ piece_at_hand
195
+ end
196
+
197
+ # States that player action has ended
198
+ # @return [Boolean]
199
+ def turn_end
200
+ piece_at_hand.is_a?(Pawn) ? level.half_move = 0 : level.half_move += 1
201
+ moves_history << piece_at_hand.last_move
202
+ level.reset_en_passant
203
+ put_piece_down
204
+ true
205
+ end
206
+
207
+ # Access player session keys
208
+ def write_metadata(key, value) = data[session_id][key] = value.is_a?(String) ? Paint.unpaint(value) : value
209
+ end
210
+ end
211
+ end