chess_tui 0.41.4

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.
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Chess
4
+ module Chess
5
+ # Game class
6
+ class Game
7
+ include Display
8
+ include Mouse
9
+ include Save
10
+ include ValidMoves
11
+ include WinAndDraw
12
+
13
+ def initialize(player = Chess::Player)
14
+ @white_player = player.new
15
+ @black_player = player.new
16
+ end
17
+
18
+ # select different actions based on what was clicked
19
+ # @param board [Chess::Board]
20
+ # @param clicked [String]
21
+ # @param board_pos [Array]
22
+ # @param mouse_coord [Array]
23
+ # @return [nil,Integer] nil or Exit Code 0
24
+ def select_click_action(board, clicked, board_pos, mouse_coord)
25
+ return if clicked == 'outside'
26
+
27
+ if clicked == 'board'
28
+ board_action(board, board_pos)
29
+ else
30
+ button_action(board, mouse_coord)
31
+ end
32
+ end
33
+
34
+ # actions for clicks on board
35
+ # @param board [Chess:Board]
36
+ # @param board_pos [Array]
37
+ # @return [nil,Integer] nil or Exit Code 0
38
+ def board_action(board, board_pos)
39
+ player = if board.current_player == 'w'
40
+ @white_player
41
+ else
42
+ @black_player
43
+ end
44
+ update_castling_rights(board)
45
+ valid_moves = player_turn(player, board, board_pos)
46
+ update_king_status(board)
47
+ redraw_display(board.data, valid_moves)
48
+ detect_win_or_draws(board)
49
+ end
50
+
51
+ # actions for clicks on buttons
52
+ # @param board [Chess::Board]
53
+ # @param mouse_coord [Array]
54
+ # @return [nil,Integer] nil or Exit Code 0
55
+ def button_action(board, mouse_coord)
56
+ type = button_type(mouse_coord)
57
+ if type == 'save&exit'
58
+ save_and_exit(board)
59
+ else
60
+ game_exit
61
+ end
62
+ end
63
+
64
+ # player turn to select move
65
+ # @param player [Chess::Player]
66
+ # @param board [Chess::Board]
67
+ # @param board_pos [Array]
68
+ # @return [void]
69
+ def player_turn(player, board, board_pos)
70
+ piece = board.piece_at(*board_pos)
71
+ if player.selected_piece == '' || same_color?(board.current_player, piece)
72
+ select_piece(piece, player, board)
73
+ else
74
+ select_move(player, board, board_pos)
75
+ []
76
+ end
77
+ end
78
+
79
+ # player turn to select move
80
+ # @param piece any subclass of {Chess::Pieces::Piece}
81
+ # @param player [Chess::Player]
82
+ # @param board [Chess::Board]
83
+ # @return [void]
84
+ def select_piece(piece, player, board)
85
+ player.selected_piece = piece if same_color?(board.current_player, piece)
86
+ if player.selected_piece == ''
87
+ []
88
+ else
89
+ player.selected_piece.valid_moves = valid_moves(piece, board)
90
+ end
91
+ end
92
+
93
+ # selects move to play
94
+ #
95
+ # @param player [Chess::Player]
96
+ # @param board [Chess::Board]
97
+ # @param move_pos [Array]
98
+ # @return [void]
99
+ def select_move(player, board, move_pos)
100
+ player.select_move(board, move_pos)
101
+ player.selected_piece.valid_moves = []
102
+ player.selected_piece = ''
103
+ end
104
+
105
+ # checks if current_player and piece have same color
106
+ #
107
+ # @param current_player [String] from {Chess::Board#current_player}
108
+ # @param piece any subclass of {Chess::Pieces::Piece}
109
+ def same_color?(current_player, piece)
110
+ piece.color.chr == current_player unless piece == ''
111
+ end
112
+
113
+ # updates king's @in_check attribute
114
+ #
115
+ # @param board [Chess::Board]
116
+ # @return [void]
117
+ def update_king_status(board)
118
+ current_player = board.current_player
119
+ board.current_player = 'b'
120
+ board.white_king.in_check?(board, board.black_pieces)
121
+ board.current_player = 'w'
122
+ board.black_king.in_check?(board, board.white_pieces)
123
+ board.current_player = current_player
124
+ end
125
+
126
+ # updates castling_rights for board
127
+ #
128
+ # @param board [Chess::Board]
129
+ # @return [void]
130
+ def update_castling_rights(board)
131
+ return if board.castling_rights == ''
132
+
133
+ location_piece_color_and_rights = {
134
+ h0: [Chess::Pieces::Rook, 'w', 'K'],
135
+ a0: [Chess::Pieces::Rook, 'w', 'Q'],
136
+ e0: [Chess::Pieces::King, 'w', 'KQ'],
137
+ h7: [Chess::Pieces::Rook, 'b', 'k'],
138
+ a7: [Chess::Pieces::Rook, 'b', 'q'],
139
+ e7: [Chess::Pieces::King, 'b', 'kq']
140
+ }
141
+ update_castling_rights_for_each_pos(board, location_piece_color_and_rights)
142
+ end
143
+
144
+ # helper method for {#update_castling_rights}
145
+ #
146
+ # @param board [Chess::Board]
147
+ # @param location_piece_color_and_rights [Hash]
148
+ # @return [void]
149
+ def update_castling_rights_for_each_pos(board, location_piece_color_and_rights) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
150
+ location_piece_color_and_rights.each_key do |pos|
151
+ type = location_piece_color_and_rights[pos].first
152
+ color = location_piece_color_and_rights[pos][1]
153
+ rights = location_piece_color_and_rights[pos].last
154
+ position = pos.to_s.chars
155
+ file = position.first
156
+ rank = position.last.to_i
157
+ piece = board.piece_at(file, rank)
158
+ rights = rights.chars
159
+ rights.each do |right|
160
+ board.castling_rights.sub!(right, '') unless piece.is_a?(type) && same_color?(color, piece)
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Chess
4
+ module Chess
5
+ # ValidMoves
6
+ module ValidMoves
7
+ # find valid_moves for given piece on the given board
8
+ #
9
+ # @param piece any subclass of {Chess::Pieces::Piece}
10
+ # @param board [Chess::Board]
11
+ # @return [Array] valid_moves_arr
12
+ def valid_moves(piece, board)
13
+ possible_moves = piece.possible_moves(board)
14
+ new_player = Player.new
15
+ fen_code = generate_fen_code(board)
16
+
17
+ if piece.is_a?(Pieces::King)
18
+ valid_moves_for_king(board, piece, possible_moves, fen_code, new_player)
19
+ else
20
+ valid_moves_from_possible_moves(piece, possible_moves, fen_code, new_player)
21
+ end
22
+ end
23
+
24
+ # find valid_moves from possible_moves
25
+ #
26
+ # @param piece any subclass of {Chess::Pieces::Piece}
27
+ # @param possible_moves [Array]
28
+ # @param fen_code [String]
29
+ # @param new_player [Chess::Player]
30
+ # @return [Array] valid_moves_arr
31
+ def valid_moves_from_possible_moves(piece, possible_moves, fen_code, new_player)
32
+ valid_moves_arr = []
33
+ possible_moves.each do |move|
34
+ invalid = invalid_normal_move?(move, piece, fen_code, new_player)
35
+ valid_moves_arr << move unless invalid
36
+ end
37
+ valid_moves_arr
38
+ end
39
+
40
+ # check if given move is invalid
41
+ #
42
+ # @param move [Array]
43
+ # @param piece any subclass of {Chess::Pieces::Piece}
44
+ # @param fen_code [String]
45
+ # @param new_player [Chess::Player]
46
+ def invalid_normal_move?(move, piece, fen_code, new_player)
47
+ new_board = Chess::Board.new(fen_code)
48
+ new_player.selected_piece = new_board.piece_at(*piece.pos)
49
+ new_player.play_move_by_type(new_player.selected_piece, new_board, move, inside_valid_moves_flag: true)
50
+ king_comes_in_check?(piece, new_board)
51
+ end
52
+
53
+ # find valid_moves for given king on the given board
54
+ #
55
+ # @param board [Chess::Board]
56
+ # @param piece any subclass of {Chess::Pieces::Piece}
57
+ # @param possible_moves [Array]
58
+ # @param fen_code [String]
59
+ # @param new_player [Chess::Player]
60
+ # @return [Array] valid_moves_arr
61
+ def valid_moves_for_king(board, piece, possible_moves, fen_code, new_player) # rubocop:disable Metrics/MethodLength
62
+ valid_moves_arr = []
63
+ castling_moves = piece.castling_moves(board)
64
+ invalid = false
65
+ possible_moves.each do |move|
66
+ invalid = if castling_moves.include?(move)
67
+ invalid_castling_move?(move, piece, fen_code, new_player)
68
+ else
69
+ invalid_normal_move?(move, piece, fen_code, new_player)
70
+ end
71
+ valid_moves_arr << move unless invalid
72
+ end
73
+ valid_moves_arr
74
+ end
75
+
76
+ # Check if given castling move is invalid
77
+ #
78
+ # @param move [Array]
79
+ # @param piece any subclass of {Chess::Pieces::Piece}
80
+ # @param fen_code [String]
81
+ # @param new_player [Chess::Player]
82
+ def invalid_castling_move?(move, piece, fen_code, new_player)
83
+ return true if piece.in_check
84
+
85
+ file = move.first
86
+ if file.downcase == 'c'
87
+ invalid_queen_side_castle?(move, piece, fen_code, new_player)
88
+ elsif file.downcase == 'g'
89
+ invalid_king_side_castle?(move, piece, fen_code, new_player)
90
+ end
91
+ end
92
+
93
+ # helper method for {#invalid_castling_move?}
94
+ # @param (see #invalid_castling_move?)
95
+ def invalid_queen_side_castle?(move, piece, fen_code, new_player)
96
+ comes_in_check = castling_king_passes_check?(move, piece, fen_code, new_player)
97
+ move = ['d', move.last]
98
+ passes_check = castling_king_passes_check?(move, piece, fen_code, new_player)
99
+ passes_check || comes_in_check
100
+ end
101
+
102
+ # helper method for {#invalid_castling_move?}
103
+ # @param (see #invalid_castling_move?)
104
+ def invalid_king_side_castle?(move, piece, fen_code, new_player)
105
+ comes_in_check = castling_king_passes_check?(move, piece, fen_code, new_player)
106
+ move = ['f', move.last]
107
+ passes_check = castling_king_passes_check?(move, piece, fen_code, new_player)
108
+ passes_check || comes_in_check
109
+ end
110
+
111
+ # helper method for {#invalid_castling_move?}
112
+ # @param (see #invalid_castling_move?)
113
+ def castling_king_passes_check?(move, piece, fen_code, new_player)
114
+ new_board = Chess::Board.new(fen_code)
115
+ new_player.selected_piece = new_board.piece_at(*piece.pos)
116
+ new_player.play_move_by_type(new_player.selected_piece, new_board, move, inside_valid_moves_flag: true)
117
+ king_comes_in_check?(piece, new_board)
118
+ end
119
+
120
+ # Checks if king comes in check
121
+ # @param piece any subclass of {Chess::Pieces::Piece}
122
+ # @param new_board new board obj of [Chess::Board]
123
+ def king_comes_in_check?(piece, new_board)
124
+ if piece.white?
125
+ new_board.white_king.in_check?(new_board, new_board.black_pieces)
126
+ else
127
+ new_board.black_king.in_check?(new_board, new_board.white_pieces)
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Chess
4
+ module Chess
5
+ # WinAndDraw
6
+ module WinAndDraw
7
+ # detects wins or draws and exits the game
8
+ #
9
+ # @param board [Chess::Board]
10
+ # @return [nil,Integer] nil or Exit Code 0
11
+ def detect_win_or_draws(board) # rubocop:disable Metrics/MethodLength
12
+ if checkmate?(board)
13
+ puts 'Checkmate!'
14
+ return game_exit
15
+ end
16
+
17
+ if stalemate?(board)
18
+ puts 'Stalemate!'
19
+ return game_exit
20
+ end
21
+
22
+ return unless fifty_move_draw?(board)
23
+
24
+ puts 'draw'
25
+ game_exit
26
+ end
27
+
28
+ # check if player is in checkmate
29
+ #
30
+ # @param board [Chess::Board]
31
+ def checkmate?(board)
32
+ in_check = if board.current_player == 'w'
33
+ board.white_king.in_check
34
+ else
35
+ board.black_king.in_check
36
+ end
37
+ return false unless in_check
38
+
39
+ return false if any_legal_move?(board)
40
+
41
+ true
42
+ end
43
+
44
+ # check if player is in stalemate
45
+ #
46
+ # @param board [Chess::Board]
47
+ def stalemate?(board)
48
+ in_check = if board.current_player == 'w'
49
+ board.white_king.in_check
50
+ else
51
+ board.black_king.in_check
52
+ end
53
+ return false if in_check
54
+
55
+ return false if any_legal_move?(board)
56
+
57
+ true
58
+ end
59
+
60
+ # check if any legal move available
61
+ #
62
+ # @param board [Chess::Board]
63
+ def any_legal_move?(board)
64
+ pieces = if board.current_player == 'w'
65
+ board.white_pieces
66
+ else
67
+ board.black_pieces
68
+ end
69
+ total_moves = []
70
+ pieces.each do |piece|
71
+ total_moves += valid_moves(piece, board)
72
+ end
73
+ total_moves != []
74
+ end
75
+
76
+ # check if there's a fifty_move draw (100 half moves)
77
+ #
78
+ # @param board [Chess::Board]
79
+ def fifty_move_draw?(board)
80
+ board.half_move >= 100
81
+ end
82
+
83
+ # save the game
84
+ #
85
+ # @param board [Chess::Board]
86
+ def save_game(board)
87
+ save_name = prompt_save_name
88
+ fen = generate_fen_code(board)
89
+ save(save_name, fen)
90
+ end
91
+
92
+ # exits the game
93
+ # @return [Integer] Exit Code 0
94
+ def game_exit
95
+ pp 'exiting...'
96
+ 0
97
+ end
98
+
99
+ # save and exit the game
100
+ # @return [Integer] Exit Code 0
101
+ def save_and_exit(board)
102
+ save_game(board)
103
+ game_exit
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Chess
4
+ module Chess
5
+ # Mouse
6
+ module Mouse
7
+ # starts listening mouse input in console
8
+ # @param board [Chess::Board]
9
+ def start_mouse_input(board)
10
+ warn_tmux_users if ENV['TMUX']
11
+ system('stty -icanon -echo') # Disable canonical mode and echo in terminal
12
+ enable_mouse_tracking
13
+ begin
14
+ puts 'Waiting for mouse click... Press Ctrl+C to quit.'
15
+ input_loop(board)
16
+ ensure # run these even if Ctrl+C was pressed
17
+ disable_mouse_tracking
18
+ system('stty icanon echo') # Restore terminal to sane mode
19
+ end
20
+ end
21
+
22
+ # warns tmux users that mouse may not work
23
+ def warn_tmux_users
24
+ warn 'Mouse input may not work as expected in tmux'
25
+ end
26
+
27
+ # enable the mouse tracking using xterm control sequences -
28
+ # https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
29
+ def enable_mouse_tracking
30
+ print "\e[?9h" # Enable X10 mouse tracking
31
+ end
32
+
33
+ # disable the mouse tracking using xterm control sequences -
34
+ # https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
35
+ def disable_mouse_tracking
36
+ print "\e[?9l" # Disable X10 mouse tracking
37
+ end
38
+
39
+ # runs loop for mouse input
40
+ # @param board [Chess::Board]
41
+ def input_loop(board) # rubocop:disable Metrics/MethodLength
42
+ file_coords = generate_file_coords
43
+ rank_coords = generate_rank_coords
44
+ loop do
45
+ char = $stdin.getc
46
+ next unless char == "\e"
47
+
48
+ coord = read_input(char)
49
+ board_pos = clicked_element(coord, file_coords, rank_coords)
50
+ clicked = read_clicked(board_pos, coord)
51
+ return_value = select_click_action(board, clicked, board_pos, coord)
52
+ break if game_exit?(return_value)
53
+ end
54
+ end
55
+
56
+ # reads whole input sequence and returns the coords X and Y
57
+ # @param char [Char]
58
+ # @return [Array(x,y)]
59
+ def read_input(char)
60
+ shift = 32
61
+ sequence = char + $stdin.read(5) # Read the full ESC [ M SPACE Cx Cy stored in buffer
62
+ sequence.bytes[4..].map { |e| e - shift } # shift and return the Cx Cy only
63
+ end
64
+
65
+ # read what was clicked
66
+ # @example file and rank are decomposed from array
67
+ # read_clicked([file,rank],coord)
68
+ #
69
+ # @param coord [Array]
70
+ # @return [String]
71
+ def read_clicked((file, rank), coord)
72
+ if !(file.nil? || rank.nil?)
73
+ 'board'
74
+ elsif buttons_touched?(coord)
75
+ 'button'
76
+ else
77
+ 'outside'
78
+ end
79
+ end
80
+
81
+ # checks if exit code 0 is returned
82
+ # @param value [Integer,nil]
83
+ def game_exit?(value)
84
+ value == 0 # rubocop:disable Style/NumericPredicate
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Chess
4
+ module Chess
5
+ # Mouse
6
+ module Mouse
7
+ # returns the position of board where mouse clicked on console display
8
+ # @example x and y are decomposed from array
9
+ # clicked_element([3,2],file_coords,rank_coords)
10
+ #
11
+ # @param file_coords [Hash]
12
+ # @param rank_coords [Hash]
13
+ # @return [Array(file,rank)]
14
+ def clicked_element((x, y), file_coords, rank_coords)
15
+ [file_coords[x], rank_coords[y]]
16
+ end
17
+
18
+ # generate file coords for the display
19
+ # @return [Hash] file_coords
20
+ def generate_file_coords
21
+ file_coords = {}
22
+ file_ord = 97 # ord for "a"
23
+ board_start_x = 2
24
+ board_end_x = 25
25
+ (board_start_x..board_end_x).each_slice(3) do |arr| # a board square's string length on display is 3.
26
+ arr.each { |elem| file_coords[elem] = file_ord.chr }
27
+ file_ord += 1
28
+ end
29
+ file_coords
30
+ end
31
+
32
+ # generate rank coord for the display
33
+ # @return [Hash] rank_coords
34
+ def generate_rank_coords
35
+ rank_coords = {}
36
+ rank = 7 # black is on upper side of board so ranks go from 8 to 1 (7 to 0 cuz index start = 0) from top to bottom
37
+ board_start_y = 2
38
+ board_end_y = 9
39
+ (board_start_y..board_end_y).each do |elem|
40
+ rank_coords[elem] = rank
41
+ rank -= 1
42
+ end
43
+ rank_coords
44
+ end
45
+
46
+ # checks if a button was touched
47
+ # @example x and y are decomposed from array
48
+ # buttons_touched?([x,y])
49
+ def buttons_touched?((coord_x, coord_y))
50
+ y = 11
51
+ return false if coord_y != y
52
+
53
+ start_x = 5
54
+ end_x = 15
55
+ save_and_exit = Array(start_x..end_x)
56
+ start_x = 19
57
+ end_x = 22
58
+ exit = Array(start_x..end_x)
59
+ (save_and_exit + exit).include?(coord_x)
60
+ end
61
+
62
+ # returns which button was clicked
63
+ # @example x and y are decomposed from array
64
+ # button_type([x,y])
65
+ # @return [String]
66
+ def button_type((x, _y))
67
+ if x.between?(5, 15)
68
+ 'save&exit'
69
+ else
70
+ 'game_exit'
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Chess
4
+ module Chess
5
+ # Pieces
6
+ module Pieces
7
+ # BishopMoves
8
+ module BishopMoves
9
+ def north_west_moves(board, file, rank)
10
+ north_west = board.north_west_pos(file, rank)
11
+
12
+ moves = []
13
+ while board.empty_at?(*north_west)
14
+ moves << north_west if board.pos_in_range?(north_west)
15
+ north_west = board.north_west_pos(*north_west)
16
+ end
17
+ moves << north_west if board.pos_in_range?(north_west) && board.enemy_at?(*north_west)
18
+ moves
19
+ end
20
+
21
+ def south_west_moves(board, file, rank)
22
+ south_west = board.south_west_pos(file, rank)
23
+
24
+ moves = []
25
+ while board.empty_at?(*south_west)
26
+ moves << south_west if board.pos_in_range?(south_west)
27
+ south_west = board.south_west_pos(*south_west)
28
+ end
29
+ moves << south_west if board.pos_in_range?(south_west) && board.enemy_at?(*south_west)
30
+ moves
31
+ end
32
+
33
+ def north_east_moves(board, file, rank)
34
+ north_east = board.north_east_pos(file, rank)
35
+
36
+ moves = []
37
+ while board.empty_at?(*north_east)
38
+ moves << north_east if board.pos_in_range?(north_east)
39
+ north_east = board.north_east_pos(*north_east)
40
+ end
41
+ moves << north_east if board.pos_in_range?(north_east) && board.enemy_at?(*north_east)
42
+ moves
43
+ end
44
+
45
+ def south_east_moves(board, file, rank)
46
+ south_east = board.south_east_pos(file, rank)
47
+
48
+ moves = []
49
+ while board.empty_at?(*south_east)
50
+ moves << south_east if board.pos_in_range?(south_east)
51
+ south_east = board.south_east_pos(*south_east)
52
+ end
53
+ moves << south_east if board.pos_in_range?(south_east) && board.enemy_at?(*south_east)
54
+ moves
55
+ end
56
+ end
57
+
58
+ # Bishop
59
+ class Bishop < Piece
60
+ include BishopMoves
61
+
62
+ # all possible_moves of Bishop
63
+ #
64
+ # @param board [Chess::Board]
65
+ # @return [Array] possible_moves_arr
66
+ def possible_moves(board)
67
+ file = @pos[0]
68
+ rank = @pos[1]
69
+ moves = []
70
+ moves += north_west_moves(board, file, rank)
71
+ moves += north_east_moves(board, file, rank)
72
+ moves += south_west_moves(board, file, rank)
73
+ moves += south_east_moves(board, file, rank)
74
+ moves
75
+ end
76
+ end
77
+ end
78
+ end