Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .config/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ en:
* • [2] Player vs Computer (Level: Easy)
f1a: "Please select a game mode: "
f1a_err: "Invalid input, please choose between 1 or 2"
f2: "Please name Player %{count} (Default: %{name})"
f2: "Please name Player %{count} (Default: %{name}) "
f2a: "Name has been updated to '%{name}'."
f3: "Provide a valid FEN below, invalid FEN will cancel the operation and a new game will be setup instead."
err: "Sessions not found, entering new game creation mode..."
Expand Down Expand Up @@ -302,9 +302,17 @@ en:
* Black: %{b_player}
* ♞ Ruby Chess by Ancient Nimbus ver: %{ver}
export: |
%{sep}

* Your Chess session has been exported as: '%{filename}'
* It can be found at the following directory:
* ==> '%{dir}'
* You can also select and copy the text below: ↴
* To import a game, visit chess site such as: https://lichess.org/paste

%{pgn_out}

%{sep}
input:
alg: "Press 'enter' to switch input detection to Algebraic notation"
smith: "Press 'enter' to switch input detection to Smith notation"
Expand Down Expand Up @@ -361,6 +369,13 @@ en:
err: "FEN error, '%{fen_str}' is not a valid sequence. Starting a new game..."
misc:
site: "Ruby Arcade Terminal Chess by Ancient Nimbus"
status:
ongoing: "In Progress"
stalemate: "Draw"
insufficient: "Draw"
fifty_move: "Draw"
threefold: "Draw"
checkmate: "Checkmate"
pieces:
k:
name: "King"
Expand Down
25 changes: 25 additions & 0 deletions lib/console/wait_utils.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

require "whirly"

module ConsoleGame
# Wait Utils class uses the whirly gem to enhance user experience when interacting with the terminal.
# @author Ancient Nimbus
class WaitUtils
def self.wait_msg(...) = new(...).wait_msg

attr_reader :msg, :time

def initialize(msg, time: 0)
@msg = msg
@time = time
end

# Wait event via whirly
def wait_msg
Whirly.start spinner: "random_dots", status: msg, color: false, stop: "⣿" do
sleep time
end
end
end
end
12 changes: 9 additions & 3 deletions lib/console_game/chess/board.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require_relative "../../console/wait_utils"
require_relative "logics/display"

module ConsoleGame
Expand All @@ -26,17 +27,22 @@ def initialize(level)
# board.print_msg(board.s(keypath, sub))
# end

# Loading message
# @see WaitUtils #wait_msg
def loading_msg(...) = WaitUtils.wait_msg(...)

# Print after the chessboard
# @param keypath [String] TF keypath
# @param sub [Hash] `{ demo: ["some text", :red] }`
def print_after_cb(keypath, sub = {}) = print_msg(s(keypath, sub), pre: "* ")
def print_after_cb(keypath, sub = {}) = print_msg(s(keypath, sub), pre: " ")

# Print turn
# @param event_msgs [Array<String>]
def print_turn(event_msgs = [""])
print "\e[2J\e[H"
system("clear")
# print "\e[2J\e[H"

print_msg(*event_msgs, pre: "* ") unless event_msgs.empty?
print_msg(*event_msgs, pre: " ") unless event_msgs.empty?
print_chessboard
level.event_msgs.clear
# self.live_board = 0
Expand Down
10 changes: 4 additions & 6 deletions lib/console_game/chess/game.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def new_game(err: false, import: false)

# Import game mode
def import_game
print_msg(s("new.f3"), pre: "* ")
print_msg(s("new.f3"), pre: " ")
@fen = controller.ask("FEN: ", input_type: :any)
end

Expand Down Expand Up @@ -107,10 +107,8 @@ def end_game
# Reset state
def reset_state
Player.player_count(0)
@player_builder = nil
@sides = {}
setup_p1
@p2 = nil
reset_config = { player_builder: nil, sides: {}, p1: setup_p1, p2: nil, fen: nil }
reset_config.each { |var, v| instance_variable_set("@#{var}", v) }
end

# Create new session data
Expand Down Expand Up @@ -167,7 +165,7 @@ def determine_opt = p1.side == w_sym ? 1 : 2
# == Player object creation ==

# Setup player 1
def setup_p1 = @p1 = create_player(user.profile[:username])
def setup_p1 = create_player(user.profile[:username])

# Create new player builder service
# @return [PlayerBuilder]
Expand Down
50 changes: 24 additions & 26 deletions lib/console_game/chess/input/chess_input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class ChessInput < Input
include SmithNotation
include AlgebraicNotation

attr_accessor :input_scheme, :input_parser
attr_accessor :input_scheme
attr_reader :alg_reg, :smith_reg, :level, :active_side, :chess_manager

# @param game_manager [GameManager]
Expand All @@ -26,7 +26,6 @@ def initialize(game_manager = nil, chess_manager = nil)
@chess_manager = chess_manager
notation_patterns_builder
@input_scheme = smith_reg
@input_parser = SMITH_PARSER
end

# Store active level object
Expand All @@ -50,7 +49,7 @@ def turn_action(player)
# @param player [ChessPlayer]
def make_a_move(player)
input = ask(s("level.action2"), reg: SMITH_PATTERN[:gp1], input_type: :custom, empty: true)
ops = case input.scan(input_parser)
ops = case input.scan(SMITH_PARSER)
in [new_pos] then { type: :move_piece, args: [new_pos] }
else { type: :invalid_input, args: [input] }
end
Expand Down Expand Up @@ -110,8 +109,8 @@ def save(_args = [], mute: false)
def load(_args = [])
return cmd_disabled if level.nil?

# save(mute: true)
print_msg(s("cmd.load"), pre: "* ")
@level = nil
print_msg(s("cmd.load"), pre: " ")
chess_manager.setup_game
end

Expand All @@ -120,28 +119,17 @@ def export(_args = [])
return cmd_disabled if level.nil?

save_moves

dir, filename, output = PgnExport.export_session(level.session).values_at(:path, :filename, :export_data)
print_msg(s("cmd.export", { filename: [filename, "gold"], dir: [dir, "gold"] }))
puts output
dir, filename, pgn_out = PgnExport.export_session(level.session).values_at(:path, :filename, :export_data)
print_msg(s("cmd.export", {
filename: [filename, "gold"], dir: [dir, "gold"], sep: ["PGN".center(80, "=")], pgn_out:
}))
end

# Change input mode to detect Smith Notation | command pattern: `smith`
def smith(_args = [])
return cmd_disabled if level.nil?

print_msg(s("cmd.input.smith"), pre: "* ")
self.input_scheme = smith_reg
self.input_parser = SMITH_PARSER
end
def smith(_args = []) = switch_notation(:smith)

# Change input mode to detect Algebraic Notation | command pattern: `alg`
def alg(_args = [])
return cmd_disabled if level.nil?

print_msg(s("cmd.input.alg"), pre: "* ")
self.input_scheme = alg_reg
end
def alg(_args = []) = switch_notation(:alg)

# Update board settings | command pattern: `board`
# @example usage example
Expand All @@ -161,17 +149,27 @@ def board(args = [])

# == Utilities ==

# Switch notation depending on user input
# @param mode [Symbol] expects :smith or :alg
def switch_notation(mode)
return cmd_disabled if level.nil?

self.input_scheme = case mode
when :smith then smith_reg
when :alg then alg_reg
end
print_msg(s("cmd.input.#{mode}"), pre: "⠗ ")
end

# Create regexp patterns for various input modes
def notation_patterns_builder
@alg_reg = regexp_algebraic
@smith_reg = regexp_smith
end

# Setup input commands
def setup_commands
super.merge({ "save" => method(:save), "load" => method(:load), "export" => method(:export),
"smith" => method(:smith), "alg" => method(:alg), "board" => method(:board) })
end
def setup_commands = super.merge({ "save" => method(:save), "load" => method(:load), "export" => method(:export),
"smith" => method(:smith), "alg" => method(:alg), "board" => method(:board) })

# Print command is disabled at this stage
def cmd_disabled = print_msg(s("cmd.disabled"), pre: D_MSG[:warn_prefix])
Expand Down
29 changes: 20 additions & 9 deletions lib/console_game/chess/level.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,7 @@ def refresh(print_turn: true)
end

# Board state refresher
# Generate all possible move and send it to board analysis
# @see PieceAnalysis #board_analysis
def update_board_state
@threats_map, @usable_pieces = PieceAnalysis.board_analysis(fetch_all.each(&:query_moves))
end
def update_board_state = board_analysis.each { |var, v| instance_variable_set("@#{var}", v) }

# Simulate next move - Find good moves
# @param piece [ChessPiece] expects a ChessPiece object
Expand Down Expand Up @@ -117,6 +113,11 @@ def fetch_all(...) = piece_lookup.fetch_all(...)
# @see PieceLookup #reverse_lookup
def reverse_lookup(...) = piece_lookup.reverse_lookup(...)

# Loading message
# @param msg [String] message
# @param time [Integer] wait time
def loading_msg(msg, time: 2) = board.loading_msg(msg, time:)

# == Export ==

# Update session moves record
Expand All @@ -143,9 +144,9 @@ def init_level
@kings = PieceAnalysis.bw_nil_hash
@threats_map, @usable_pieces = Array.new(2) { PieceAnalysis.bw_arr_hash }
@event_msgs = []
@turn_data, @white_turn, @castling_states, @en_passant, @half_move, @full_move =
fen_data.values_at(:turn_data, :white_turn, :castling_states, :en_passant, :half, :full)
fen_data.each { |field, v| instance_variable_set("@#{field}", v) }
set_current_player
update_board_state
refresh(print_turn: false)
greet_player
end
Expand Down Expand Up @@ -177,7 +178,7 @@ def play_chess

# Player action flow
def player_action
board.print_msg(board.s("level.turn", { player: player.name }), pre: "* ")
board.print_msg(board.s("level.turn", { player: player.name }), pre: " ")
player.is_a?(ChessComputer) ? player.play_turn : controller.turn_action(player)
end

Expand All @@ -187,20 +188,30 @@ def set_current_player
@player, @opponent = white_turn ? [w_player, b_player] : [b_player, w_player]
end

# Generate all possible move and send it to board analysis
# @see PieceAnalysis #board_analysis
# @return [Hash]
def board_analysis = PieceAnalysis.board_analysis(fetch_all.each(&:query_moves))

# == Endgame Logics ==

# Handle checkmate and draw event
# @param type [String] grounds of draw
# @param side [Symbol, nil] player side
# @return [Boolean]
def handle_result(type:, side: nil)
update_event_status(type:)
save_turn
winner = session[opposite_of(side)]
kings[side].color = "#CC0000" if type == "checkmate"
event_msgs << board.s("level.endgame.#{type}", { win_player: winner })
board.print_turn(event_msgs)
board.print_turn(event_msgs[-1])
@game_ended = true
end

# Update event state
def update_event_status(type:) = session[:event].sub!(board.s("status.ongoing"), board.s("status.#{type}"))

# Add checked or checkmate marker to opponent's last move
def add_check_marker
side = player.side
Expand Down
10 changes: 5 additions & 5 deletions lib/console_game/chess/logics/piece_analysis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ module Chess
class PieceAnalysis
class << self
# Analyse the board
# usable_pieces: usable pieces of the given turn
# threats_map: all blunder tile for each side
# @return [Array<Hash<ChessPiece>>] usable_pieces and threats_map
# @return [Hash] Board analysis data
# @option return [Hash<Symbol, Set<Integer>>] :threats_map Threatened squares by side
# @option return [Hash<Symbol, Array<String>>] :usable_pieces Movable piece positions by side
def board_analysis(...) = new(...).board_analysis

# Returns a hash with :white and :black keys to nil.
Expand All @@ -33,14 +33,14 @@ def initialize(all_pieces)
# Analyse the board
# usable_pieces: usable pieces of the given turn
# threats_map: all blunder tile for each side
# @return [Array<Hash<ChessPiece>>] usable_pieces and threats_map
# @return [Hash] Board analysis data
def board_analysis
threats_map, usable_pieces = Array.new(2) { PieceAnalysis.bw_arr_hash }
pieces_group.each do |side, pieces|
threats_map[side] = add_pos_to_blunder_tracker(pieces)
usable_pieces[side] = pieces.map { |piece| piece.info unless piece.possible_moves.empty? }.compact
end
[threats_map, usable_pieces]
{ threats_map:, usable_pieces: }
end

private
Expand Down
10 changes: 6 additions & 4 deletions lib/console_game/chess/utilities/fen_import.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,17 @@ def parse_fen
# @param fen_data [Array<String>] expects splitted FEN as an array
# @return [Array<Hash, nil>]
def process_fen_data(fen_data)
fen_board, active_color, c_state, ep_state, halfmove, fullmove = fen_data
fen_board, active_color, c_state, ep_state, half_move, full_move = fen_data
[
parse_piece_placement(fen_board), parse_active_color(active_color), parse_castling_str(c_state),
parse_en_passant(ep_state), parse_move_num(halfmove, :half), parse_move_num(fullmove, :full)
parse_en_passant(ep_state), parse_move_num(half_move, :half_move), parse_move_num(full_move, :full_move)
]
end

# Process flow when there is an issue during FEN parsing
# @param err_msg [String] error message during FEN error
def fen_error(err_msg: "FEN error, '#{fen_str}' is not a valid sequence. Starting a new game...")
puts err_msg
level.loading_msg(err_msg, time: 3)
self.class.parse_fen(level)
end

Expand Down Expand Up @@ -173,7 +173,9 @@ def ep_ghost_pos(alg_pos) = to_1d_pos(alg_pos)
# @param num [String] expects a string with either half-move or full-move data
# @param type [Symbol] specify the key type for the hash
# @return [Hash, nil] a hash of either half-move or full-move data
def parse_move_num(num, type) = num.match?(/\A\d+\z/) && %i[half full].include?(type) ? { type => num.to_i } : nil
def parse_move_num(num, type)
num.match?(/\A\d+\z/) && %i[half_move full_move].include?(type) ? { type => num.to_i } : nil
end

# Initialize chess piece via string value
# @param pos [Integer] positional value
Expand Down
Loading