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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4ffa182999d0b42056132dfbd562068e4761d5a1f90b66351a9dc04b280c5c0a
4
+ data.tar.gz: 719c2048abb1e1826ab7abb1b389a5373bc7751b3f6ddbb0fad22abc5ab22e62
5
+ SHA512:
6
+ metadata.gz: c555796aa4b5dd6a116174465ec178be2993e747663db5a7fd339f420e64201c5b2cf2c9d2224d8f32fd910d03bc7e252dfc76ff43e846a746f46106249367b8
7
+ data.tar.gz: 1de74e7fb8aab8a626f228250d84c4065608f5b6a422127d54438dfc496ae11f8e79cfc1e86f8feb126f37606f8cc058dcbd2fdc4c078e8159913f56ac93ba5c
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Chess
2
+
3
+ <!-- uses shields.io for tags and simpleicons.org for icons -->
4
+ [![Version](https://img.shields.io/github/v/tag/xajx179/Chess?label=version&logo=lichess)](https://github.com/XAJX179/Chess/tags)
5
+ ![last-commit](https://img.shields.io/github/last-commit/XAJX179/Chess?logo=git&label=Last%20Commit)
6
+ [![Documentation](https://github.com/XAJX179/Chess/actions/workflows/documentation.yml/badge.svg)](https://github.com/XAJX179/Chess/actions/workflows/documentation.yml)
7
+ [![Ruby Test](https://github.com/XAJX179/Chess/actions/workflows/tests.yml/badge.svg)](https://github.com/XAJX179/Chess/actions/workflows/tests.yml)
8
+ [![Rubocop](https://github.com/XAJX179/Chess/actions/workflows/rubocop.yml/badge.svg)](https://github.com/XAJX179/Chess/actions/workflows/rubocop.yml)
9
+
10
+ ## About
11
+
12
+ Chess game with terminal ui and mouse click support,
13
+ load game with FEN codes or start new!
14
+
15
+ ## Install
16
+
17
+ pre-requisite: ruby >= 3.2
18
+
19
+ ```bash
20
+ gem install chess_tui
21
+ ```
22
+
23
+ ## uninstall
24
+
25
+ ```bash
26
+ gem uninstall chess_tui
27
+ ```
28
+
29
+ ## Documentation
30
+
31
+ [Documentation](https://xajx179.github.io/Chess/)
32
+
33
+ ## 🫣 Peek
34
+
35
+ ![screenshot](https://raw.githubusercontent.com/XAJX179/Chess/refs/heads/main/docs/images/chess_tui_screenshot.png)
36
+
37
+ showcase video -
38
+
39
+ <https://github.com/user-attachments/assets/180d563f-16d9-4f26-9b3a-c16bb66271a2>
data/bin/chess_tui ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/chess'
5
+
6
+ Chess.start
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Chess
4
+ module Chess
5
+ # Board
6
+ class Board
7
+ include BoardFromFenCode
8
+ include BoardPos
9
+
10
+ # @!attribute data
11
+ # 'a’ to ‘h’ are files, rnbqkp are black pieces
12
+ # and RNBQKP are white pieces along the 8 ranks.
13
+ # @example generated data looks like this
14
+ # {
15
+ # "a"=>["R", "P", "", "", "", "", "p", "r"],
16
+ # "b"=>["N", "P", "", "", "", "", "p", "n"],
17
+ # "c"=>["B", "P", "", "", "", "", "p", "b"],
18
+ # "d"=>["Q", "P", "", "", "", "", "p", "q"],
19
+ # "e"=>["K", "P", "", "", "", "", "p", "k"],
20
+ # "f"=>["B", "P", "", "", "", "", "p", "b"],
21
+ # "g"=>["N", "P", "", "", "", "", "p", "n"],
22
+ # "h"=>["R", "P", "", "", "", "", "p", "r"]
23
+ # }
24
+ # @return [Hash] board data
25
+
26
+ attr_accessor :data, :current_player, :castling_rights, :possible_en_passant_target,
27
+ :half_move, :full_move, :white_pieces, :black_pieces, :white_king, :black_king
28
+
29
+ # Returns a new instance of Board.
30
+ # @param fen_code [String]
31
+ def initialize(fen_code)
32
+ @white_pieces = []
33
+ @black_pieces = []
34
+ @data = generate_data(fen_code)
35
+ end
36
+
37
+ # returns piece at the given position
38
+ # @param file [String]
39
+ # @param rank [Integer]
40
+ # @return any subclass of {Chess::Pieces::Piece}
41
+ def piece_at(file, rank)
42
+ return if @data[file].nil?
43
+
44
+ @data[file][rank]
45
+ end
46
+
47
+ # checks if a pos is empty
48
+ # @param file [String]
49
+ # @param rank [Integer]
50
+ def empty_at?(file, rank)
51
+ piece_at(file, rank) == ''
52
+ end
53
+
54
+ # removes the piece from board and also from {#black_pieces} or {#white_pieces}
55
+ # @example file and rank are decomposed from array
56
+ # remove_piece_at(['d',0])
57
+ # @return [void]
58
+ def remove_piece_at((file, rank))
59
+ @data[file][rank] = ''
60
+ @black_pieces.reject! { |piece| piece.pos == [file, rank] }
61
+ @white_pieces.reject! { |piece| piece.pos == [file, rank] }
62
+ end
63
+
64
+ # inserts given piece at given pos
65
+ # @param piece [Chess::Pieces::Piece]
66
+ # @example file and rank are decomposed from array
67
+ # insert_piece_at(['d',0])
68
+ # @return [void]
69
+ def insert_piece_at(piece, (file, rank))
70
+ @data[file][rank] = piece
71
+ piece.pos = [file, rank]
72
+ if @current_player == 'w'
73
+ @white_pieces << piece unless @white_pieces.include?(piece)
74
+ else
75
+ @black_pieces << piece unless @white_pieces.include?(piece)
76
+ end
77
+ end
78
+
79
+ # check if enemy at pos
80
+ # @param file [String]
81
+ # @param rank [Integer]
82
+ def enemy_at?(file, rank)
83
+ piece = piece_at(file, rank)
84
+ @current_player != piece.color.chr
85
+ end
86
+
87
+ # resets half move to 0
88
+ def reset_half_move
89
+ @half_move = 0
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Chess
4
+ module Chess
5
+ # create board from fen_code
6
+ module BoardFromFenCode
7
+ # generate game’s data from FEN code.
8
+ # @param fen_code [String]
9
+ # @return [Hash] generated_board_data
10
+ def generate_data(fen_code)
11
+ fen_parts_array = fen_code.split
12
+ board_data = fen_parts_array[0]
13
+ @current_player = fen_parts_array[1]
14
+ @castling_rights = fen_parts_array[2]
15
+ @possible_en_passant_target = fen_parts_array[3]
16
+ @half_move = fen_parts_array[4].to_i
17
+ @full_move = fen_parts_array[5].to_i
18
+
19
+ create_board(board_data)
20
+ end
21
+
22
+ # creates board data
23
+ # @param board_data [String] board_data part of FEN code
24
+ # @return [Hash] board data
25
+ def create_board(board_data)
26
+ ranks = board_data.split('/')
27
+ files = ('a'..'h') # value for 'a' to 'h'
28
+ board = files.to_h { |file| [file, Array.new(8) { '' }] } # empty board
29
+
30
+ board = fill_board(ranks, board)
31
+ @data = board
32
+ end
33
+
34
+ # fills board with rank wise data.
35
+ # @param ranks [Array]
36
+ # @param board [Hash]
37
+ # @return [Hash] filled board.
38
+ def fill_board(ranks, board)
39
+ ranks.reverse!
40
+ ranks.each_index do |ranks_array_index|
41
+ rank = ranks[ranks_array_index].chars
42
+ fill_rank(board, rank, ranks_array_index)
43
+ end
44
+ board
45
+ end
46
+
47
+ # fills a rank
48
+ # @param board [Hash]
49
+ # @param rank [Array]
50
+ # @param ranks_array_index [Integer]
51
+ # @return [void]
52
+ def fill_rank(board, rank, ranks_array_index)
53
+ index = 0
54
+ rank.each do |letter|
55
+ if letter.to_i.zero?
56
+ index += 1
57
+ insert_piece(board, index, ranks_array_index, letter)
58
+ else
59
+ index += letter.to_i
60
+ end
61
+ end
62
+ end
63
+
64
+ # insert a piece on given board
65
+ # @param board [Hash]
66
+ # @param index [Integer]
67
+ # @param ranks_array_index [Integer]
68
+ # @param letter [String]
69
+ # @return [void]
70
+ def insert_piece(board, index, ranks_array_index, letter)
71
+ shift_ord = 96
72
+ file = (shift_ord + index).chr
73
+ rank = ranks_array_index
74
+
75
+ board[file][rank] = create_piece(letter)
76
+ piece_pos = [file, rank]
77
+ board[file][rank].pos = piece_pos
78
+ end
79
+
80
+ # create piece from the letter
81
+ # @param letter [String]
82
+ # @return any subclass of {Chess::Pieces::Piece}
83
+ def create_piece(letter)
84
+ if letter == letter.upcase
85
+ piece = white_piece(letter)
86
+ @white_pieces << piece
87
+ else
88
+ piece = black_piece(letter)
89
+ @black_pieces << piece
90
+ end
91
+ piece
92
+ end
93
+
94
+ # create white color piece from the letter
95
+ # @param letter [String]
96
+ # @return any subclass of {Chess::Pieces::Piece} with color white
97
+ def white_piece(letter) # rubocop:disable Metrics/MethodLength
98
+ color = 'white'
99
+ case letter
100
+ when 'R'
101
+ Chess::Pieces::Rook.new(color)
102
+ when 'N'
103
+ Chess::Pieces::Knight.new(color)
104
+ when 'B'
105
+ Chess::Pieces::Bishop.new(color)
106
+ when 'Q'
107
+ Chess::Pieces::Queen.new(color)
108
+ when 'K'
109
+ @white_king = Chess::Pieces::King.new(color)
110
+ when 'P'
111
+ Chess::Pieces::Pawn.new(color)
112
+ end
113
+ end
114
+
115
+ # create black color piece from the letter
116
+ # @param letter [String]
117
+ # @return any subclass of {Chess::Pieces::Piece} with color black
118
+ def black_piece(letter) # rubocop:disable Metrics/MethodLength
119
+ color = 'black'
120
+ case letter
121
+ when 'r'
122
+ Chess::Pieces::Rook.new(color)
123
+ when 'n'
124
+ Chess::Pieces::Knight.new(color)
125
+ when 'b'
126
+ Chess::Pieces::Bishop.new(color)
127
+ when 'q'
128
+ Chess::Pieces::Queen.new(color)
129
+ when 'k'
130
+ @black_king = Chess::Pieces::King.new(color)
131
+ when 'p'
132
+ Chess::Pieces::Pawn.new(color)
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Chess
4
+ module Chess
5
+ # Board
6
+ module BoardPos
7
+ # the north pos from the given pos
8
+ def north_pos(file, rank)
9
+ [file, rank + 1]
10
+ end
11
+
12
+ # the south pos from the given pos
13
+ def south_pos(file, rank)
14
+ [file, rank - 1]
15
+ end
16
+
17
+ # the east pos from the given pos
18
+ def east_pos(file, rank)
19
+ [(file.ord - 1).chr, rank]
20
+ end
21
+
22
+ # the west pos from the given pos
23
+ def west_pos(file, rank)
24
+ [(file.ord + 1).chr, rank]
25
+ end
26
+
27
+ # the north_east_pos pos from the given pos
28
+ def north_east_pos(file, rank)
29
+ [(file.ord - 1).chr, rank + 1]
30
+ end
31
+
32
+ # the north_west_pos pos from the given pos
33
+ def north_west_pos(file, rank)
34
+ [(file.ord + 1).chr, rank + 1]
35
+ end
36
+
37
+ # the south_east_pos pos from the given pos
38
+ def south_east_pos(file, rank)
39
+ [(file.ord - 1).chr, rank - 1]
40
+ end
41
+
42
+ # the south_west_pos pos from the given pos
43
+ def south_west_pos(file, rank)
44
+ [(file.ord + 1).chr, rank - 1]
45
+ end
46
+
47
+ # checks if a pos is nil value
48
+ # @example file and rank are decomposed from array
49
+ # pos_nil?(['d',0])
50
+ def pos_nil?((file, rank))
51
+ piece_at(file, rank).nil?
52
+ end
53
+
54
+ # checks if a pos is in range
55
+ # @example file and rank are decomposed from array
56
+ # pos_in_range?(['d',0])
57
+ def pos_in_range?((file, rank))
58
+ first_rank = 0
59
+ last_rank = 7
60
+ first_file_ord = 'a'.ord
61
+ last_file_ord = 'h'.ord
62
+ rank.between?(first_rank, last_rank) && file.ord.between?(first_file_ord, last_file_ord)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Chess
4
+ module Chess
5
+ # Console Display
6
+ module Display
7
+ # displays board with ASCII characters
8
+ # @param board_data [Hash]
9
+ # @param valid_moves [Array]
10
+ # @return [void]
11
+ def display_board(board_data, valid_moves = [])
12
+ print_files(board_data)
13
+ print_board_data(board_data, valid_moves)
14
+ print_files(board_data)
15
+ end
16
+
17
+ # prints the board data
18
+ #
19
+ # @param board_data [Hash]
20
+ # @param valid_moves [Array]
21
+ # @return [void]
22
+ def print_board_data(board_data, valid_moves)
23
+ shift_rank = 1
24
+ (0..7).reverse_each do |rank|
25
+ print rank + shift_rank
26
+ ('a'..'h').each do |file|
27
+ bg_color_name = square_bg_color_name(file, rank)
28
+ print_square(board_data, file, rank, bg_color_name, valid_moves)
29
+ end
30
+ print rank + shift_rank
31
+ puts
32
+ end
33
+ end
34
+
35
+ # prints files above the board 'a' to 'h'
36
+ #
37
+ # @param board_data [Hash]
38
+ # @return [void]
39
+ def print_files(board_data)
40
+ print ' '
41
+ board_data.each_key { |key| print " #{key} " }
42
+ puts
43
+ end
44
+
45
+ # prints a single square
46
+ #
47
+ # @param board_data [Hash]
48
+ # @param file [String]
49
+ # @param rank [Integer]
50
+ # @param bg_color_name [Symbol]
51
+ # @param valid_moves [Array]
52
+ # @return [void]
53
+ def print_square(board_data, file, rank, bg_color_name, valid_moves)
54
+ piece = board_data[file][rank]
55
+ square_pos = [file, rank]
56
+ if piece == ''
57
+ print square_string(piece, ' ', bg_color_name, square_pos, valid_moves)
58
+ else
59
+ print_piece_square(piece, bg_color_name, square_pos, valid_moves)
60
+ end
61
+ end
62
+
63
+ # helper method for {#print_square}
64
+ #
65
+ # @param piece any subclass of {Chess::Pieces::Piece}
66
+ # @param bg_color_name [Symbol]
67
+ # @param square_pos [Array]
68
+ # @param valid_moves [Array]
69
+ # @return [void]
70
+ def print_piece_square(piece, bg_color_name, square_pos, valid_moves) # rubocop:disable Metrics/MethodLength
71
+ case piece
72
+ when Pieces::Rook
73
+ print square_string(piece, "\u{265C}", bg_color_name, square_pos, valid_moves)
74
+ when Pieces::Knight
75
+ print square_string(piece, "\u{265E}", bg_color_name, square_pos, valid_moves)
76
+ when Pieces::Bishop
77
+ print square_string(piece, "\u{265D}", bg_color_name, square_pos, valid_moves)
78
+ when Pieces::Queen
79
+ print square_string(piece, "\u{265B}", bg_color_name, square_pos, valid_moves)
80
+ when Pieces::King
81
+ print square_string(piece, "\u{265A}", bg_color_name, square_pos, valid_moves)
82
+ when Pieces::Pawn
83
+ print square_string(piece, "\u{265F}", bg_color_name, square_pos, valid_moves)
84
+ end
85
+ end
86
+
87
+ # returns background color name for the square at
88
+ # given file and rank.
89
+ #
90
+ # @param file [String]
91
+ # @param rank [Integer]
92
+ # @return [Symbol]
93
+ def square_bg_color_name(file, rank)
94
+ if (file.ord.odd? && rank.odd?) || (file.ord.even? && rank.even?)
95
+ :dull_white
96
+ else
97
+ :green
98
+ end
99
+ end
100
+
101
+ # arrange and format strings for the square
102
+ #
103
+ # @param piece [Chess::Piece]
104
+ # @param unicode [String]
105
+ # @param bg_color_name [Symbol]
106
+ # @param square_pos [Array]
107
+ # @param valid_moves [Array]
108
+ # @return [String]
109
+ def square_string(piece, unicode, bg_color_name, square_pos, valid_moves)
110
+ if piece == ''
111
+ bg_color("#{move_dots(square_pos, valid_moves)}\u{00A0}\u{00A0}", bg_color_name)
112
+ elsif piece.is_a?(Chess::Pieces::King)
113
+ piece_color = piece.color
114
+ bg_color("#{move_dots(square_pos, valid_moves)}#{color(unicode, piece_color.to_sym)}#{
115
+ king_check_dot(piece)}", bg_color_name)
116
+ else
117
+ piece_color = piece.color
118
+ bg_color("#{move_dots(square_pos, valid_moves)}#{color(unicode, piece_color.to_sym)} ", bg_color_name)
119
+ end
120
+ end
121
+
122
+ # gives dot for move for the pos if it's inside valid_moves
123
+ #
124
+ # @param square_pos [Array]
125
+ # @param valid_moves [Array]
126
+ # @return [String] string (with dot or empty)
127
+ def move_dots(square_pos, valid_moves)
128
+ if valid_moves.include?(square_pos)
129
+ color("\u{2022}", :black)
130
+ else
131
+ "\u00A0"
132
+ end
133
+ end
134
+
135
+ # gives dot for king in check if given king is in check
136
+ #
137
+ # @param king [Chess::Pieces::King]
138
+ # @return [String] string (with red dot or empty)
139
+ def king_check_dot(king)
140
+ if king.in_check
141
+ color("\u{29BE}", :red)
142
+ else
143
+ "\u00A0"
144
+ end
145
+ end
146
+
147
+ # display Save & Exit and Exit buttons
148
+ # return [void]
149
+ def display_buttons
150
+ print ' '
151
+ print bg_color(color('Save & Exit', :green), :black)
152
+ print ' '
153
+ print bg_color(color('Exit', :green), :black)
154
+ puts
155
+ end
156
+
157
+ # clears the display and redraws the board with given board_data
158
+ #
159
+ # @param board_data [Hash]
160
+ # @param valid_moves [Array]
161
+ # @return [void]
162
+ def redraw_display(board_data, valid_moves = [])
163
+ system 'clear'
164
+ display_board(board_data, valid_moves)
165
+ display_buttons
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Chess
4
+ module Chess
5
+ # Console Display
6
+ module Display
7
+ # Colors values for use in escape code.
8
+ COLORS = {
9
+ green: '119;149;86',
10
+ dull_white: '235;236;208',
11
+ white: '255;187;143',
12
+ black: '1;1;1',
13
+ red: '249;23;32'
14
+ }.freeze
15
+
16
+ # background color the string
17
+ #
18
+ # @param string [String]
19
+ # @param name [Symbol]
20
+ # @return [String]
21
+ def bg_color(string, name)
22
+ value = COLORS[name]
23
+ "\e[48;2;#{value}m#{string}\e[0m"
24
+ end
25
+
26
+ # color the string
27
+ #
28
+ # @param string [String]
29
+ # @param name [Symbol]
30
+ # @return [String]
31
+ #
32
+ # @note will not reset the color like #bg_color
33
+ def color(string, name)
34
+ value = COLORS[name]
35
+ "\e[38;2;#{value}m#{string}"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Chess
4
+ module Chess
5
+ # Console Display
6
+ module Display
7
+ # Prompts user to select between New Game
8
+ # /Load Save/Load Code(FEN) and returns the choice.
9
+ #
10
+ # @return [Integer] 0 for New Game. 1 for Load Save. 2 for Load Code(FEN).
11
+ def prompt_start_choices
12
+ prompt = TTY::Prompt.new
13
+ prompt.select('Create new game or load save/code?') do |menu|
14
+ menu.choice name: 'New Game', value: 1
15
+ if File.file?(SAVE_PATH) # if save.json exists
16
+ menu.choice name: 'Load Save', value: 2
17
+ else
18
+ menu.choice name: 'Load Save', disabled: '(No Save Found.)'
19
+ end
20
+ menu.choice name: 'Load using Code (FEN)', value: 3
21
+ end
22
+ end
23
+
24
+ # Promps User to select between saved data.
25
+ #
26
+ # @param save_data [Hash]
27
+ # @return [String] the FEN code of selected save name.
28
+ def prompt_select_save(save_data)
29
+ names = save_data.keys
30
+ prompt = TTY::Prompt.new
31
+ selected_save_key = prompt.select('Select a save.', per_page: 15, filter: true) do |menu|
32
+ names.each do |name|
33
+ menu.choice name: name, value: name
34
+ end
35
+ end
36
+ save_data[selected_save_key]
37
+ end
38
+
39
+ # Prompts User to enter a FEN code.
40
+ # @note method expects user is entering a valid FEN code.
41
+ #
42
+ # @return [String] the FEN code entered.
43
+ def prompt_enter_code
44
+ prompt = TTY::Prompt.new
45
+ prompt.ask('Enter a valid self attested FEN code : ') do |question|
46
+ regex = %r{\A[prnbqkPRNBQK0-9/ w\-a-h]+\z} # only checks if valid characters
47
+ # are used like 1-8,kqbnrKQBNR, w for white chance(b=bisop already),
48
+ # a-h(for en passant) and the slash(/).
49
+ question.required true
50
+ question.validate regex, 'Invalid FEN Code. '
51
+ end
52
+ end
53
+
54
+ # Prompts User to choose piece to promote pawn
55
+ #
56
+ # @return [String] the letter of the piece
57
+ def prompt_pawn_promotion_choices
58
+ prompt = TTY::Prompt.new
59
+ prompt.select('Select the piece for pawn promotion!') do |menu|
60
+ menu.choice name: 'Queen', value: 'q'
61
+ menu.choice name: 'Knight', value: 'n'
62
+ menu.choice name: 'Bishop', value: 'b'
63
+ menu.choice name: 'Rook', value: 'r'
64
+ end
65
+ end
66
+
67
+ # Prompts User to enter a save name
68
+ #
69
+ # @return [String] save_name
70
+ def prompt_save_name
71
+ prompt = TTY::Prompt.new
72
+ prompt.ask('Enter a valid save name : ') do |question|
73
+ regex = /^[\w.]+$/
74
+ question.required true
75
+ question.validate regex, 'Only a-z , A-Z , 0-9 , _ allowed'
76
+ end
77
+ end
78
+ end
79
+ end