rb_chess 0.0.0

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: 04be0bab3ec8dd8abbb014e44d3a209cf727c52973752bb371d46cc9f40d19f0
4
+ data.tar.gz: 077b182bb41c50319c21dbba238d51d36f7d7f8760722168a24cb8d4ae675661
5
+ SHA512:
6
+ metadata.gz: 79dbefdc24671860bc959442bcfcaf62c77329db1e879a2d34a56551cf557a499f8e22e1a5baa413a1dee4c05be93f82376781d28a970636d770d21012137514
7
+ data.tar.gz: 7bbfc032021fc78f66dd6c0727d06cbc4d89ae10addc092c72e5f8a2a3a1dd721bddd558a8b92ce231814465d37f5ea1550fa80f7d9174d19519d15f2aaf82c6
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Peter Abah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # rb_chess
2
+ A chess library written in ruby. It provides all a representation of a chess game with all rules and serialization. It also provides a command line interface for playing the game
3
+
4
+ ## Installation
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'just_chess'
9
+ ```
10
+
11
+ And then run:
12
+ ```
13
+ $ bundle
14
+ ```
15
+
16
+ Or install it yourself as:
17
+ ```
18
+ $ gem install rb_chess
19
+ ```
20
+
21
+ ## Features
22
+ - Supports all chess moves including:
23
+ + En passant.
24
+ + Castling.
25
+ + Pawn promotion.
26
+ - The game detects and declares:
27
+ + Checkmate.
28
+ + Stalemate.
29
+ + Insufficient material.
30
+ + Fivefold repetition
31
+ + Seventy five moves rule
32
+ - Move generation.
33
+ - Move validation.
34
+ - Serializing (json and binary).
35
+
36
+ ## Usage
37
+
38
+ The code below plays a random game of chess:
39
+
40
+ ```ruby
41
+ require 'rb_chess'
42
+
43
+ game = RbChess::Game.new
44
+
45
+ until game.game_over?
46
+ puts game.board.ascii
47
+ game.make_move(game.all_moves.sample)
48
+ end
49
+
50
+ puts game.board.to_fen
51
+ ```
52
+
53
+ You can find the full example of using this library in the [RbChess::CLI](lib/rb_chess/cli/cli.rb) class in lib/rb_chess/cli/cli.rb.
54
+
55
+ ## Playing
56
+ - To start run ``` rb_chess ```.
57
+ - Moves are made by typing in coordinate system i.e `e2e4`.
58
+ - Castling moves are made by typing `0-0` for kingside and `0-0-0` for queenside.
59
+ - Promotion moves are made by typing the move with the promotion piece after it e.g `a7a8Q`.
60
+ - To save type `save` or `s`.
61
+ - To quit the game type `exit`.
62
+
63
+ ## To do
64
+ - Support for PGN.
65
+ - Support for SAN move format.
66
+
67
+ ## License
68
+ Distributed under the MIT License. See LICENSE for more information.
data/bin/rb_chess ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'rb_chess/cli/cli'
5
+
6
+ RbChess::CLI.new.start
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'require_all'
4
+
5
+ require_relative 'fen_parser'
6
+ require_relative 'position'
7
+ require_relative 'errors'
8
+ require_relative 'letter_display'
9
+ require_rel 'pieces/pawn', 'pieces/piece_constants'
10
+
11
+ # A class to represent a chess board
12
+ module RbChess
13
+ class Board
14
+ include LetterDisplay
15
+ include PieceConstants
16
+
17
+ attr_reader :pieces, :active_color, :en_passant_square, :halfmove_clock, :fullmove_no, :castling_rights
18
+
19
+ def initialize(fen_notation: nil, segments: nil)
20
+ segments ||= fen_notation ? FENParser.new(fen_notation).parse : FENParser.new.parse
21
+ @pieces = segments[:pieces].freeze
22
+ @active_color = segments[:active_color]
23
+ @castling_rights = segments[:castling_rights]
24
+ @en_passant_square = segments[:en_passant_square]
25
+ @halfmove_clock = segments[:halfmove_clock]
26
+ @fullmove_no = segments[:fullmove_no]
27
+ end
28
+
29
+ def make_move(move)
30
+ segments = {
31
+ pieces: update_pieces(move),
32
+ active_color: active_color == :white ? :black : :white,
33
+ castling_rights: update_castling_rights(move),
34
+ en_passant_square: update_en_passant_square(move),
35
+ halfmove_clock: update_halfmove_clock(move),
36
+ fullmove_no: active_color == :black ? fullmove_no + 1 : fullmove_no
37
+ }
38
+
39
+ self.class.new(segments: segments)
40
+ end
41
+
42
+ def player_pieces(color)
43
+ pieces.select { |piece| piece.color == color }
44
+ end
45
+
46
+ def opponent_color
47
+ active_color == :white ? :black : :white
48
+ end
49
+
50
+ def piece_at(pos, pieces_n: pieces)
51
+ pos = Position.parse(pos) unless pos.is_a? Position
52
+
53
+ pieces_n.find { |piece| piece.position == pos }
54
+ end
55
+
56
+ def can_castle_kingside?(color)
57
+ castling_rights.kingside[color]
58
+ end
59
+
60
+ def can_castle_queenside?(color)
61
+ castling_rights.queenside[color]
62
+ end
63
+
64
+ def to_fen
65
+ FENParser.board_to_fen self
66
+ end
67
+
68
+ def to_s
69
+ to_fen
70
+ end
71
+
72
+ private
73
+
74
+ def update_castling_rights(move)
75
+ new_castling_rights = castling_rights.dup
76
+
77
+ new_castling_rights.kingside[:white] = invalidate_kingside_castling(move, :white)
78
+ new_castling_rights.queenside[:white] = invalidate_queenside_castling(move, :white)
79
+
80
+ new_castling_rights.kingside[:black] = invalidate_kingside_castling(move, :black)
81
+ new_castling_rights.queenside[:black] = invalidate_queenside_castling(move, :black)
82
+
83
+ new_castling_rights
84
+ end
85
+
86
+ def invalidate_kingside_castling(move, color)
87
+ invalidate = move.moved.all? do |hash|
88
+ if color == active_color
89
+ hash[:from].king_pos?(color) || hash[:from].kingside_rook_pos?(color)
90
+ elsif move.removed
91
+ move.removed.kingside_rook_pos?(color)
92
+ end
93
+ end
94
+
95
+ castling_rights.kingside[color] && !invalidate
96
+ end
97
+
98
+ def invalidate_queenside_castling(move, color)
99
+ invalidate = move.moved.all? do |hash|
100
+ if color == active_color
101
+ hash[:from].king_pos?(color) || hash[:from].queenside_rook_pos?(color)
102
+ elsif move.removed
103
+ move.removed.queenside_rook_pos?(color)
104
+ end
105
+ end
106
+
107
+ castling_rights.queenside[color] && !invalidate
108
+ end
109
+
110
+ def update_pieces(move)
111
+ pieces = remove_piece(move)
112
+ move_pieces(pieces, move)
113
+ end
114
+
115
+ def update_en_passant_square(move)
116
+ move.moved.reduce('-') do |res, hash|
117
+ if en_passant_possible?(hash)
118
+ direction = piece_at(hash[:from]).direction
119
+ return hash[:to].increment(y: -direction)
120
+ end
121
+
122
+ res
123
+ end
124
+ end
125
+
126
+ def update_halfmove_clock(move)
127
+ reset_clock = move.moved.reduce(false) do |res, hash|
128
+ piece = piece_at(hash[:from])
129
+ piece.is_a?(Pawn) ? true : res
130
+ end
131
+
132
+ move.removed || reset_clock ? 0 : halfmove_clock + 1
133
+ end
134
+
135
+ def en_passant_possible?(hash)
136
+ piece = piece_at(hash[:from])
137
+
138
+ piece.is_a?(Pawn) && hash[:from].starting_pawn_rank?(piece.color) &&
139
+ hash[:to].en_passant_rank?(piece.color)
140
+ end
141
+
142
+ def remove_piece(move)
143
+ return pieces.dup unless move.removed
144
+ raise ChessError, 'Cannot remove your own piece' if piece_at(move.removed).color == active_color
145
+
146
+ pieces.reject { |piece| piece.position == move.removed }
147
+ end
148
+
149
+ def move_pieces(pieces, move)
150
+ move.moved.reduce(pieces) do |res, hash|
151
+ piece = move_piece(res, hash)
152
+ piece = promote_piece(pieces, hash, move.promotion) if move.promotion
153
+ res = res.reject { |piece| piece.position == hash[:from] }
154
+ res.push(piece)
155
+ end
156
+ end
157
+
158
+ def move_piece(pieces, hash)
159
+ piece = piece_at(hash[:from], pieces_n: pieces)
160
+ raise ChessError, 'Can only move your own piece' unless piece.color == active_color
161
+
162
+ piece.update_position(hash[:to])
163
+ end
164
+
165
+ def promote_piece(pieces, hash, piece_letter)
166
+ piece_class = LETTER_TO_PIECE_CLASS[piece_letter.downcase.to_sym]
167
+ color = piece_at(hash[:from], pieces_n: pieces).color
168
+ piece_class.new(color, hash[:to])
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbChess
4
+ class CastlingRights
5
+ attr_reader :kingside, :queenside
6
+
7
+ def initialize(kingside: nil, queenside: nil)
8
+ @kingside = kingside || { white: false, black: false }
9
+ @queenside = queenside || { white: false, black: false }
10
+ end
11
+
12
+ def to_s
13
+ res = ''
14
+
15
+ res += 'K' if kingside[:white]
16
+ res += 'Q' if queenside[:white]
17
+ res += 'k' if kingside[:black]
18
+ res += 'q' if queenside[:black]
19
+
20
+ res = '-' if res.empty?
21
+
22
+ res
23
+ end
24
+
25
+ def dup
26
+ new_kingside = kingside.dup
27
+ new_queenside = queenside.dup
28
+ CastlingRights.new(kingside: new_kingside, queenside: new_queenside)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'require_all'
4
+
5
+ require_relative '../game'
6
+ require_rel '.'
7
+
8
+ module RbChess
9
+ class CLI
10
+ def initialize
11
+ @game_saver = GameSaver.new
12
+ end
13
+
14
+ def start
15
+ welcome
16
+ @game, @players = game_option == :new ? new_game : load_game
17
+ play_game
18
+ end
19
+
20
+ def welcome
21
+ puts 'Welcome to Chess.'
22
+ help
23
+ puts 'Enjoy the game.'
24
+ end
25
+
26
+ def game_option
27
+ choice = -1
28
+ until choice.between?(1, 2)
29
+ puts <<~HEREDOC
30
+ ---------------------
31
+ 1 - New game
32
+ 2 - Load saved game
33
+ HEREDOC
34
+
35
+ choice = gets.chomp.to_i
36
+ end
37
+
38
+ choice == 1 ? :new : :load
39
+ end
40
+
41
+ def new_game
42
+ players = get_players
43
+ [Game.new, players]
44
+ end
45
+
46
+ def load_game
47
+ files = @game_saver.saved_games
48
+ if files.empty?
49
+ puts 'No Games to load. Starting new game'
50
+ return new_game
51
+ end
52
+
53
+ choice = -1
54
+ until choice.between?(0, files.size - 1)
55
+ display_list(files)
56
+ puts 'Choose game to load. (Enter number)'
57
+ choice = gets.chomp.to_i - 1
58
+ end
59
+
60
+ @game_saver.load(files[choice])
61
+ end
62
+
63
+ def play_game
64
+ game_loop until @game.game_over?
65
+
66
+ puts @game.board.ascii
67
+ puts end_game_msg
68
+ play_again? && start
69
+ end
70
+
71
+ def game_loop
72
+ puts @game.board.ascii
73
+ puts "#{player.name} in check!" if @game.check?
74
+ input = player_input
75
+
76
+ if commands[input.to_sym]
77
+ commands[input.to_sym].call
78
+ elsif pos? input
79
+ show_pos_moves input
80
+ elsif @game.valid_move? input
81
+ @game.make_move input
82
+ else
83
+ puts 'Invalid move or command!'
84
+ end
85
+ end
86
+
87
+ def player
88
+ @players.find { |p| p.color == @game.current_player }
89
+ end
90
+
91
+ def end_game_msg
92
+ player = @players.find { |p| p.color == @game.winner }
93
+
94
+ if @game.checkmate?
95
+ "Checkmate #{player.name} wins!"
96
+ elsif @game.stalemate?
97
+ 'Draw by stalemate!'
98
+ elsif @game.fivefold?
99
+ 'Draw by fivefold repetition!'
100
+ elsif @game.seventy_five_moves?
101
+ 'Draw by seventy five moves rule (No pawn move or capture in last seventy five moves)'
102
+ elsif @game.insufficient_material?
103
+ 'Draw by insufficient material'
104
+ else
105
+ 'Game over!'
106
+ end
107
+ end
108
+
109
+ def play_again?
110
+ p 'Do you want to play again?(Y|N default is N): '
111
+ choice = gets.chomp.downcase
112
+ choice == 'y'
113
+ end
114
+
115
+ def player_input
116
+ return player.move(@game) if player.is_a? ComputerPlayer
117
+
118
+ puts "#{player.name} Enter move"
119
+ gets.chomp.downcase
120
+ end
121
+
122
+ def help
123
+ puts <<~HEREDOC
124
+ Moves are in coordinate system format e.g e2e4 with exception of castling moves
125
+ which are 0-0 for kingside and 0-0-0 for queenside.
126
+
127
+ You can also enter a piece position e.g b1.
128
+ If the piece has legal moves available, the game will display them or the the
129
+ player must choose another piece.
130
+
131
+ For promotion moves append the promotion piece (Q, R, B, N) after the move
132
+ e.g a2a1R
133
+
134
+ If a player is in check, they can only select pieces with moves that take them
135
+ out of check.
136
+
137
+ Enter [moves] or [m] to view all available moves.
138
+
139
+ Enter [save] or [s] at any time to save the game.
140
+
141
+ Enter [exit] at any time to end the game
142
+
143
+ Enter [help] or [h] to view this guide again
144
+
145
+ The game ends by checkmate or draw (stalemate, fivefold repetition,
146
+ insufficient material or seventy five turns without a capture or pawn move)
147
+ HEREDOC
148
+ end
149
+
150
+ def moves
151
+ display_list(@game.all_moves)
152
+ end
153
+
154
+ def save
155
+ filename = ''
156
+ while filename.empty?
157
+ puts 'Enter filename: '
158
+ filename = gets.chomp
159
+ end
160
+
161
+ @game_saver.save(@game, @players, filename)
162
+ end
163
+
164
+ def get_players
165
+ mode = get_mode
166
+ return [Player.new(:white), Player.new(:black)] if mode == :human
167
+
168
+ color = get_color
169
+ computer_color = color == :white ? :black : :white
170
+ [Player.new(color), RandomAI.new(computer_color)]
171
+ end
172
+
173
+ def get_mode
174
+ choice = -1
175
+ until choice.between?(1, 2)
176
+ puts <<~HEREDOC
177
+ -------------
178
+ Choose mode
179
+ -------------
180
+ 1 - vs Human
181
+ 2 - vs Computer (just random moves)
182
+ HEREDOC
183
+ choice = gets.chomp.to_i
184
+ end
185
+
186
+ choice == 1 ? :human : :computer
187
+ end
188
+
189
+ def get_color
190
+ choice = -1
191
+ until choice.between?(1, 2)
192
+ puts <<~HEREDOC
193
+ -------------
194
+ Choose color
195
+ -------------
196
+ 1 - White
197
+ 2 - Black
198
+ HEREDOC
199
+ choice = gets.chomp.to_i
200
+ end
201
+
202
+ choice == 1 ? :white : :black
203
+ end
204
+
205
+ def commands
206
+ @commands ||= {
207
+ help: method(:help),
208
+ h: method(:help),
209
+ moves: method(:moves),
210
+ m: method(:moves),
211
+ exit: method(:exit),
212
+ save: method(:save),
213
+ s: method(:save)
214
+ }
215
+ end
216
+
217
+ def pos?(pos)
218
+ /^[a-h][1-8]$/.match pos
219
+ end
220
+
221
+ def show_pos_moves(pos)
222
+ moves = @game.moves_at pos
223
+ if moves.empty?
224
+ puts "No moves at #{pos}"
225
+ return
226
+ end
227
+
228
+ display_list(moves)
229
+ end
230
+
231
+ def display_list(list)
232
+ list.each_with_index do |data, i|
233
+ puts "#{i + 1} - #{data}"
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module RbChess
6
+ class GameSaver
7
+ def initialize
8
+ Dir.exist?(dirname) || FileUtils.mkdir_p(dirname)
9
+ end
10
+
11
+ def saved_games
12
+ files = Dir.entries(dirname).reject { |f| ['.', '..'].include?(f) }
13
+ end
14
+
15
+ def save(game, players, filename)
16
+ filename = File.join(dirname, filename)
17
+ data = Marshal.dump([game, players])
18
+ File.open(filename, 'wb') { |f| f.write(data) }
19
+ puts 'Game saved'
20
+ end
21
+
22
+ def load(filename)
23
+ filename = File.join(dirname, filename)
24
+ data = File.open(filename, 'rb') { |f| f.read }
25
+
26
+ Marshal.load(data)
27
+ end
28
+
29
+ def dirname
30
+ File.join(Dir.home, '.rb_chess/saved')
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbChess
4
+ class Player
5
+ attr_reader :color, :name
6
+
7
+ def initialize(color)
8
+ @color = color
9
+ @name = color.to_s.capitalize
10
+ end
11
+ end
12
+
13
+ class ComputerPlayer < Player; end
14
+
15
+ class RandomAI < ComputerPlayer
16
+ def move(game)
17
+ game.all_moves.sample
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbChess
4
+ class ChessError < StandardError; end
5
+ end