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.
@@ -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