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.
- checksums.yaml +7 -0
- data/.config/locale/debug_en.yml +4 -0
- data/.config/locale/en.yml +424 -0
- data/bin/chess_cli +6 -0
- data/lib/chess_cli.rb +11 -0
- data/lib/console/console.rb +202 -0
- data/lib/console/wait_utils.rb +25 -0
- data/lib/console_game/base_game.rb +74 -0
- data/lib/console_game/chess/board.rb +110 -0
- data/lib/console_game/chess/game.rb +184 -0
- data/lib/console_game/chess/input/algebraic_notation.rb +103 -0
- data/lib/console_game/chess/input/chess_input.rb +191 -0
- data/lib/console_game/chess/input/smith_notation.rb +38 -0
- data/lib/console_game/chess/launcher.rb +20 -0
- data/lib/console_game/chess/level.rb +276 -0
- data/lib/console_game/chess/logics/display.rb +182 -0
- data/lib/console_game/chess/logics/endgame_logic.rb +126 -0
- data/lib/console_game/chess/logics/logic.rb +137 -0
- data/lib/console_game/chess/logics/moves_simulation.rb +75 -0
- data/lib/console_game/chess/logics/piece_analysis.rb +76 -0
- data/lib/console_game/chess/logics/piece_lookup.rb +93 -0
- data/lib/console_game/chess/pieces/bishop.rb +18 -0
- data/lib/console_game/chess/pieces/chess_piece.rb +204 -0
- data/lib/console_game/chess/pieces/king.rb +200 -0
- data/lib/console_game/chess/pieces/knight.rb +46 -0
- data/lib/console_game/chess/pieces/pawn.rb +142 -0
- data/lib/console_game/chess/pieces/queen.rb +16 -0
- data/lib/console_game/chess/pieces/rook.rb +37 -0
- data/lib/console_game/chess/player/chess_computer.rb +25 -0
- data/lib/console_game/chess/player/chess_player.rb +211 -0
- data/lib/console_game/chess/utilities/chess_utils.rb +67 -0
- data/lib/console_game/chess/utilities/fen_export.rb +114 -0
- data/lib/console_game/chess/utilities/fen_import.rb +196 -0
- data/lib/console_game/chess/utilities/load_manager.rb +51 -0
- data/lib/console_game/chess/utilities/pgn_export.rb +97 -0
- data/lib/console_game/chess/utilities/pgn_utils.rb +134 -0
- data/lib/console_game/chess/utilities/player_builder.rb +74 -0
- data/lib/console_game/chess/utilities/session_builder.rb +48 -0
- data/lib/console_game/chess/version.rb +8 -0
- data/lib/console_game/console_menu.rb +68 -0
- data/lib/console_game/game_manager.rb +181 -0
- data/lib/console_game/input.rb +87 -0
- data/lib/console_game/player.rb +100 -0
- data/lib/console_game/user_profile.rb +65 -0
- data/lib/nimbus_file_utils/nimbus_file_utils.rb +194 -0
- data/user_data/.keep +0 -0
- data/user_data/dummy_user.json +124 -0
- data/user_data/pgn_export/.keep +0 -0
- 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
|