chess_engine 0.0.7 → 0.0.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 715d471484b4e74f3ea03d4a5efc6b206ae414626ecdf7c31dea440579237ff5
4
- data.tar.gz: be335d9dc761bef96bb75056d0ea83fcd16456e851ce802ae07e8fc4ed30632e
3
+ metadata.gz: 96485fa28f530a602da170422a2b08b757a674bef18cc89d995f620b91c9d4f0
4
+ data.tar.gz: b9d5e4ba5d7e632bc9db2906667deee9c4b807e76371eff33022c3a9c88f5fc4
5
5
  SHA512:
6
- metadata.gz: 6233ac39dcaa5153f4284cde217842295bcc336bcfb2764a6bbb96d5695f18c252a1a61ebc764b35de4e37cae0dc758069c44271b21d1cfcfed43a86a4d0492b
7
- data.tar.gz: 6873b7022d9a44a392f3cc5eb71af17c27baead9aa4fc7d7353f653bacbac8773be384676d9e58990f2a35f3fbe632952c7a74b92d840287a4e111ede4f7397a
6
+ metadata.gz: 2eca066e4f4ad8c4afd56f82a580a40e7568f187ca4e68a1c914fab1ae3d16c48bf75e28fe8a9b8992bcb44db51618aa016440112be590b12b81f7e7cbe75b7d
7
+ data.tar.gz: 8f20a87cd9d933ce5cd5b945937d7d3d208edf476633d4e0236f63ed25c262630161f01f5581d227ea8c893eb50bd8f308debe78021213fca11a8d0e0082d912
@@ -0,0 +1,95 @@
1
+ require_relative "piece"
2
+ require "colorize"
3
+
4
+ module ChessEngine
5
+ class Board
6
+ def initialize
7
+ @board = Array.new(8) { Array.new(8) { nil } }
8
+ end
9
+
10
+ def set_default
11
+ [[:white, 0, 1], [:black, 7, 6]].each do |color, row1, row2|
12
+ ["Rook", "Knight", "Elephant", "Queen", "King", "Elephant", "Knight", "Rook"].each.with_index do |class_name, column|
13
+ self[column, row1] = Module.const_get("ChessEngine::#{class_name}").new(color)
14
+ end
15
+
16
+ 0.upto(7) do |column|
17
+ self[column, row2] = Pawn.new(color)
18
+ end
19
+ end
20
+ end
21
+
22
+ def [](column, row)
23
+ @board[column][row]
24
+ end
25
+
26
+ def []=(column, row, piece)
27
+ @board[column][row] = piece
28
+ end
29
+
30
+ def at(coordinates)
31
+ return nil unless self.exists_at?(coordinates)
32
+ @board[coordinates[0]][coordinates[1]]
33
+ end
34
+
35
+ def set_at(coordinates, piece)
36
+ @board[coordinates[0]][coordinates[1]] = piece
37
+ end
38
+
39
+ def exists_at?(coordinates)
40
+ coordinates.all? { |c| c.between?(0, 7) }
41
+ end
42
+
43
+ def to_s
44
+ string = ""
45
+ colors = [[:default, :light_white].cycle, [:light_white, :default].cycle].cycle
46
+
47
+ 7.downto(0) do |row|
48
+ string += "#{row + 1} "
49
+ colors_cycle = colors.next
50
+
51
+ 0.upto(7) do |column|
52
+ piece = self[column, row]
53
+ string += piece.nil? ? " " : piece.symbol
54
+ string += " "
55
+ string[-2..-1] = string[-2..-1].colorize(background: colors_cycle.next)
56
+ end
57
+ string += "\n"
58
+ end
59
+
60
+ string += " a b c d e f g h"
61
+ string
62
+ end
63
+
64
+ def move_piece(from, to)
65
+ piece = self.at(from)
66
+ self.set_at(from, nil)
67
+ self.set_at(to, piece)
68
+ end
69
+
70
+ def pieces(color)
71
+ @board.flatten.compact.select { |piece| piece.color == color }
72
+ end
73
+
74
+ def Board.coordinates_list
75
+ list = []
76
+ (0..7).each do |x|
77
+ (0..7).each { |y| list << [x, y]}
78
+ end
79
+ list
80
+ end
81
+
82
+ def king_coords(color)
83
+ Board.coordinates_list.find do |coord|
84
+ at(coord) && at(coord).king? && at(coord).color == color
85
+ end
86
+ end
87
+
88
+ def piece_coordinates(color)
89
+ Board.coordinates_list.select do |coord|
90
+ piece = at(coord)
91
+ !piece.nil? && piece.color == color
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,106 @@
1
+ require_relative "game"
2
+ require_relative "input"
3
+ require "yaml"
4
+
5
+
6
+ module ChessEngine
7
+ class NoGamesError < StandardError; end
8
+
9
+ class CLI
10
+ include Input
11
+
12
+ def initialize
13
+ begin
14
+ mode = get_input("\nChoose the game mode:\n1. New game\n2. Continue\nEnter your choice (1 or 2): ", /^[12]$/)
15
+ @game = mode == "1" ? Game.new : choose_game
16
+ rescue NoGamesError => e
17
+ puts "#{e.message}. Try again"
18
+ retry
19
+ end
20
+ play
21
+ save if save?
22
+ end
23
+
24
+ private
25
+
26
+ def play
27
+ until @game.over?
28
+ puts "\n#{@game.name}\n#{@game.draw}"
29
+ declare_check if @game.check?
30
+ begin
31
+ promotion if @game.needs_promotion?
32
+ print "#{@game.current_color.to_s.capitalize}'s move (e. g. \"e2e4\" or \"exit\" or \"000\"/\"00\" (for castling)): "
33
+ move = gets.chomp.gsub(/\s+/, " ")
34
+ case move
35
+ when /^[oOоО0]{2}$/ #matches cyrillic letters
36
+ @game.castling(:short)
37
+ when /^[oOоО0]{3}$/
38
+ @game.castling(:long)
39
+ when /^[a-h][1-8][a-h][1-8]$/i
40
+ @game.move(move)
41
+ when /^exit$/i
42
+ return
43
+ else
44
+ raise InvalidMove, "Incorrect input"
45
+ end
46
+ rescue InvalidMove => e
47
+ puts "#{e.message}. Try again"
48
+ retry
49
+ end
50
+ end
51
+ game_over
52
+ end
53
+
54
+ def promotion
55
+ pieces = ["Queen", "Rook", "Knight", "Elephant"]
56
+ choice = get_input(
57
+ "Pawn promotion. Choose the new piece:\n1. Queen\n2. Rook\n3. Knight\n4. Elephant",
58
+ /^[1-4]$/, "Input must be a single digit"
59
+ )
60
+ @game.promotion(pieces[choice - 1])
61
+ end
62
+
63
+ def declare_check
64
+ puts "#{@game.current_color.to_s.capitalize} player is given a check"
65
+ end
66
+
67
+ def declare_checkmate
68
+ puts "#{@game.current_color.to_s.capitalize} player got mated!"
69
+ end
70
+
71
+ def game_over
72
+ puts @game.draw
73
+ if @game.check?
74
+ declare_checkmate
75
+ else
76
+ puts "Stalemate!"
77
+ end
78
+ end
79
+
80
+ def choose_game
81
+ saved_names = Dir[File.join(CLI.dirname, "/*.yaml")].map { |filename| File.basename(filename, ".yaml") }
82
+ raise NoGamesError, "You have no saved games" if saved_names.empty?
83
+ puts "\nSaved games (#{saved_names.size}):"
84
+ saved_names.each { |name| puts name }
85
+ filename = get_input("Enter the name of the game that you want to continue: ",
86
+ nil, "Such game doesn't exist, try again") { |input| saved_names.include?(input) }
87
+ return YAML::load(File.read(File.join(CLI.dirname, "#{filename}.yaml")))
88
+ end
89
+
90
+ def save?
91
+ choice = get_input("Do you want to save the game? (y/n): ", /^[yn]$/i)
92
+ choice == "y" ? true : false
93
+ end
94
+
95
+ def save
96
+ @game.name ||= get_input("Enter the game name: ", /[^\/\>\<\|\:\&]+/)
97
+ game = YAML::dump(@game)
98
+ Dir.mkdir(CLI.dirname) unless File.exists?(CLI.dirname)
99
+ File.open(File.join(CLI.dirname, "#{@game.name}.yaml"), "w") { |file| file.write(game) }
100
+ end
101
+
102
+ def CLI.dirname
103
+ File.join(Dir.home, "./.chess_engine")
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,148 @@
1
+ require_relative "board"
2
+ require_relative "validator"
3
+ require_relative "move"
4
+
5
+ module ChessEngine
6
+ class InvalidMove < StandardError; end
7
+
8
+ class Game
9
+ attr_accessor :name
10
+ attr_reader :current_color
11
+ include MoveValidator
12
+
13
+ def initialize
14
+ @board = Board.new
15
+ @board.set_default
16
+ @current_color = :white
17
+ @last_piece = nil
18
+ @name = nil
19
+ @promotion_coord = false
20
+ end
21
+
22
+ def move(string)
23
+ from, to = Game.string_to_move(string)
24
+ piece = @board.at(from)
25
+ raise InvalidMove, "Game is over" if over?
26
+ raise InvalidMove, "#{@current_color} player should execute pawn promotion first" if needs_promotion?
27
+ raise InvalidMove, "Empty square is chosen" if piece.nil?
28
+ raise InvalidMove, "This is not your piece" unless piece.color == @current_color
29
+ raise InvalidMove, "Invalid move" unless valid_moves(from).include?(to)
30
+ move = Move.new(@board, from, to)
31
+ move.commit
32
+ if king_attacked?
33
+ move.rollback
34
+ raise InvalidMove, "Fatal move"
35
+ end
36
+
37
+ @last_piece = piece
38
+ piece.moves_count += 1
39
+ @promotion_coord = to and return if piece.pawn? && [7, 0].include?(to[1])
40
+ next_player
41
+ end
42
+
43
+ def [](str)
44
+ letters = ("a".."h").to_a
45
+ return nil unless /[a-h][1-8]/.match?(str)
46
+ @board.at([letters.find_index(str[0]), str[1].to_i - 1])
47
+ end
48
+
49
+ def draw
50
+ @board.to_s
51
+ end
52
+
53
+ def promotion(class_name)
54
+ unless ["rook", "knight", "elephant", "queen"].include?(class_name.downcase)
55
+ raise InvalidMove, "Invalid promotion"
56
+ end
57
+ @board.set_at(@promotion_coord, Module.const_get("ChessEngine::#{class_name.capitalize}").new(@current_color))
58
+ @promotion_coord = nil
59
+ next_player
60
+ end
61
+
62
+ def castling(length)
63
+ row = @current_color == :white ? 0 : 7
64
+ king = @board.at([4, row])
65
+ if length == :short
66
+ rook = @board.at([7, row])
67
+ line = [5, 6]
68
+ moves = [Move.new(@board, [4, row], [6, row]),
69
+ Move.new(@board, [7, row], [5, row])]
70
+ else
71
+ rook = @board.at([0, row])
72
+ line = [1, 2, 3]
73
+ moves = [Move.new(@board, [4, row], [2, row]),
74
+ Move.new(@board, [0, row], [3, row])]
75
+ end
76
+ raise InvalidMove, "Invalid castling" unless
77
+ king && rook && king.moves_count == 0 && rook.moves_count == 0 &&
78
+ line.all? { |x| @board.at([x, row]).nil? }
79
+
80
+ moves.each { |move| move.commit }
81
+ if king_attacked?
82
+ moves.each { |move| move.rollback }
83
+ raise InvalidMove, "Fatal move"
84
+ end
85
+ @last_piece = nil
86
+ next_player
87
+ end
88
+
89
+ def over?
90
+ @board.piece_coordinates(@current_color).all? do |coord|
91
+ safe_moves(coord).empty?
92
+ end
93
+ end
94
+
95
+ def needs_promotion?
96
+ !!@promotion_coord
97
+ end
98
+
99
+ def check?
100
+ king_attacked?
101
+ end
102
+
103
+ private
104
+
105
+ def king_attacked?
106
+ king_coords = @board.king_coords(@current_color)
107
+ [[1, 1], [-1, 1], [-1, -1], [1, -1]].each do |move|
108
+ next_coords = relative_coords(king_coords, move)
109
+ piece = @board.at(next_coords)
110
+ return true if piece && piece.color != @current_color && (piece.pawn? || piece.king?)
111
+ edge_coords = repeated_move(king_coords, move).last
112
+ piece = edge_coords.nil? ? nil : @board.at(edge_coords)
113
+ return true if piece && piece.beats_diagonally?
114
+ end
115
+ [[1, 0], [-1, 0], [0, 1], [0, -1]].each do |move|
116
+ next_coords = relative_coords(king_coords, move)
117
+ piece = @board.at(next_coords)
118
+ return true if piece && piece.king?
119
+ edge_coords = repeated_move(king_coords, move).last
120
+ piece = edge_coords.nil? ? nil : @board.at(edge_coords)
121
+ return true if !piece.nil? && piece.beats_straight?
122
+ end
123
+ [[1, 2], [2, 1], [1, -2], [-2, 1],
124
+ [-1, 2], [2, -1], [-1, -2], [-2, -1]].each do |move|
125
+ coords = relative_coords(king_coords, move)
126
+ piece = valid_move?(coords) ? @board.at(coords) : nil
127
+ return true if !piece.nil? && piece.knight?
128
+ end
129
+ false
130
+ end
131
+
132
+ def Game.string_to_move(string)
133
+ string = string.gsub(/\s+/, "").downcase
134
+ raise InvalidMove, "Input must look like \"e2 e4\" or \"a6b5\"" unless /^[a-h][1-8][a-h][1-8]$/.match?(string)
135
+ letters = ("a".."h").to_a
136
+ [[letters.find_index(string[0]), string[1].to_i - 1],
137
+ [letters.find_index(string[2]), string[3].to_i - 1]]
138
+ end
139
+
140
+ def opposite_color
141
+ @current_color == :white ? :black : :white
142
+ end
143
+
144
+ def next_player
145
+ @current_color = opposite_color
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,19 @@
1
+ module Input
2
+ class Incorrect < StandardError; end
3
+
4
+ def get_input(input_message, regex = nil, err_message = "Incorrect input, try again")
5
+ begin
6
+ print input_message
7
+ input = gets.chomp
8
+ if block_given?
9
+ raise Input::Incorrect unless yield(input)
10
+ else
11
+ raise Input::Incorrect unless regex.match?(input)
12
+ end
13
+ rescue
14
+ puts err_message
15
+ retry
16
+ end
17
+ input
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ module ChessEngine
2
+ class Move
3
+ def initialize(board, from, to)
4
+ @board = board
5
+ @from = from
6
+ @to = to
7
+ @original_squares = []
8
+ @original_squares << {coord: from, piece: board.at(from)}
9
+ @original_squares << {coord: to, piece: board.at(to)}
10
+ if en_passant?
11
+ @en_passant_coord = [to[0], from[1]]
12
+ @original_squares << {coord: @en_passant_coord, piece: board.at(@en_passant_coord)}
13
+ end
14
+ end
15
+
16
+ def commit
17
+ if en_passant?
18
+ @board.set_at(@en_passant_coord, nil)
19
+ end
20
+ @board.move_piece(@from, @to)
21
+ end
22
+
23
+ def rollback
24
+ @original_squares.each do |square|
25
+ @board.set_at(square[:coord], square[:piece])
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def en_passant?
32
+ @board.at(@from).pawn? && @from[0] != @to[0] && @board.at(@to).nil?
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,97 @@
1
+ module ChessEngine
2
+ class Piece
3
+ attr_reader :symbol, :color
4
+ attr_accessor :moves_count
5
+
6
+ def initialize(color)
7
+ @color = color
8
+ @moves_count = 0
9
+ end
10
+
11
+ def inspect
12
+ "#{self.class}:#{@color}"
13
+ end
14
+
15
+ def beats_diagonally?
16
+ elephant? || queen?
17
+ end
18
+
19
+ def beats_straight?
20
+ rook? || queen?
21
+ end
22
+
23
+ ["knight", "king", "pawn", "rook", "queen", "elephant"].each do |piece|
24
+ define_method(:"#{piece}?") do
25
+ self.class.to_s == "ChessEngine::#{piece.capitalize}"
26
+ end
27
+ end
28
+ end
29
+
30
+ class Elephant < Piece
31
+ def initialize(color)
32
+ super
33
+ @symbol = (@color == :black) ? "\u25B2" : "\u25B3"
34
+ end
35
+
36
+ def moves
37
+ [[1, 1], [1, -1], [-1, 1], [-1, -1]]
38
+ end
39
+ end
40
+
41
+ class King < Piece
42
+ def initialize(color)
43
+ super
44
+ @symbol = (@color == :black) ? "\u265A" : "\u2654"
45
+ end
46
+
47
+ def moves
48
+ [[0, 1], [0, -1], [1, 0], [-1, 0],
49
+ [1, 1], [1, -1], [-1, 1], [-1, -1]]
50
+ end
51
+ end
52
+
53
+ class Knight < Piece
54
+ def initialize(color)
55
+ super
56
+ @symbol = (@color == :black) ? "\u265E" : "\u2658"
57
+ end
58
+
59
+ def moves
60
+ [[1, 2], [2, 1], [1, -2], [-2, 1],
61
+ [-1, 2], [2, -1], [-1, -2], [-2, -1]]
62
+ end
63
+ end
64
+
65
+ class Pawn < Piece
66
+ attr_reader :direction
67
+
68
+ def initialize(color)
69
+ super
70
+ @symbol = (@color == :black) ? "\u265F" : "\u2659"
71
+ @direction = (@color == :white) ? 1 : -1
72
+ end
73
+ end
74
+
75
+ class Queen < Piece
76
+ def initialize(color)
77
+ super
78
+ @symbol = (@color == :black) ? "\u265B" : "\u2655"
79
+ end
80
+
81
+ def moves
82
+ [[0, 1], [0, -1], [1, 0], [-1, 0],
83
+ [1, 1], [1, -1], [-1, 1], [-1, -1]]
84
+ end
85
+ end
86
+
87
+ class Rook < Piece
88
+ def initialize(color)
89
+ super
90
+ @symbol = (@color == :black) ? "\u265C" : "\u2656"
91
+ end
92
+
93
+ def moves
94
+ [[1, 0], [0, 1], [-1, 0], [0, -1]]
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,87 @@
1
+ module ChessEngine
2
+ module MoveValidator
3
+ def safe_moves(from)
4
+ valid_moves(from).reject { |move| fatal_move?(from, move) }
5
+ end
6
+
7
+ def valid_moves(from)
8
+ piece = @board.at(from)
9
+ if piece.king? || piece.knight?
10
+ piece.moves.map do |move|
11
+ to = relative_coords(from, move)
12
+ to if valid_move?(to)
13
+ end.compact
14
+ elsif piece.pawn?
15
+ pawn_valid_moves(from)
16
+ else
17
+ valid_moves_recursive(from)
18
+ end
19
+ end
20
+
21
+ def valid_moves_recursive(from)
22
+ piece = @board.at(from)
23
+ piece.moves.inject([]) do |valid_moves, move|
24
+ valid_moves.push(*repeated_move(from, move))
25
+ end
26
+ end
27
+
28
+ def repeated_move(from, move, valid_moves = [])
29
+ coordinates = relative_coords(from, move)
30
+ return valid_moves unless valid_move?(coordinates)
31
+ return valid_moves << coordinates unless @board.at(coordinates).nil?
32
+ repeated_move(coordinates, move, valid_moves << coordinates)
33
+ end
34
+
35
+ def relative_coords(from, move)
36
+ [from[0] + move[0], from[1] + move[1]]
37
+ end
38
+
39
+ def valid_move?(coordinates)
40
+ if @board.exists_at?(coordinates)
41
+ piece = @board.at(coordinates)
42
+ return (piece.nil? || piece.color != @current_color)
43
+ end
44
+ return false
45
+ end
46
+
47
+ def fatal_move?(from, to)
48
+ is_fatal = false
49
+ move = Move.new(@board, from, to)
50
+ move.commit
51
+ is_fatal = true if king_attacked?
52
+ move.rollback
53
+ is_fatal
54
+ end
55
+
56
+ def pawn_valid_moves(from)
57
+ pawn = @board.at(from)
58
+ direction = pawn.direction
59
+ moves = []
60
+ next_coords = relative_coords(from, [0, direction])
61
+ jump_coords = relative_coords(from, [0, direction * 2])
62
+ take_coords = [relative_coords(from, [1, direction]),
63
+ relative_coords(from, [-1, direction])]
64
+ if @board.exists_at?(next_coords) && @board.at(next_coords).nil?
65
+ moves << next_coords
66
+ moves << jump_coords unless pawn.moves_count > 0 || @board.at(jump_coords)
67
+ end
68
+ take_coords.each do |coords|
69
+ moves << coords if @board.at(coords) && @board.at(coords).color != pawn.color
70
+ end
71
+ en_passant_coords(from) ? moves << en_passant_coords(from) : moves
72
+ end
73
+
74
+ def en_passant_coords(from)
75
+ pawn = @board.at(from)
76
+ [1, -1].each do |x|
77
+ next_coords = [from[0] + x, from[1]]
78
+ next_piece = @board.at(next_coords)
79
+ if next_piece.class == Pawn && next_piece == @last_piece &&
80
+ next_piece.moves_count == 1 && from[1].between?(3, 4)
81
+ return [from[0] + x, from[1] + pawn.direction]
82
+ end
83
+ end
84
+ nil
85
+ end
86
+ end
87
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chess_engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anikeev Gennadiy
@@ -20,6 +20,13 @@ extra_rdoc_files: []
20
20
  files:
21
21
  - bin/chess_engine
22
22
  - lib/chess_engine.rb
23
+ - lib/chess_engine/board.rb
24
+ - lib/chess_engine/cli.rb
25
+ - lib/chess_engine/game.rb
26
+ - lib/chess_engine/input.rb
27
+ - lib/chess_engine/move.rb
28
+ - lib/chess_engine/piece.rb
29
+ - lib/chess_engine/validator.rb
23
30
  - spec/board_spec.rb
24
31
  - spec/game_helper.rb
25
32
  - spec/game_spec.rb