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,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "whirly"
|
4
|
+
|
5
|
+
module ConsoleGame
|
6
|
+
# Wait Utils class uses the whirly gem to enhance user experience when interacting with the terminal.
|
7
|
+
# @author Ancient Nimbus
|
8
|
+
class WaitUtils
|
9
|
+
def self.wait_msg(...) = new(...).wait_msg
|
10
|
+
|
11
|
+
attr_reader :msg, :time
|
12
|
+
|
13
|
+
def initialize(msg, time: 0)
|
14
|
+
@msg = msg
|
15
|
+
@time = time
|
16
|
+
end
|
17
|
+
|
18
|
+
# Wait event via whirly
|
19
|
+
def wait_msg
|
20
|
+
Whirly.start spinner: "random_dots", status: msg, color: false, stop: "⣿" do
|
21
|
+
sleep time
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../console/console"
|
4
|
+
require_relative "../nimbus_file_utils/nimbus_file_utils"
|
5
|
+
|
6
|
+
module ConsoleGame
|
7
|
+
# Base Game class
|
8
|
+
class BaseGame
|
9
|
+
include Console
|
10
|
+
include ::NimbusFileUtils
|
11
|
+
|
12
|
+
attr_reader :game_manager, :controller, :title, :user, :ver
|
13
|
+
attr_accessor :state, :game_result
|
14
|
+
|
15
|
+
# @param game_manager [GameManager]
|
16
|
+
# @param title [String]
|
17
|
+
# @param input [Input]
|
18
|
+
# @param ver [String] game version
|
19
|
+
def initialize(game_manager = nil, title = "Base Game", input = nil, ver:)
|
20
|
+
@ver = ver
|
21
|
+
@game_manager = game_manager
|
22
|
+
@controller = input
|
23
|
+
@title = title
|
24
|
+
@user = game_config[:users][0]
|
25
|
+
@state = :created
|
26
|
+
end
|
27
|
+
|
28
|
+
# Game config
|
29
|
+
def game_config
|
30
|
+
return nil if game_manager.nil?
|
31
|
+
|
32
|
+
{ users: [game_manager.user] }
|
33
|
+
end
|
34
|
+
|
35
|
+
# State machine
|
36
|
+
|
37
|
+
# Start the game
|
38
|
+
def start
|
39
|
+
self.state = :playing
|
40
|
+
boot
|
41
|
+
setup_game
|
42
|
+
end
|
43
|
+
|
44
|
+
# Change game state to paused
|
45
|
+
def pause = self.state = :paused
|
46
|
+
|
47
|
+
# Change game state to playing
|
48
|
+
def resume = self.state = :playing
|
49
|
+
|
50
|
+
# Change game state to ended
|
51
|
+
def end_game(result)
|
52
|
+
self.state = :ended
|
53
|
+
@game_result = result
|
54
|
+
show_end_screen
|
55
|
+
restart
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check if current game session is active
|
59
|
+
def active? = state == :playing
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Print the boot screen
|
64
|
+
def boot; end
|
65
|
+
|
66
|
+
def setup_game; end
|
67
|
+
|
68
|
+
def show_end_screen
|
69
|
+
puts "Game Over! Result: #{@game_result}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def restart; end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../console/wait_utils"
|
4
|
+
require_relative "logics/display"
|
5
|
+
|
6
|
+
module ConsoleGame
|
7
|
+
module Chess
|
8
|
+
# The Board class handles the rendering of the chessboard
|
9
|
+
# @author Ancient Nimbus
|
10
|
+
class Board
|
11
|
+
include Console
|
12
|
+
include Display
|
13
|
+
|
14
|
+
attr_accessor :board_size, :board_side, :board_padding, :flip_board, :highlight
|
15
|
+
attr_reader :level, :type_hl, :alg_pos_hl
|
16
|
+
|
17
|
+
# @param level [Level] chess Level object
|
18
|
+
def initialize(level)
|
19
|
+
@level = level
|
20
|
+
display_configs
|
21
|
+
end
|
22
|
+
|
23
|
+
# Print before the chessboard
|
24
|
+
# @param keypath [String] TF keypath
|
25
|
+
# @param subs [Hash] `{ demo: ["some text", :red] }`
|
26
|
+
# def print_before_cb(keypath, sub)
|
27
|
+
# board.print_msg(board.s(keypath, sub))
|
28
|
+
# end
|
29
|
+
|
30
|
+
# Loading message
|
31
|
+
# @see WaitUtils #wait_msg
|
32
|
+
def loading_msg(...) = WaitUtils.wait_msg(...)
|
33
|
+
|
34
|
+
# Print after the chessboard
|
35
|
+
# @param keypath [String] TF keypath
|
36
|
+
# @param sub [Hash] `{ demo: ["some text", :red] }`
|
37
|
+
def print_after_cb(keypath, sub = {}) = print_msg(s(keypath, sub), pre: "⠗ ")
|
38
|
+
|
39
|
+
# Print turn
|
40
|
+
# @param event_msgs [Array<String>]
|
41
|
+
def print_turn(event_msgs = [""])
|
42
|
+
system("clear")
|
43
|
+
# print "\e[2J\e[H"
|
44
|
+
|
45
|
+
print_msg(*event_msgs, pre: "\n⠗ ") unless event_msgs.empty?
|
46
|
+
print_chessboard
|
47
|
+
level.event_msgs.clear
|
48
|
+
end
|
49
|
+
|
50
|
+
# Print the chessboard
|
51
|
+
def print_chessboard
|
52
|
+
puts "\n"
|
53
|
+
print_msg(*build_chessboard, pre: "".ljust(board_padding), clear: false)
|
54
|
+
puts "\n"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Enable & disable board flipping
|
58
|
+
def flip_setting
|
59
|
+
self.flip_board = !flip_board
|
60
|
+
print_chessboard
|
61
|
+
keypath = flip_board ? "cmd.board.flip_on" : "cmd.board.flip_off"
|
62
|
+
print_msg(s(keypath), pre: D_MSG[:gear_icon])
|
63
|
+
end
|
64
|
+
|
65
|
+
# Make board bigger or smaller
|
66
|
+
def adjust_board_size
|
67
|
+
self.board_size, self.board_padding = board_size == 1 ? BOARD[:b_size_l] : BOARD[:b_size_s]
|
68
|
+
print_chessboard
|
69
|
+
keypath = board_size == 1 ? "cmd.board.size1" : "cmd.board.size2"
|
70
|
+
print_msg(s(keypath), pre: D_MSG[:gear_icon])
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# Display configs
|
76
|
+
def display_configs
|
77
|
+
@board_size, @board_padding = BOARD[:b_size_s]
|
78
|
+
@flip_board = true
|
79
|
+
@board_side = :white
|
80
|
+
@highlight = THEME[:classic].slice(:icon, :highlight)
|
81
|
+
@type_hl, @alg_pos_hl = MSG_HIGHLIGHT[:std].values_at(:type, :alg_pos)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Pre-process turn data before sending it to display module
|
85
|
+
# @return [Array] 2D array respect to bound limit
|
86
|
+
def rendering_data
|
87
|
+
display_data = highlight_moves(level.turn_data.dup, level.active_piece)
|
88
|
+
to_matrix(display_data)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Temporary display move indicator highlight on the board
|
92
|
+
# @param display_data [Array<ChessPiece, String>] 1D copied of turn_data
|
93
|
+
# @param active_piece [King, Queen, Bishop, Knight, Rook, Pawn] chess piece
|
94
|
+
# @return [Array]
|
95
|
+
def highlight_moves(display_data, active_piece)
|
96
|
+
return display_data if active_piece.nil?
|
97
|
+
|
98
|
+
active_piece.possible_moves.each { |move| display_data[move] = highlight if display_data[move].is_a?(String) }
|
99
|
+
display_data
|
100
|
+
end
|
101
|
+
|
102
|
+
# Build the chessboard
|
103
|
+
# @return [Array<String>]
|
104
|
+
def build_chessboard
|
105
|
+
board_direction = flip_board ? level.player.side : board_side
|
106
|
+
build_board(rendering_data, side: board_direction, size: board_size)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "version"
|
4
|
+
require_relative "../base_game"
|
5
|
+
require_relative "player/chess_player"
|
6
|
+
require_relative "player/chess_computer"
|
7
|
+
require_relative "level"
|
8
|
+
require_relative "input/chess_input"
|
9
|
+
require_relative "logics/display"
|
10
|
+
require_relative "utilities/chess_utils"
|
11
|
+
require_relative "utilities/player_builder"
|
12
|
+
require_relative "utilities/session_builder"
|
13
|
+
require_relative "utilities/load_manager"
|
14
|
+
require_relative "utilities/fen_import"
|
15
|
+
|
16
|
+
module ConsoleGame
|
17
|
+
# The Chess module features all the working parts for the game Chess.
|
18
|
+
# @author Ancient Nimbus
|
19
|
+
# @version 0.9.0
|
20
|
+
module Chess
|
21
|
+
# Main game flow for the game Chess, a subclass of ConsoleGame::BaseGame
|
22
|
+
class Game < BaseGame
|
23
|
+
include ChessUtils
|
24
|
+
include Display
|
25
|
+
|
26
|
+
attr_reader :mode, :p1, :p2, :sides, :sessions, :level
|
27
|
+
attr_accessor :fen
|
28
|
+
|
29
|
+
# @param game_manager [GameManager]
|
30
|
+
# @param title [String]
|
31
|
+
def initialize(game_manager = nil, title = "Chess")
|
32
|
+
super(game_manager, title, ChessInput.new(game_manager, self), ver: VER)
|
33
|
+
user.profile[:appdata][:chess] ||= {}
|
34
|
+
@sessions = user.profile[:appdata][:chess]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Setup sequence
|
38
|
+
# new game or load game
|
39
|
+
def setup_game
|
40
|
+
reset_state
|
41
|
+
opt = game_selection
|
42
|
+
id = opt == 2 ? load_game : new_game(import: (opt == 3))
|
43
|
+
@fen ||= sessions.dig(id, :fens, -1)
|
44
|
+
@level = Level.new(controller, sides, sessions[id], fen).open_level
|
45
|
+
end_game
|
46
|
+
end
|
47
|
+
|
48
|
+
# Handle new game sequence
|
49
|
+
# @param err [Boolean] is use when there is a load err
|
50
|
+
# @param import [Boolean] true if opt is 3
|
51
|
+
def new_game(err: false, import: false)
|
52
|
+
print_msg(s("new.err")) if err
|
53
|
+
print_msg(s("new.f1"))
|
54
|
+
@mode = controller.ask(s("new.f1a"), err_msg: s("new.f1a_err"), reg: [1, 2], input_type: :range).to_i
|
55
|
+
@p1, @p2 = setup_players
|
56
|
+
start_order
|
57
|
+
import_game if import
|
58
|
+
create_session
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Import game mode
|
64
|
+
def import_game
|
65
|
+
print_msg(s("new.f3"), pre: "⠗ ")
|
66
|
+
@fen = controller.ask("FEN: ", input_type: :any)
|
67
|
+
end
|
68
|
+
|
69
|
+
# == Flow ==
|
70
|
+
|
71
|
+
# Game intro
|
72
|
+
def boot
|
73
|
+
tf_fetcher("", *%w[boot how_to help]).each do |msg|
|
74
|
+
print_msg(msg.sub("0.0.0", ver))
|
75
|
+
controller.ask(s("blanks.enter"), empty: true)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Prompt player for new game or load game
|
80
|
+
def game_selection
|
81
|
+
print_msg(s("load.f1"))
|
82
|
+
controller.ask(s("load.f1a"), err_msg: s("load.f1a_err"), reg: [1, 3], input_type: :range).to_i
|
83
|
+
end
|
84
|
+
|
85
|
+
# Handle load game sequence
|
86
|
+
def load_game
|
87
|
+
user_opt, session = select_session
|
88
|
+
@mode = session[:mode]
|
89
|
+
begin
|
90
|
+
@p1, @p2 = build_players(session)
|
91
|
+
rescue KeyError
|
92
|
+
print_msg(s("load.err"))
|
93
|
+
new_game(err: true)
|
94
|
+
end
|
95
|
+
assign_sides
|
96
|
+
user_opt
|
97
|
+
end
|
98
|
+
|
99
|
+
# Endgame handling
|
100
|
+
def end_game
|
101
|
+
self.state = :ended
|
102
|
+
opt = controller.ask(s("session.restart"), reg: COMMON_REG[:yesno], input_type: :custom)
|
103
|
+
setup_game if opt.downcase.include?("y")
|
104
|
+
end
|
105
|
+
|
106
|
+
# == Utilities ==
|
107
|
+
|
108
|
+
# Reset state
|
109
|
+
def reset_state
|
110
|
+
Player.player_count(0)
|
111
|
+
reset_config = { player_builder: nil, sides: {}, p1: setup_p1, p2: nil, fen: nil }
|
112
|
+
reset_config.each { |var, v| instance_variable_set("@#{var}", v) }
|
113
|
+
end
|
114
|
+
|
115
|
+
# Create new session data
|
116
|
+
# @return [Integer] session id
|
117
|
+
def create_session
|
118
|
+
id, session_data = SessionBuilder.build_session(self)
|
119
|
+
sessions[id] = p1.register_session(id, **session_data)
|
120
|
+
id
|
121
|
+
end
|
122
|
+
|
123
|
+
# Select game session from list of sessions
|
124
|
+
# @see LoadManager #select_session
|
125
|
+
def select_session = LoadManager.select_session(self)
|
126
|
+
|
127
|
+
# Setup players
|
128
|
+
def setup_players = [p1, p2].map { |player| player_profile(player) }
|
129
|
+
|
130
|
+
# Set up player profile
|
131
|
+
# @param player [ConsoleGame::ChessPlayer, nil]
|
132
|
+
# @return [ChessPlayer, ChessComputer]
|
133
|
+
def player_profile(player)
|
134
|
+
player ||= mode == 1 ? create_player("") : create_player("Computer", type: :ai)
|
135
|
+
return player if player.is_a?(ChessComputer)
|
136
|
+
|
137
|
+
# flow 2: name players
|
138
|
+
f2 = s("new.f2", { count: [Player.total_player], name: [player.name] })
|
139
|
+
player.edit_name(controller.ask(f2, reg: COMMON_REG[:filename], empty: true, input_type: :custom))
|
140
|
+
print_msg(s("new.f2a", { name: player.name }))
|
141
|
+
|
142
|
+
player
|
143
|
+
end
|
144
|
+
|
145
|
+
# Set start order
|
146
|
+
def start_order
|
147
|
+
f1, f1a, f1a_err = tf_fetcher("order", *%w[.f1 .f1a .f1a_err])
|
148
|
+
print_msg(f1)
|
149
|
+
opt = controller.ask(f1a, err_msg: f1a_err, reg: [1, 3], input_type: :range).to_i
|
150
|
+
opt = rand(1..2) if opt == 3
|
151
|
+
assign_sides(opt:)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Assign players to a sides hash
|
155
|
+
# @param opt [Integer] expects 1 or 2, where 1 will set p1 as white and p2 as black, and 2 in reverse
|
156
|
+
# @return [Hash<ChessPlayer, ChessComputer>]
|
157
|
+
def assign_sides(opt: 1)
|
158
|
+
validated_opt = p1.side ? determine_opt : opt
|
159
|
+
sides[w_sym], sides[b_sym] = validated_opt == 1 ? [p1, p2] : [p2, p1]
|
160
|
+
end
|
161
|
+
|
162
|
+
# Helper: determine side assignment option, usable only when p1.side is not nil
|
163
|
+
# @return [Integer]
|
164
|
+
def determine_opt = p1.side == w_sym ? 1 : 2
|
165
|
+
|
166
|
+
# == Player object creation ==
|
167
|
+
|
168
|
+
# Setup player 1
|
169
|
+
def setup_p1 = create_player(user.profile[:username])
|
170
|
+
|
171
|
+
# Create new player builder service
|
172
|
+
# @return [PlayerBuilder]
|
173
|
+
def player_builder = @player_builder ||= PlayerBuilder.new(self)
|
174
|
+
|
175
|
+
# Create players based on load mode
|
176
|
+
# @see PlayerBuilder #build_player
|
177
|
+
def build_players(...) = player_builder.build_players(...)
|
178
|
+
|
179
|
+
# Create a player
|
180
|
+
# @see PlayerBuilder #create_player
|
181
|
+
def create_player(...) = player_builder.create_player(...)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ConsoleGame
|
4
|
+
module Chess
|
5
|
+
# Module to parse Algebraic notation
|
6
|
+
module AlgebraicNotation
|
7
|
+
# Algebraic Input Regexp pattern
|
8
|
+
# keys:
|
9
|
+
# :pieces - Piece notations.
|
10
|
+
# :disambiguation - Useful when two (or more) identical pieces can move to the same square.
|
11
|
+
# :capture - Indicate the move is a capture.
|
12
|
+
# :destination - Indicate destination square.
|
13
|
+
# :promote - Pawn specific pattern, usable when Pawn reaches the other end of the board.
|
14
|
+
# :check - Optional check and checkmate indicator.
|
15
|
+
# :castling - King specific pattern usable when Castling move is possible.
|
16
|
+
# @return [Hash<Symbol, String>] patterns required to construct algebraic notation input.
|
17
|
+
ALG_PATTERN = {
|
18
|
+
pieces: "(?<piece>[KQRBN])?",
|
19
|
+
disambiguation: "(?<file_rank>[a-h][1-8]|[a-h])?",
|
20
|
+
capture: "(?<capture>x)?",
|
21
|
+
destination: "(?<target>[a-h][1-8])",
|
22
|
+
promote: "(?:=(?<promote>[QRBN]))?",
|
23
|
+
check: "(?<check>[+#])?",
|
24
|
+
castling: "(?<castle>O-O(?:-O)?)"
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# == Algebraic notation ==
|
30
|
+
|
31
|
+
# Input validation when input scheme is set to Algebraic notation
|
32
|
+
# @param input [String] input value from prompt
|
33
|
+
# @param side [Symbol] player side :white or :black
|
34
|
+
# @param reg [String] regexp pattern
|
35
|
+
# @return [Hash] a command pattern hash
|
36
|
+
def validate_algebraic(input, side, reg)
|
37
|
+
captures = alg_output_capture_gps(input, reg)
|
38
|
+
return { type: :invalid_input, args: [input] } unless captures
|
39
|
+
|
40
|
+
if captures[:castle]
|
41
|
+
parse_castling(side, captures[:castle])
|
42
|
+
elsif captures[:promote]
|
43
|
+
parse_promote(side, captures)
|
44
|
+
else
|
45
|
+
parse_move(side, captures)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Helper: Process regexp and returns a named capture groups
|
50
|
+
# @param input [String] input value from prompt
|
51
|
+
# @param reg [String] regexp pattern
|
52
|
+
# @return [Hash]
|
53
|
+
def alg_output_capture_gps(input, reg) = input.match(reg)&.named_captures(symbolize_names: true)&.compact
|
54
|
+
|
55
|
+
# Helper: parse castling input
|
56
|
+
# @param side [Symbol] player side :white or :black
|
57
|
+
# @param castle [String]
|
58
|
+
# @return [Hash] a command pattern hash
|
59
|
+
def parse_castling(side, castle)
|
60
|
+
rank = side == :white ? "1" : "8"
|
61
|
+
new_file = castle == "O-O" ? "g" : "c"
|
62
|
+
{ type: :direct_move, args: ["e#{rank}", "#{new_file}#{rank}"] }
|
63
|
+
end
|
64
|
+
|
65
|
+
# Helper: parse pawn promote & capture
|
66
|
+
# @param side [Symbol] player side :white or :black
|
67
|
+
# @param captures [hash]
|
68
|
+
# @return [Hash] a command pattern hash
|
69
|
+
def parse_promote(side, captures)
|
70
|
+
target, promote = captures.slice(:target, :promote).values
|
71
|
+
rank = side == :white ? "7" : "2"
|
72
|
+
file = captures[:file_rank] || captures[:target][0]
|
73
|
+
|
74
|
+
{ type: :direct_promote, args: ["#{file}#{rank}", target, notation_to_sym(promote)] }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Helper: parse pawn movement
|
78
|
+
# @param side [Symbol] player side :white or :black
|
79
|
+
# @param captures [hash]
|
80
|
+
# @return [Hash] a command pattern hash
|
81
|
+
def parse_move(side, captures)
|
82
|
+
piece_type = notation_to_sym(captures[:piece] || :p)
|
83
|
+
|
84
|
+
{ type: :fetch_and_move, args: [side, piece_type, captures[:target], captures[:file_rank]].compact }
|
85
|
+
end
|
86
|
+
|
87
|
+
# == Utilities ==
|
88
|
+
|
89
|
+
# Algebraic Regexp pattern builder
|
90
|
+
# @return [String]
|
91
|
+
def regexp_algebraic
|
92
|
+
castling_gp = ALG_PATTERN.select { |k, _| k == :castling }.values.join
|
93
|
+
regular_gp = ALG_PATTERN.reject { |k, _| k == :castling }.values.join
|
94
|
+
"(#{[castling_gp, regular_gp].join('|')})"
|
95
|
+
end
|
96
|
+
|
97
|
+
# Helper: Convert algebraic notation to internal symbol
|
98
|
+
# @param notation [String]
|
99
|
+
# @return [Symbol]
|
100
|
+
def notation_to_sym(notation) = notation.is_a?(Symbol) ? notation : notation.downcase.to_sym
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../input"
|
4
|
+
require_relative "../logics/display"
|
5
|
+
require_relative "../utilities/chess_utils"
|
6
|
+
require_relative "../utilities/pgn_export"
|
7
|
+
require_relative "smith_notation"
|
8
|
+
require_relative "algebraic_notation"
|
9
|
+
|
10
|
+
module ConsoleGame
|
11
|
+
module Chess
|
12
|
+
# Input controller for the game Chess
|
13
|
+
class ChessInput < Input
|
14
|
+
include ChessUtils
|
15
|
+
include Display
|
16
|
+
include SmithNotation
|
17
|
+
include AlgebraicNotation
|
18
|
+
|
19
|
+
attr_accessor :input_scheme
|
20
|
+
attr_reader :alg_reg, :smith_reg, :level, :active_side, :chess_manager
|
21
|
+
|
22
|
+
# @param game_manager [GameManager]
|
23
|
+
# @param chess_manager [Game]
|
24
|
+
def initialize(game_manager = nil, chess_manager = nil)
|
25
|
+
super(game_manager)
|
26
|
+
@chess_manager = chess_manager
|
27
|
+
notation_patterns_builder
|
28
|
+
@input_scheme = smith_reg
|
29
|
+
end
|
30
|
+
|
31
|
+
# Store active level object
|
32
|
+
# @param level [Chess::Level] expects a chess Level class object
|
33
|
+
def link_level(level) = @level = level
|
34
|
+
|
35
|
+
# == Core methods ==
|
36
|
+
|
37
|
+
# Get user input and process them accordingly
|
38
|
+
# @param player [ChessPlayer]
|
39
|
+
def turn_action(player)
|
40
|
+
input = ask(s("level.action1"), reg: input_scheme, input_type: :custom, empty: true)
|
41
|
+
ops = case input_scheme
|
42
|
+
when smith_reg then validate_smith(input)
|
43
|
+
when alg_reg then validate_algebraic(input, player.side, input_scheme)
|
44
|
+
end
|
45
|
+
turn_action(player) unless player.method(ops[:type]).call(*ops[:args])
|
46
|
+
end
|
47
|
+
|
48
|
+
# Prompt user for the second time in the same turn if the first prompt was a preview move event
|
49
|
+
# @param player [ChessPlayer]
|
50
|
+
def make_a_move(player)
|
51
|
+
input = ask(s("level.action2"), reg: SMITH_PATTERN[:gp1], input_type: :custom, empty: true)
|
52
|
+
ops = case input.scan(SMITH_PARSER)
|
53
|
+
in [new_pos] then { type: :move_piece, args: [new_pos] }
|
54
|
+
else { type: :invalid_input, args: [input] }
|
55
|
+
end
|
56
|
+
make_a_move(player) unless player.method(ops[:type]).call(*ops[:args])
|
57
|
+
end
|
58
|
+
|
59
|
+
# Prompt user for Pawn promotion option when notation for promotion is not provided at the previous prompt
|
60
|
+
def promote_a_pawn
|
61
|
+
ask(s("level.promo_alert"), err_msg: s("level.err.promo2"), reg: SMITH_PATTERN[:promotion], input_type: :custom)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Chess Override: process user input
|
65
|
+
# @param msg [String] first print
|
66
|
+
# @param cmds [Hash] expects a list of commands hash
|
67
|
+
# @param err_msg [String] second print
|
68
|
+
# @param reg [Regexp, String, Array<String>] pattern to match, use an Array when input type is :range
|
69
|
+
# @param empty [Boolean] allow empty input value, default to false
|
70
|
+
# @param input_type [Symbol] expects the following option: :any, :range, :custom
|
71
|
+
# @return [String]
|
72
|
+
def ask(msg = "", cmds: commands, err_msg: s("cmd.std_err"), reg: ".*", empty: false, input_type: :any) = super
|
73
|
+
|
74
|
+
# == Console Commands ==
|
75
|
+
|
76
|
+
# Exit sequences | command patterns: `exit`
|
77
|
+
def quit(_args = [])
|
78
|
+
print_msg(s("cmd.exit"))
|
79
|
+
save_moves unless level.nil?
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
83
|
+
# Display help string | command pattern: `help`
|
84
|
+
def help(args = [])
|
85
|
+
keypath = case args
|
86
|
+
in ["alg"] then "alg_h"
|
87
|
+
in ["smith"] then "sm_h"
|
88
|
+
else "help"
|
89
|
+
end
|
90
|
+
print_msg(s(keypath))
|
91
|
+
end
|
92
|
+
|
93
|
+
# Display system info | command pattern: `info`
|
94
|
+
def info(_args = [])
|
95
|
+
str = level.nil? ? s("cmd.info", { ver: chess_manager.ver }) : s("cmd.info2", build_info_data)
|
96
|
+
print_msg(str)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Save session to player data | command pattern: `save`
|
100
|
+
# @param mute [Boolean] bypass printing when use at the background
|
101
|
+
def save(_args = [], mute: false)
|
102
|
+
return cmd_disabled if level.nil?
|
103
|
+
|
104
|
+
level.session[:date] = Time.new.ceil.strftime(STR_TIME)
|
105
|
+
game_manager.save_user_profile(mute:)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Load another session from player data | command pattern: `load`
|
109
|
+
def load(_args = [])
|
110
|
+
return cmd_disabled if level.nil?
|
111
|
+
|
112
|
+
@level = nil
|
113
|
+
print_msg(s("cmd.load"), pre: "⠗ ")
|
114
|
+
chess_manager.setup_game
|
115
|
+
end
|
116
|
+
|
117
|
+
# Export current game session as pgn file | command pattern: `export`
|
118
|
+
def export(_args = [])
|
119
|
+
return cmd_disabled if level.nil?
|
120
|
+
|
121
|
+
save_moves
|
122
|
+
dir, filename, pgn_out = PgnExport.export_session(level.session).values_at(:path, :filename, :export_data)
|
123
|
+
print_msg(s("cmd.export", {
|
124
|
+
filename: [filename, "gold"], dir: [dir, "gold"], sep: ["PGN".center(80, "=")], pgn_out:
|
125
|
+
}))
|
126
|
+
end
|
127
|
+
|
128
|
+
# Change input mode to detect Smith Notation | command pattern: `smith`
|
129
|
+
def smith(_args = []) = switch_notation(:smith)
|
130
|
+
|
131
|
+
# Change input mode to detect Algebraic Notation | command pattern: `alg`
|
132
|
+
def alg(_args = []) = switch_notation(:alg)
|
133
|
+
|
134
|
+
# Update board settings | command pattern: `board`
|
135
|
+
# @example usage example
|
136
|
+
# `--board size`
|
137
|
+
# `--board flip`
|
138
|
+
def board(args = [])
|
139
|
+
return cmd_disabled if level.nil?
|
140
|
+
|
141
|
+
case args
|
142
|
+
in ["size"] then level.board.adjust_board_size
|
143
|
+
in ["flip"] then level.board.flip_setting
|
144
|
+
else print_msg(s("cmd.err"), pre: D_MSG[:warn_prefix])
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
# == Utilities ==
|
151
|
+
|
152
|
+
# Switch notation depending on user input
|
153
|
+
# @param mode [Symbol] expects :smith or :alg
|
154
|
+
def switch_notation(mode)
|
155
|
+
return cmd_disabled if level.nil?
|
156
|
+
|
157
|
+
self.input_scheme = case mode
|
158
|
+
when :smith then smith_reg
|
159
|
+
when :alg then alg_reg
|
160
|
+
end
|
161
|
+
print_msg(s("cmd.input.#{mode}"), pre: "⠗ ")
|
162
|
+
end
|
163
|
+
|
164
|
+
# Create regexp patterns for various input modes
|
165
|
+
def notation_patterns_builder
|
166
|
+
@alg_reg = regexp_algebraic
|
167
|
+
@smith_reg = regexp_smith
|
168
|
+
end
|
169
|
+
|
170
|
+
# Setup input commands
|
171
|
+
def setup_commands = super.merge({ "save" => method(:save), "load" => method(:load), "export" => method(:export),
|
172
|
+
"smith" => method(:smith), "alg" => method(:alg), "board" => method(:board) })
|
173
|
+
|
174
|
+
# Print command is disabled at this stage
|
175
|
+
def cmd_disabled = print_msg(s("cmd.disabled"), pre: D_MSG[:warn_prefix])
|
176
|
+
|
177
|
+
# Helper to build session info data
|
178
|
+
# @return [Hash]
|
179
|
+
def build_info_data
|
180
|
+
date, fens, event, white, black = level.session.values_at(:date, :fens, :event, :white, :black)
|
181
|
+
{ date:, fen: fens.last, event:, w_player: white, b_player: black, ver: chess_manager.ver }
|
182
|
+
end
|
183
|
+
|
184
|
+
# Helper to save session moves data
|
185
|
+
def save_moves
|
186
|
+
level.update_session_moves
|
187
|
+
save(mute: true)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|