checkers-game 0.1.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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +42 -0
- data/.tool-versions +1 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/LICENSE.txt +21 -0
- data/README.md +50 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/checkers-game.gemspec +30 -0
- data/exe/checkers +37 -0
- data/gems.locked +59 -0
- data/gems.rb +12 -0
- data/lib/checkers.rb +16 -0
- data/lib/checkers/ai/engine/alphabeta.rb +35 -0
- data/lib/checkers/ai/engine/base.rb +37 -0
- data/lib/checkers/ai/engine/minmax.rb +37 -0
- data/lib/checkers/ai/node.rb +31 -0
- data/lib/checkers/ai/tree.rb +29 -0
- data/lib/checkers/board.rb +146 -0
- data/lib/checkers/board/moves.rb +33 -0
- data/lib/checkers/board/score.rb +72 -0
- data/lib/checkers/game/engine.rb +28 -0
- data/lib/checkers/game/state.rb +25 -0
- data/lib/checkers/gui.rb +9 -0
- data/lib/checkers/gui/scene.rb +58 -0
- data/lib/checkers/gui/scene/board.rb +108 -0
- data/lib/checkers/gui/scene/piece_animation.rb +61 -0
- data/lib/checkers/jump_move.rb +18 -0
- data/lib/checkers/move.rb +16 -0
- data/lib/checkers/ruby2d/piece.rb +29 -0
- data/lib/checkers/ruby2d/square_with_piece.rb +28 -0
- data/lib/checkers/version.rb +5 -0
- metadata +124 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkers
|
4
|
+
module AI
|
5
|
+
module Engine
|
6
|
+
class Base
|
7
|
+
attr_reader :tree_depth
|
8
|
+
|
9
|
+
def initialize(tree_depth = 3)
|
10
|
+
@tree_depth = tree_depth
|
11
|
+
end
|
12
|
+
|
13
|
+
def next_board(board)
|
14
|
+
if board.jumped
|
15
|
+
Board.generate_boards(board, :ai).first
|
16
|
+
else
|
17
|
+
decision_tree_root = Tree.build(board, tree_depth).root
|
18
|
+
|
19
|
+
yield(decision_tree_root, tree_depth)
|
20
|
+
|
21
|
+
decision_tree_root.children.max_by(&:score).board
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def max(a, b)
|
28
|
+
a > b ? a : b
|
29
|
+
end
|
30
|
+
|
31
|
+
def min(a, b)
|
32
|
+
a < b ? a : b
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkers
|
4
|
+
module AI
|
5
|
+
module Engine
|
6
|
+
class Minmax < Base
|
7
|
+
def next_board(board)
|
8
|
+
super(board) { |root, tree_depth| minmax(root, tree_depth, true) }
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def minmax(node, tree_depth, maxplayer)
|
14
|
+
return node.score if tree_depth.zero? || node.children_size.zero?
|
15
|
+
|
16
|
+
value = nil
|
17
|
+
|
18
|
+
if maxplayer
|
19
|
+
value = Float::MIN
|
20
|
+
|
21
|
+
node.children.each do |child|
|
22
|
+
value = max(value, minmax(child, tree_depth - 1, !maxplayer))
|
23
|
+
end
|
24
|
+
else
|
25
|
+
value = Float::MAX
|
26
|
+
|
27
|
+
node.children.each do |child|
|
28
|
+
value = min(value, minmax(child, tree_depth - 1, !maxplayer))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
node.score = value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkers
|
4
|
+
module AI
|
5
|
+
class Node
|
6
|
+
attr_reader :children, :player, :board
|
7
|
+
attr_accessor :score
|
8
|
+
|
9
|
+
def initialize(board, player, depth = 0)
|
10
|
+
@board = board
|
11
|
+
@player = player
|
12
|
+
@score = board.calculate_score(player: player)
|
13
|
+
@children = generate_children(depth)
|
14
|
+
end
|
15
|
+
|
16
|
+
def children_size
|
17
|
+
@children.size
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def generate_children(depth)
|
23
|
+
return [] if depth.zero?
|
24
|
+
|
25
|
+
plays_next = player == :human ? :ai : :human
|
26
|
+
boards = Board.generate_boards(board, player)
|
27
|
+
boards.map { |board| Node.new(board, plays_next, depth - 1) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkers
|
4
|
+
module AI
|
5
|
+
class Tree
|
6
|
+
attr_reader :root
|
7
|
+
|
8
|
+
def self.build(board, depth)
|
9
|
+
Tree.new(Node.new(board, :ai, depth))
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(node)
|
13
|
+
@root = node
|
14
|
+
end
|
15
|
+
|
16
|
+
def depth
|
17
|
+
current_depth = 0
|
18
|
+
children = @root.children
|
19
|
+
|
20
|
+
while children.any?
|
21
|
+
current_depth += 1
|
22
|
+
children = children.first.children
|
23
|
+
end
|
24
|
+
|
25
|
+
current_depth
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkers
|
4
|
+
HUMAN_PIECE = -1
|
5
|
+
HUMAN_KING = -2
|
6
|
+
HUMAN_PIECES = [HUMAN_PIECE, HUMAN_KING].freeze
|
7
|
+
|
8
|
+
AI_PIECE = 1
|
9
|
+
AI_KING = 2
|
10
|
+
AI_PIECES = [AI_PIECE, AI_KING].freeze
|
11
|
+
|
12
|
+
class Board
|
13
|
+
include Score
|
14
|
+
include Moves
|
15
|
+
extend Forwardable
|
16
|
+
|
17
|
+
attr_reader :board, :jumped, :last_move
|
18
|
+
|
19
|
+
def_delegators :board, :each_with_index, :row_count
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def generate_boards(board_object, player)
|
23
|
+
moves = board_object.find_moves_for_player(player: player)
|
24
|
+
moves.map do |move|
|
25
|
+
make_move(board_object, move)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def make_move(board_object, move)
|
30
|
+
new_board = board_object.board.dup
|
31
|
+
jumped = false
|
32
|
+
|
33
|
+
new_board[*move.end_square] = if move.end_square[0].zero? && new_board[*move.start_square] == HUMAN_PIECE
|
34
|
+
HUMAN_KING
|
35
|
+
elsif move.end_square[0] == 7 && new_board[*move.start_square] == AI_PIECE
|
36
|
+
AI_KING
|
37
|
+
else
|
38
|
+
new_board[*move.start_square]
|
39
|
+
end
|
40
|
+
new_board[*move.start_square] = 0
|
41
|
+
|
42
|
+
if move.is_a?(JumpMove)
|
43
|
+
new_board[*move.jump_over_square] = 0
|
44
|
+
jumped = true
|
45
|
+
end
|
46
|
+
Board.new(board: new_board, jumped: jumped, last_move: move)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def initialize(board: nil, jumped: false, last_move: nil)
|
51
|
+
@board = board || set_board
|
52
|
+
@jumped = jumped
|
53
|
+
@last_move = last_move
|
54
|
+
end
|
55
|
+
|
56
|
+
def calculate_score(player:)
|
57
|
+
number_of_pieces(player: player) +
|
58
|
+
number_of_pieces_on_opponets_side(player: player) +
|
59
|
+
3 * movable_pieces(player: player) +
|
60
|
+
4 * number_of_unoccupied_promotion_squares(player: player)
|
61
|
+
end
|
62
|
+
|
63
|
+
def count_pieces(player:)
|
64
|
+
board.count { |piece| player_pieces(player).include?(piece) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def find_moves_for_player(player:)
|
68
|
+
found_moves = []
|
69
|
+
@board.each_with_index do |e, row, col|
|
70
|
+
next unless player_pieces(player).include?(e)
|
71
|
+
|
72
|
+
moves = find_available_moves(row: row, col: col, player: player)
|
73
|
+
found_moves += moves
|
74
|
+
break found_moves = moves if moves.any? { |move| move.is_a?(JumpMove) }
|
75
|
+
end
|
76
|
+
found_moves
|
77
|
+
end
|
78
|
+
|
79
|
+
def any_jump_moves?(player:)
|
80
|
+
find_moves_for_player(player: player).one? { |move| move.is_a?(JumpMove) }
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
def player_pieces(player)
|
86
|
+
player == :human ? HUMAN_PIECES : AI_PIECES
|
87
|
+
end
|
88
|
+
|
89
|
+
def adjacent_squares(row:, col:, player:)
|
90
|
+
possible_squares(row: row, col: col, player: player) do |squares|
|
91
|
+
squares.select { |square| within_board?(row: square[0], col: square[1]) }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def movable_squares(row:, col:, player:)
|
96
|
+
possible_squares(row: row, col: col, player: player) do |squares|
|
97
|
+
squares.select { |square| move?(row: square[0], col: square[1]) }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def possible_squares(row:, col:, player:)
|
102
|
+
move_up = [[row - 1, col + 1], [row - 1, col - 1]]
|
103
|
+
move_down = [[row + 1, col + 1], [row + 1, col - 1]]
|
104
|
+
if player == :human
|
105
|
+
if @board[row, col] == HUMAN_PIECE
|
106
|
+
yield move_up
|
107
|
+
else
|
108
|
+
yield(move_up + move_down)
|
109
|
+
end
|
110
|
+
else
|
111
|
+
if @board[row, col] == AI_PIECE
|
112
|
+
yield move_down
|
113
|
+
else
|
114
|
+
yield(move_down + move_up)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def move?(row:, col:)
|
122
|
+
within_board?(row: row, col: col) && square_empty?(row: row, col: col)
|
123
|
+
end
|
124
|
+
|
125
|
+
def within_board?(row:, col:)
|
126
|
+
row <= 7 && row >= 0 && col <= 7 && col >= 0
|
127
|
+
end
|
128
|
+
|
129
|
+
def square_empty?(row:, col:)
|
130
|
+
@board[row, col].zero?
|
131
|
+
end
|
132
|
+
|
133
|
+
def set_board
|
134
|
+
Matrix[
|
135
|
+
[0, 1, 0, 1, 0, 1, 0, 1],
|
136
|
+
[1, 0, 1, 0, 1, 0, 1, 0],
|
137
|
+
[0, 1, 0, 1, 0, 1, 0, 1],
|
138
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
139
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
140
|
+
[-1, 0, -1, 0, -1, 0, -1, 0],
|
141
|
+
[0, -1, 0, -1, 0, -1, 0, -1],
|
142
|
+
[-1, 0, -1, 0, -1, 0, -1, 0]
|
143
|
+
]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkers
|
4
|
+
class Board
|
5
|
+
module Moves
|
6
|
+
def find_available_moves(row:, col:, player:)
|
7
|
+
jumps = jump_moves(row: row, col: col, player: player)
|
8
|
+
return jumps if jumps.any?
|
9
|
+
|
10
|
+
basic_moves(row: row, col: col, player: player)
|
11
|
+
end
|
12
|
+
|
13
|
+
def jump_moves(row:, col:, player:)
|
14
|
+
jump_moves = []
|
15
|
+
adjacent_squares(row: row, col: col, player: player).each do |square|
|
16
|
+
adjacent_row, adjacent_col = square
|
17
|
+
next if [0, *player_pieces(player)].include?(@board[adjacent_row, adjacent_col])
|
18
|
+
|
19
|
+
vector = [adjacent_row - row, adjacent_col - col]
|
20
|
+
jump_square = [adjacent_row + vector[0], adjacent_col + vector[1]]
|
21
|
+
jump_moves << JumpMove.new([row, col], jump_square) if move?(row: jump_square[0], col: jump_square[1])
|
22
|
+
end
|
23
|
+
jump_moves
|
24
|
+
end
|
25
|
+
|
26
|
+
def basic_moves(row:, col:, player:)
|
27
|
+
possible_squares(row: row, col: col, player: player) do |squares|
|
28
|
+
squares.filter_map { |square| Move.new([row, col], square) if move?(row: square[0], col: square[1]) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkers
|
4
|
+
class Board
|
5
|
+
module Score
|
6
|
+
def number_of_pieces(player:)
|
7
|
+
opponent = opponent(player)
|
8
|
+
player_pieces = board.count { |piece| player_pieces(player).include?(piece) }
|
9
|
+
opponent_pieces = board.count { |piece| player_pieces(opponent).include?(piece) }
|
10
|
+
opponent_pieces - player_pieces
|
11
|
+
end
|
12
|
+
|
13
|
+
def number_of_pieces_on_opponets_side(player:)
|
14
|
+
opponent = opponent(player)
|
15
|
+
|
16
|
+
player_pieces = board.each_with_index.count do |piece, row, _col|
|
17
|
+
next if row >= 3
|
18
|
+
|
19
|
+
player_pieces(player).include?(piece)
|
20
|
+
end
|
21
|
+
|
22
|
+
opponent_pieces = board.each_with_index.count do |piece, row, _col|
|
23
|
+
next unless row == 5
|
24
|
+
|
25
|
+
player_pieces(opponent).include?(piece)
|
26
|
+
end
|
27
|
+
|
28
|
+
opponent_pieces - player_pieces
|
29
|
+
end
|
30
|
+
|
31
|
+
# for kings implementation
|
32
|
+
def number_of_unoccupied_promotion_squares(player:)
|
33
|
+
opponent = opponent(player)
|
34
|
+
opponent_squares = 0
|
35
|
+
player_squares = 0
|
36
|
+
|
37
|
+
board.each_with_index do |square, row, _col|
|
38
|
+
next if player_pieces(player).include?(square) || player_pieces(opponent).include?(square)
|
39
|
+
|
40
|
+
opponent_squares += 1 if row == 7
|
41
|
+
player_squares += 1 if row.zero?
|
42
|
+
end
|
43
|
+
|
44
|
+
opponent_squares - player_squares
|
45
|
+
end
|
46
|
+
|
47
|
+
def movable_pieces(player:)
|
48
|
+
opponent = opponent(player)
|
49
|
+
opponent_pieces = 0
|
50
|
+
player_pieces = 0
|
51
|
+
|
52
|
+
board.each_with_index do |piece, row, col|
|
53
|
+
next if piece.zero?
|
54
|
+
|
55
|
+
if player_pieces(player).include?(piece) && movable_squares(row: row, col: col, player: player).any?
|
56
|
+
player_pieces += 1
|
57
|
+
elsif movable_squares(row: row, col: col, player: opponent).any?
|
58
|
+
opponent_pieces += 1
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
opponent_pieces - player_pieces
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def opponent(player)
|
68
|
+
player == :human ? :ai : :human
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkers
|
4
|
+
module Game
|
5
|
+
class Engine
|
6
|
+
def initialize(state, ai_engine)
|
7
|
+
@state = state
|
8
|
+
@ai = ai_engine
|
9
|
+
end
|
10
|
+
|
11
|
+
def play
|
12
|
+
return if @state.winner || @state.tie
|
13
|
+
|
14
|
+
if @state.turn == :ai
|
15
|
+
new_board = @ai.next_board(@state.board)
|
16
|
+
|
17
|
+
turn = if new_board.jumped
|
18
|
+
new_board.any_jump_moves?(player: :ai) ? :ai : :human
|
19
|
+
else
|
20
|
+
:human
|
21
|
+
end
|
22
|
+
|
23
|
+
@state.set_state(board: new_board, turn: turn)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkers
|
4
|
+
module Game
|
5
|
+
class State
|
6
|
+
include Observable
|
7
|
+
|
8
|
+
attr_accessor :winner, :tie, :board, :turn
|
9
|
+
private :winner=, :tie=, :board=, :turn=
|
10
|
+
|
11
|
+
def initialize(turn)
|
12
|
+
@board = Board.new
|
13
|
+
@turn = turn
|
14
|
+
@winner = nil
|
15
|
+
@tie = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def set_state(attrs = {})
|
19
|
+
changed
|
20
|
+
attrs.each { |attr, value| send("#{attr}=", value) }
|
21
|
+
notify_observers
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|