checkers-game 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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