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