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,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