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,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ConsoleGame
|
4
|
+
module Chess
|
5
|
+
# Logic module for the game Chess in Console Game
|
6
|
+
# @author Ancient Nimbus
|
7
|
+
# @version 1.3.2
|
8
|
+
module Logic
|
9
|
+
# Default values
|
10
|
+
PRESET = { bound: [8, 8], nil_hash: -> { Hash.new { |h, k| h[k] = nil } } }.freeze
|
11
|
+
|
12
|
+
# A hash of lambda functions for calculating movement in 8 directions on a grid
|
13
|
+
DIRECTIONS = {
|
14
|
+
n: ->(value, step, row) { value + row * step },
|
15
|
+
ne: ->(value, step, row) { value + row * step + step },
|
16
|
+
e: ->(value, step, _row) { value + step },
|
17
|
+
se: ->(value, step, row) { value - row * step + step },
|
18
|
+
s: ->(value, step, row) { value - row * step },
|
19
|
+
sw: ->(value, step, row) { value - row * step - step },
|
20
|
+
w: ->(value, step, _row) { value - step },
|
21
|
+
nw: ->(value, step, row) { value + row * step - step }
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Recursively find the next position depending on direction
|
27
|
+
# @param pos [Integer] start position
|
28
|
+
# @param path [Symbol] see DIRECTIONS for available options. E.g., :e for count from left to right
|
29
|
+
# @param combination [Array<Integer>] default value is an empty array
|
30
|
+
# @param length [Symbol, Integer] :max for maximum range within bound or custom length of the sequence
|
31
|
+
# @param bound [Array<Integer>] grid size `[row, col]`
|
32
|
+
# @return [Array<Integer>] array of numbers
|
33
|
+
def pathfinder(pos = 0, path = :e, combination = nil, length: :max, bound: PRESET[:bound])
|
34
|
+
combination ||= [pos]
|
35
|
+
arr_size = combination.size
|
36
|
+
return combination if arr_size == length
|
37
|
+
|
38
|
+
next_value = DIRECTIONS.fetch(path) do |key|
|
39
|
+
raise ArgumentError, "Invalid path: #{key}"
|
40
|
+
end.call(pos, arr_size, bound[0])
|
41
|
+
|
42
|
+
combination << next_value
|
43
|
+
|
44
|
+
if out_of_bound?(next_value, bound) || not_adjacent?(path, combination)
|
45
|
+
return length == :max ? combination[0..-2] : []
|
46
|
+
end
|
47
|
+
|
48
|
+
pathfinder(pos, path, combination, length:, bound:)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Calculate valid sequence based on positional value
|
52
|
+
# @param movements [Hash] expects a hash with DIRECTION as keys
|
53
|
+
# @param pos [Integer] positional value within a matrix
|
54
|
+
# @param paths [Hash] a hash with `nil` set as default value
|
55
|
+
# @return [Hash<Array<Integer>>] an array of valid directional path within given bound
|
56
|
+
def all_paths(movements, pos, paths: PRESET[:nil_hash].call)
|
57
|
+
movements.each do |path, range|
|
58
|
+
next if range.nil?
|
59
|
+
|
60
|
+
sequence = path(pos, path, range: range)
|
61
|
+
paths[path] = sequence unless sequence.empty?
|
62
|
+
end
|
63
|
+
paths
|
64
|
+
end
|
65
|
+
|
66
|
+
# Path via Pathfinder
|
67
|
+
# @param pos [Integer] board positional value
|
68
|
+
# @param path [Symbol] compass direction
|
69
|
+
# @param range [Symbol, Integer] movement range of the given piece or :max for furthest possible range
|
70
|
+
# @return [Array<Integer>]
|
71
|
+
def path(pos = 0, path = :e, range: 1)
|
72
|
+
seq_length = range.is_a?(Integer) ? range + 1 : range
|
73
|
+
path = pathfinder(pos, path, length: seq_length)
|
74
|
+
path.size > 1 ? path : []
|
75
|
+
end
|
76
|
+
|
77
|
+
# Possible movement direction for the given piece
|
78
|
+
# @param directions [Array<Symbol>] possible paths
|
79
|
+
# @param range [Symbol, Integer] movement range of the given piece or :max for furthest possible range
|
80
|
+
# @param movements [Hash] a hash with `nil` set as default value
|
81
|
+
# @return [Hash]
|
82
|
+
def movement_range(directions = [], range: 1, movements: PRESET[:nil_hash].call)
|
83
|
+
DIRECTIONS.each_key { |k| movements[k] }
|
84
|
+
directions.each { |dir| movements[dir] = range }
|
85
|
+
movements
|
86
|
+
end
|
87
|
+
|
88
|
+
# Convert coordinate array to cell position
|
89
|
+
# @param coord [Array<Integer>] `[row, col]`
|
90
|
+
# @param bound [Array<Integer>] `[row, col]`
|
91
|
+
# @return [Integer]
|
92
|
+
def to_pos(coord = [0, 0], bound: PRESET[:bound])
|
93
|
+
row, col = coord
|
94
|
+
grid_width, _grid_height = bound
|
95
|
+
pos_value = (row * grid_width) + col
|
96
|
+
|
97
|
+
raise ArgumentError, "#{coord} is out of bound!" unless pos_value.between?(0, bound.reduce(:*) - 1)
|
98
|
+
|
99
|
+
pos_value
|
100
|
+
end
|
101
|
+
|
102
|
+
# Convert cell position to coordinate array
|
103
|
+
# @param pos [Integer] positional value
|
104
|
+
# @param bound [Array<Integer>] `[row, col]`
|
105
|
+
# @return [Array<Integer>]
|
106
|
+
def to_coord(pos = 0, bound: PRESET[:bound])
|
107
|
+
raise ArgumentError, "#{pos} is out of bound!" unless pos.between?(0, bound.reduce(:*) - 1)
|
108
|
+
|
109
|
+
grid_width, _grid_height = bound
|
110
|
+
pos.divmod(grid_width)
|
111
|
+
end
|
112
|
+
|
113
|
+
# == Pathfinder ==
|
114
|
+
|
115
|
+
# Helper method to check for out of bound cases for top and bottom borders
|
116
|
+
# @param value [Integer]
|
117
|
+
# @param bound [Array<Integer>] grid size `[row, col]`
|
118
|
+
# @return [Boolean]
|
119
|
+
def out_of_bound?(value, bound) = value.negative? || value > bound.reduce(:*) - 1
|
120
|
+
|
121
|
+
# Helper method to check for out of bound cases for left and right borders (Previously: not_one_unit_apart?)
|
122
|
+
# @param path [Symbol] see DIRECTIONS for available options. E.g., :e for count from left to right
|
123
|
+
# @param values_arr [Array<Integer>]
|
124
|
+
# @return [Boolean]
|
125
|
+
def not_adjacent?(path, values_arr)
|
126
|
+
return false unless %i[e w ne nw se sw].include?(path)
|
127
|
+
|
128
|
+
first_col, prev_col, curr_col = [values_arr.first, *values_arr.last(2)].map { |pos| to_coord(pos)[1] }
|
129
|
+
|
130
|
+
wraps_around_edge = ((first_col - curr_col).abs - (values_arr.size - 1)).abs != 0
|
131
|
+
not_adjacent = (prev_col - curr_col).abs != 1
|
132
|
+
|
133
|
+
wraps_around_edge || not_adjacent
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ConsoleGame
|
4
|
+
module Chess
|
5
|
+
# The Moves Simulation class helps simulate the next possible moves of a chess piece
|
6
|
+
# @author Ancient Nimbus
|
7
|
+
class MovesSimulation
|
8
|
+
# Simulates the next possible moves for a given chess position.
|
9
|
+
# @return [Array<Integer>] good moves
|
10
|
+
def self.simulate_next_moves(...) = new(...).simulate_next_moves
|
11
|
+
|
12
|
+
# @!attribute [r] turn_data
|
13
|
+
# @return [Array<ChessPiece, String>] complete state of the current turn
|
14
|
+
attr_reader :level, :piece, :turn_data, :i_am_the_king, :king, :smart_moves
|
15
|
+
attr_accessor :good_pos
|
16
|
+
|
17
|
+
# @param level [Level] expects a Chess::Level class object
|
18
|
+
# @param piece [ChessPiece] expects a ChessPiece class objet
|
19
|
+
# @param smart [Boolean] when true, it returns moves that is safe for itself too
|
20
|
+
def initialize(level, piece, smart: false)
|
21
|
+
@level = level
|
22
|
+
@piece = piece
|
23
|
+
@turn_data = level.turn_data
|
24
|
+
@good_pos = []
|
25
|
+
is_king = piece.is_a?(King)
|
26
|
+
@king = is_king ? piece : level.kings[piece.side]
|
27
|
+
@i_am_the_king = is_king
|
28
|
+
@smart_moves = smart
|
29
|
+
end
|
30
|
+
|
31
|
+
# Simulate next move - Find good moves
|
32
|
+
# @return [Set<Integer>] good moves
|
33
|
+
def simulate_next_moves
|
34
|
+
current_pos = piece.curr_pos
|
35
|
+
turn_data[current_pos] = ""
|
36
|
+
piece.possible_moves.each { |new_pos| simulate_move(new_pos) }
|
37
|
+
restore_previous_state(current_pos)
|
38
|
+
good_pos.compact.to_set
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# Simulation helper: find a good move
|
44
|
+
# @param new_pos [Integer]
|
45
|
+
def simulate_move(new_pos)
|
46
|
+
tile = turn_data[new_pos]
|
47
|
+
turn_data[new_pos] = piece
|
48
|
+
piece.curr_pos = new_pos
|
49
|
+
level.update_board_state
|
50
|
+
good_pos.push(new_pos) if good_move?
|
51
|
+
turn_data[new_pos] = tile
|
52
|
+
end
|
53
|
+
|
54
|
+
# Helper to determine good move
|
55
|
+
def good_move? = !(i_am_the_king ? am_i_in_danger? : hows_the_king?)
|
56
|
+
|
57
|
+
# Determine check conditions based on smart_moves
|
58
|
+
def hows_the_king? = smart_moves ? (am_i_in_danger? && king_in_danger?) : king_in_danger?
|
59
|
+
|
60
|
+
# Check yourself
|
61
|
+
def am_i_in_danger? = piece.under_threat?
|
62
|
+
|
63
|
+
# Check the king
|
64
|
+
def king_in_danger? = king.under_threat?
|
65
|
+
|
66
|
+
# Simulation helper: restore pre simulation state
|
67
|
+
# @param current_pos [Integer]
|
68
|
+
def restore_previous_state(current_pos)
|
69
|
+
piece.curr_pos = current_pos
|
70
|
+
turn_data[current_pos] = piece
|
71
|
+
level.update_board_state
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ConsoleGame
|
4
|
+
module Chess
|
5
|
+
# The Piece Analysis class calculates the overall state of the chessboard
|
6
|
+
# @author Ancient Nimbus
|
7
|
+
class PieceAnalysis
|
8
|
+
class << self
|
9
|
+
# Analyse the board
|
10
|
+
# @return [Hash] Board analysis data
|
11
|
+
# @option return [Hash<Symbol, Set<Integer>>] :threats_map Threatened squares by side
|
12
|
+
# @option return [Hash<Symbol, Array<String>>] :usable_pieces Movable piece positions by side
|
13
|
+
def board_analysis(...) = new(...).board_analysis
|
14
|
+
|
15
|
+
# Returns a hash with :white and :black keys to nil.
|
16
|
+
# @return [Hash<nil>]
|
17
|
+
def bw_nil_hash = { white: nil, black: nil }
|
18
|
+
|
19
|
+
# Returns a Hash with default values as empty arrays, and initial keys :white and :black set to empty arrays.
|
20
|
+
# @return [Hash<Array>]
|
21
|
+
def bw_arr_hash = Hash.new { |h, k| h[k] = [] }.merge({ white: [], black: [] })
|
22
|
+
end
|
23
|
+
|
24
|
+
# @!attribute [r] all_pieces
|
25
|
+
# @return [Array<ChessPiece>] All the ChessPiece that's currently on the board
|
26
|
+
attr_reader :all_pieces
|
27
|
+
|
28
|
+
# @param all_pieces [Array<ChessPiece>]
|
29
|
+
def initialize(all_pieces)
|
30
|
+
@all_pieces = all_pieces
|
31
|
+
end
|
32
|
+
|
33
|
+
# Analyse the board
|
34
|
+
# usable_pieces: usable pieces of the given turn
|
35
|
+
# threats_map: all blunder tile for each side
|
36
|
+
# @return [Hash] Board analysis data
|
37
|
+
def board_analysis
|
38
|
+
threats_map, usable_pieces = Array.new(2) { PieceAnalysis.bw_arr_hash }
|
39
|
+
pieces_group.each do |side, pieces|
|
40
|
+
threats_map[side] = add_pos_to_blunder_tracker(pieces)
|
41
|
+
usable_pieces[side] = pieces.map { |piece| piece.info unless piece.possible_moves.empty? }.compact
|
42
|
+
end
|
43
|
+
{ threats_map:, usable_pieces: }
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# Split chess pieces into two group
|
49
|
+
# @return [Hash]
|
50
|
+
def pieces_group
|
51
|
+
grouped_pieces = PieceAnalysis.bw_nil_hash
|
52
|
+
grouped_pieces[:white], grouped_pieces[:black] = all_pieces.partition { |piece| piece.side == :white }
|
53
|
+
grouped_pieces
|
54
|
+
end
|
55
|
+
|
56
|
+
# Helper: add blunder tiles to session variable
|
57
|
+
# @param pieces [ChessPiece]
|
58
|
+
# @return [Set<Integer>]
|
59
|
+
def add_pos_to_blunder_tracker(pieces)
|
60
|
+
pawns, back_row = pieces.partition { |piece| piece.is_a?(Pawn) }
|
61
|
+
bad_moves = pawns_threat(pawns) + back_rows_threat(back_row)
|
62
|
+
bad_moves.flatten.sort.to_set
|
63
|
+
end
|
64
|
+
|
65
|
+
# Helper: Add pawn pieces's threat to map
|
66
|
+
# @param pawns [Pawn]
|
67
|
+
# @return [Array<Integer>]
|
68
|
+
def pawns_threat(pawns) = pawns.map { |unit| unit.sights + unit.targets.values.compact }
|
69
|
+
|
70
|
+
# Helper: Add back row pieces's threat to map
|
71
|
+
# @param back_row [King, Queen, Bishop, Knight, Rook]
|
72
|
+
# @return [Array<Integer>]
|
73
|
+
def back_rows_threat(back_row) = back_row.map { |unit| unit.sights + unit.possible_moves.compact }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../utilities/chess_utils"
|
4
|
+
|
5
|
+
module ConsoleGame
|
6
|
+
module Chess
|
7
|
+
# The Piece lookup class handles various piece fetching tasks
|
8
|
+
# @author Ancient Nimbus
|
9
|
+
class PieceLookup
|
10
|
+
include ChessUtils
|
11
|
+
# @!attribute [r] level
|
12
|
+
# @return [Level] current level
|
13
|
+
# @!attribute [r] turn_data
|
14
|
+
# @return [Array<ChessPiece, String>] complete state of the current turn
|
15
|
+
# @!attribute [r] player
|
16
|
+
# @return [ChessPlayer, ChessComputer] player of the current turn
|
17
|
+
attr_reader :level, :turn_data, :player
|
18
|
+
|
19
|
+
# @param level [Level] expects a Chess::Level class object
|
20
|
+
def initialize(level)
|
21
|
+
@level = level
|
22
|
+
@turn_data = level.turn_data
|
23
|
+
@player = level.player
|
24
|
+
end
|
25
|
+
|
26
|
+
# Fetch a single chess piece
|
27
|
+
# @param query [String] algebraic notation `"e4"`
|
28
|
+
# @param bypass [Boolean] for internal use only, use this to bypass user-end validation
|
29
|
+
# @return [ChessPiece]
|
30
|
+
def fetch_piece(query, bypass: true)
|
31
|
+
return nil unless bypass || choices.include?(query)
|
32
|
+
|
33
|
+
turn_data[to_1d_pos(query)]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Fetch a group of pieces notation from turn_data based on algebraic notation
|
37
|
+
# @param query [Array<String>]
|
38
|
+
# @return [Array<Array<ChessPiece>, Array<String>>]
|
39
|
+
def group_fetch(query)
|
40
|
+
pieces = []
|
41
|
+
notations = query.flatten.map do |alg_pos|
|
42
|
+
pieces << (piece = fetch_piece(alg_pos))
|
43
|
+
piece.notation
|
44
|
+
end
|
45
|
+
[pieces, notations]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Grab all pieces, only whites or only blacks
|
49
|
+
# @param side [Symbol] expects :all, :white or :black
|
50
|
+
# @param type [ChessPiece, King, Queen, Rook, Bishop, Knight, Pawn] limit selection
|
51
|
+
# @return [Array<ChessPiece>] a list of chess pieces
|
52
|
+
def fetch_all(side = :all, type: ChessPiece)
|
53
|
+
turn_data.select { |tile| tile.is_a?(type) && (SIDES_SYM.include?(side) ? tile.side == side : true) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Lookup a piece based on its possible move position
|
57
|
+
# @param side [Symbol] :black or :white
|
58
|
+
# @param type [Symbol] expects a notation
|
59
|
+
# @param target [String] expects a algebraic notation
|
60
|
+
# @param file_rank [String] expects a file rank data
|
61
|
+
# @return [ChessPiece, nil]
|
62
|
+
def reverse_lookup(side, type, target, file_rank = nil)
|
63
|
+
type = Chess.const_get(ALG_REF.dig(type, :class))
|
64
|
+
result = refined_lookup(fetch_all(side, type:), side, to_1d_pos(target), file_rank)
|
65
|
+
result.size > 1 ? nil : result[0]
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# Helper: Filter pieces by checking whether it is usable at the current term with file info for extra measure
|
71
|
+
# @param filtered_pieces [Array<ChessPiece>]
|
72
|
+
# @param side [Symbol] :black or :white
|
73
|
+
# @param new_pos [Integer] expects a positional value
|
74
|
+
# @param file_rank [String] expects a file rank data
|
75
|
+
# @return [Array<ChessPiece>]
|
76
|
+
def refined_lookup(filtered_pieces, side, new_pos, file_rank)
|
77
|
+
filtered_pieces.select do |piece|
|
78
|
+
next unless usable_pieces[side].include?(piece.info)
|
79
|
+
|
80
|
+
piece.possible_moves.include?(new_pos) && (file_rank.nil? || piece.info.include?(file_rank))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Helper: fetch latest usable piece
|
85
|
+
# @return [Hash{Symbol => Array<String>}]
|
86
|
+
def usable_pieces = level.usable_pieces
|
87
|
+
|
88
|
+
# Helper: fetch latest player choices
|
89
|
+
# @return [Array<String>]
|
90
|
+
def choices = usable_pieces[player.side]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "chess_piece"
|
4
|
+
|
5
|
+
module ConsoleGame
|
6
|
+
module Chess
|
7
|
+
# Bishop is a sub-class of ChessPiece for the game Chess in Console Game
|
8
|
+
# @author Ancient Nimbus
|
9
|
+
class Bishop < 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 = :c1, side = :white, level: nil)
|
14
|
+
super(alg_pos, side, :b, movements: %i[ne se sw nw], range: :max, level:)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../logics/logic"
|
4
|
+
require_relative "../logics/display"
|
5
|
+
require_relative "../utilities/fen_import"
|
6
|
+
require_relative "../utilities/chess_utils"
|
7
|
+
|
8
|
+
module ConsoleGame
|
9
|
+
module Chess
|
10
|
+
# Chess Piece is a parent class for the game Chess in Console Game
|
11
|
+
# @author Ancient Nimbus
|
12
|
+
class ChessPiece
|
13
|
+
include ChessUtils
|
14
|
+
include Logic
|
15
|
+
include Display
|
16
|
+
|
17
|
+
# Points system for chess pieces
|
18
|
+
PTS_VALUES = { k: 100, q: 9, r: 5, b: 5, n: 3, p: 1 }.freeze
|
19
|
+
|
20
|
+
attr_accessor :at_start, :curr_pos, :targets, :sights, :color, :moved, :last_move, :possible_moves
|
21
|
+
attr_reader :level, :notation, :name, :icon, :pts, :movements, :start_pos, :side, :captured,
|
22
|
+
:std_color, :highlight
|
23
|
+
|
24
|
+
# @param alg_pos [Symbol] expects board position in Algebraic notation
|
25
|
+
# @param side [Symbol] specify unit side :black or :white
|
26
|
+
# @param notation [Symbol] expects a chess notation of a specific piece, e.g., Knight = :n
|
27
|
+
# @param movements [Hash] expects compass direction
|
28
|
+
# @param range [Integer, Symbol] unit movement range
|
29
|
+
# @param level [Chess::Level] chess level object
|
30
|
+
# @param at_start [Boolean] determine if the piece is at its start location
|
31
|
+
def initialize(alg_pos = :e1, side = :white, notation = :k, movements: DIRECTIONS.keys, range: 1, level: nil,
|
32
|
+
at_start: true)
|
33
|
+
@level = level
|
34
|
+
@side = side
|
35
|
+
@pts = PTS_VALUES[notation]
|
36
|
+
piece_styling(notation)
|
37
|
+
@start_pos = alg_pos.is_a?(Symbol) ? alg_map[alg_pos] : alg_pos
|
38
|
+
@curr_pos = start_pos
|
39
|
+
@at_start = at_start.nil? ? false : at_start # @todo Better logic??
|
40
|
+
movements_trackers(movements, range)
|
41
|
+
end
|
42
|
+
|
43
|
+
# == Public methods ==
|
44
|
+
|
45
|
+
# Move the chess piece to a new valid location
|
46
|
+
# @param new_alg_pos [Symbol, Integer] expects board position in Algebraic notation, e.g., :e3
|
47
|
+
def move(new_alg_pos)
|
48
|
+
old_pos = curr_pos
|
49
|
+
new_pos = new_alg_pos.is_a?(Integer) ? new_alg_pos : alg_map[new_alg_pos.to_sym]
|
50
|
+
return self.moved = false unless possible_moves.include?(new_pos)
|
51
|
+
|
52
|
+
process_movement(level.turn_data, old_pos, new_pos)
|
53
|
+
self.moved = true
|
54
|
+
end
|
55
|
+
|
56
|
+
# Query and update possible_moves
|
57
|
+
# @param limiter [Array] limit piece movement when player is checked
|
58
|
+
def query_moves(limiter = [])
|
59
|
+
validate_moves(level.turn_data, curr_pos).map { |pos| alg_map.key(pos) }
|
60
|
+
@possible_moves = possible_moves & limiter unless limiter.empty?
|
61
|
+
threat_response
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns the algebraic notation of current position
|
65
|
+
# @return [String] full algebraic position
|
66
|
+
def info = to_alg_pos(curr_pos)
|
67
|
+
|
68
|
+
# Returns the file of current position
|
69
|
+
# @return [String] file of the piece
|
70
|
+
def file = info[0]
|
71
|
+
|
72
|
+
# Returns the rank of current position
|
73
|
+
# @return [String] file of the piece
|
74
|
+
def rank = info[1]
|
75
|
+
|
76
|
+
# Determine if a piece is currently under threats
|
77
|
+
# @return [Boolean]
|
78
|
+
def under_threat? = level.threats_map[opposite_of(side)].include?(curr_pos)
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# == Setup ==
|
83
|
+
|
84
|
+
# Initialize piece styling
|
85
|
+
# @param notation [Symbol] expects a chess notation of a specific piece, e.g., Knight = :n
|
86
|
+
def piece_styling(notation)
|
87
|
+
@notation, @name, @icon = PIECES[notation].slice(:notation, :name, :style1).values
|
88
|
+
@std_color, @highlight = THEME[:classic].slice(side, :highlight).values
|
89
|
+
@color = std_color
|
90
|
+
end
|
91
|
+
|
92
|
+
# Initialize piece movements trackers
|
93
|
+
# @param movements [Array]
|
94
|
+
# @param range [Integer, Symbol]
|
95
|
+
def movements_trackers(movements, range)
|
96
|
+
@movements = movement_range(movements, range: range)
|
97
|
+
@targets = movement_range(movements, range: nil)
|
98
|
+
@sights, @captured = Array.new(2) { [] }
|
99
|
+
@moved = false
|
100
|
+
end
|
101
|
+
|
102
|
+
# == Move logic ==
|
103
|
+
|
104
|
+
# Process movement
|
105
|
+
# @param turn_data [Array<ChessPiece, String>]
|
106
|
+
# @param old_pos [Integer]
|
107
|
+
# @param new_pos [Integer]
|
108
|
+
# @return [Integer] new location's positional value
|
109
|
+
def process_movement(turn_data, old_pos, new_pos)
|
110
|
+
self.at_start = false
|
111
|
+
self.curr_pos = new_pos
|
112
|
+
new_tile = turn_data[new_pos]
|
113
|
+
|
114
|
+
turn_data[old_pos] = ""
|
115
|
+
turn_data[new_pos] = self
|
116
|
+
@last_move = store_last_move(:move, old_pos:)
|
117
|
+
return curr_pos if new_tile.is_a?(String)
|
118
|
+
|
119
|
+
captured << new_tile
|
120
|
+
@last_move = store_last_move(:capture, old_pos:)
|
121
|
+
curr_pos
|
122
|
+
end
|
123
|
+
|
124
|
+
# Last move formatted as algebraic notation
|
125
|
+
# @param event [Symbol] expects the following key: :move, :capture
|
126
|
+
# @param old_pos [Integer] original position
|
127
|
+
# @return [String]
|
128
|
+
def store_last_move(event = :move, old_pos:)
|
129
|
+
alg_notation = notation.to_s.upcase
|
130
|
+
move = "#{alg_notation}#{info}"
|
131
|
+
if event == :capture
|
132
|
+
move.insert(1, "x")
|
133
|
+
move.insert(1, to_alg_pos(old_pos, :f))
|
134
|
+
end
|
135
|
+
move
|
136
|
+
end
|
137
|
+
|
138
|
+
# Valid movement
|
139
|
+
# @param pos1 [Integer] original board positional value
|
140
|
+
# @param pos2 [Integer] new board positional value
|
141
|
+
# @return [Boolean]
|
142
|
+
# def valid_moves?(pos1, pos2); end
|
143
|
+
|
144
|
+
# == Threat Query ==
|
145
|
+
|
146
|
+
# Handle events when the opposite active piece can capture self in the upcoming turn
|
147
|
+
# Switch color when under threat
|
148
|
+
def threat_response = self.color = under_threat_by?(level.active_piece) ? highlight : std_color
|
149
|
+
|
150
|
+
# Determine if a piece might get attacked by multiple pieces, similar to #under_threat? but more specific
|
151
|
+
# @param attacker [ChessPiece]
|
152
|
+
# @return [Boolean]
|
153
|
+
def under_threat_by?(attacker)
|
154
|
+
return false unless attacker.is_a?(ChessPiece) && attacker.side != side
|
155
|
+
|
156
|
+
attacker.targets.value?(curr_pos) && attacker.possible_moves.include?(curr_pos)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns the path of an attacking chess piece based on the current position of self
|
160
|
+
# @param attacker [ChessPiece]
|
161
|
+
# @return [Array<Integer>]
|
162
|
+
def find_attacker_path(attacker)
|
163
|
+
atk_direction = attacker.targets.key(curr_pos)
|
164
|
+
opt = %i[n s].include?(atk_direction) ? 0 : 1
|
165
|
+
length = (to_coord(attacker.curr_pos)[opt] - to_coord(curr_pos)[opt]).abs
|
166
|
+
pathfinder(attacker.curr_pos, atk_direction, length:)
|
167
|
+
end
|
168
|
+
|
169
|
+
# == Pathfinder related ==
|
170
|
+
|
171
|
+
# Store all valid placement
|
172
|
+
# @param pos [Integer] positional value within a matrix
|
173
|
+
def validate_moves(turn_data, pos = curr_pos)
|
174
|
+
self.sights = []
|
175
|
+
targets.transform_values! { |_| nil }
|
176
|
+
possible_moves = all_paths(movements, pos)
|
177
|
+
possible_moves.each do |path, positions|
|
178
|
+
# remove blocked spot and onwards
|
179
|
+
possible_moves[path] = detect_occupied_tiles(path, turn_data, positions)
|
180
|
+
end
|
181
|
+
@possible_moves = (possible_moves.values.flatten + targets.values).compact.to_set
|
182
|
+
end
|
183
|
+
|
184
|
+
# Detect blocked tile based on the given positions
|
185
|
+
# @param path [Symbol]
|
186
|
+
# @param turn_data [Array] board data array
|
187
|
+
# @param positions [Array] rank array
|
188
|
+
# @return [Array]
|
189
|
+
def detect_occupied_tiles(path, turn_data, positions)
|
190
|
+
new_positions = positions[1..]
|
191
|
+
positions.each_with_index do |pos, idx|
|
192
|
+
tile = turn_data[pos]
|
193
|
+
next unless tile != self && tile.is_a?(ChessPiece)
|
194
|
+
|
195
|
+
tile.side == side ? sights.push(pos) : targets[path] = pos
|
196
|
+
# p "#{side} #{name} at #{alg_map.key(curr_pos)} is watching over #{sights}"
|
197
|
+
new_positions = positions[1...idx]
|
198
|
+
break
|
199
|
+
end
|
200
|
+
new_positions
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|