ttt-core 1.0.0 → 2.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 +4 -4
- data/lib/ai_player.rb +117 -117
- data/lib/board.rb +1 -1
- data/lib/game.rb +14 -6
- data/lib/player_options.rb +1 -1
- data/lib/ttt-core/version.rb +1 -1
- data/spec/board_spec.rb +2 -2
- data/spec/game_spec.rb +30 -6
- data/spec/player_options_spec.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 824e96f3c46aab6ab11e4d1fdca9771b30656d8e
|
|
4
|
+
data.tar.gz: 436d80f07cb56f402e20f3d258df8d4315270c75
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 066027d759aaee23a10b626a2e86c35884aebb5ad1d6f4191479f04726a2d71d562a2b6b0622f1eedb927b731836ff4fb47e3168c5a064ef279dc3b62ab5a6db
|
|
7
|
+
data.tar.gz: 91596679e33a8026f5d1c7e84bc2dfa75869cd7f3b4241c68f19d5908695ae6055091bb17416f523a6000a742df4c7aec3b0b83812619117b1037203ff070b62
|
data/lib/ai_player.rb
CHANGED
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
require 'player_symbols'
|
|
2
|
-
require 'player'
|
|
3
|
-
|
|
4
|
-
class AiPlayer
|
|
5
|
-
include Player
|
|
6
|
-
|
|
7
|
-
def initialize(symbol)
|
|
8
|
-
super(symbol)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def choose_move(board)
|
|
12
|
-
minimax(board, true, board.vacant_indices.size, ALPHA, BETA).first[1]
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def ready?
|
|
16
|
-
true
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def minimax(board, is_max_player, depth, alpha, beta)
|
|
20
|
-
best_score_so_far = initial_score(is_max_player)
|
|
21
|
-
|
|
22
|
-
if round_is_over(board, depth)
|
|
23
|
-
return score(board, depth)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
board.vacant_indices.each do |i|
|
|
27
|
-
new_board = board.make_move(i, current_players_symbol(is_max_player))
|
|
28
|
-
result = minimax(new_board, !is_max_player, depth - 1, alpha, beta)
|
|
29
|
-
best_score_so_far = update_score(is_max_player, i, best_score_so_far, score_from(result))
|
|
30
|
-
|
|
31
|
-
alpha = update_alpha(is_max_player, best_score_so_far, alpha)
|
|
32
|
-
beta = update_beta(is_max_player, best_score_so_far, beta)
|
|
33
|
-
|
|
34
|
-
if alpha > beta
|
|
35
|
-
break
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
best_score_so_far
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
ALPHA = -2
|
|
46
|
-
BETA = 2
|
|
47
|
-
|
|
48
|
-
def update_alpha(is_max_player, best_score_so_far, alpha)
|
|
49
|
-
if is_max_player && score_from(best_score_so_far) > alpha
|
|
50
|
-
alpha = score_from(best_score_so_far)
|
|
51
|
-
end
|
|
52
|
-
alpha
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def score_from(score)
|
|
56
|
-
score.first.first
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def update_beta(is_max_player, best_score_so_far, beta)
|
|
60
|
-
if !is_max_player && score_from(best_score_so_far) < beta
|
|
61
|
-
beta = score_from(best_score_so_far)
|
|
62
|
-
end
|
|
63
|
-
beta
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def round_is_over(board, depth)
|
|
67
|
-
@winner = board.winning_symbol
|
|
68
|
-
!@winner.nil? || depth == 0
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def score(board, depth)
|
|
72
|
-
if @winner.nil?
|
|
73
|
-
return {0 => nil}
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
if maximizing_player_won?(@winner)
|
|
77
|
-
return {(1 + depth) => nil}
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
if minimizing_player_won?(@winner)
|
|
81
|
-
return {(-1 - depth) => nil}
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def maximizing_player_won?(winners_symbol)
|
|
86
|
-
symbols_are_equal?(winners_symbol, game_symbol)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def minimizing_player_won?(winners_symbol)
|
|
90
|
-
symbols_are_equal?(winners_symbol, PlayerSymbols::opponent(game_symbol))
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def symbols_are_equal?(symbol1, symbol2)
|
|
94
|
-
symbol1 == symbol2
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def initial_score(is_max_player)
|
|
98
|
-
if is_max_player
|
|
99
|
-
best_score_so_far = {ALPHA => nil}
|
|
100
|
-
else
|
|
101
|
-
best_score_so_far = {BETA => nil}
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def current_players_symbol(is_max_player)
|
|
106
|
-
is_max_player == true ? game_symbol : PlayerSymbols::opponent(game_symbol)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def update_score(is_max_player, move, score, result_score)
|
|
110
|
-
if is_max_player && (result_score >= score_from(score))
|
|
111
|
-
return {result_score => move}
|
|
112
|
-
elsif !is_max_player && (result_score < score_from(score))
|
|
113
|
-
return {result_score => move}
|
|
114
|
-
end
|
|
115
|
-
return score
|
|
116
|
-
end
|
|
117
|
-
end
|
|
1
|
+
require 'player_symbols'
|
|
2
|
+
require 'player'
|
|
3
|
+
|
|
4
|
+
class AiPlayer
|
|
5
|
+
include Player
|
|
6
|
+
|
|
7
|
+
def initialize(symbol)
|
|
8
|
+
super(symbol)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def choose_move(board)
|
|
12
|
+
minimax(board, true, board.vacant_indices.size, ALPHA, BETA).first[1]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def ready?
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def minimax(board, is_max_player, depth, alpha, beta)
|
|
20
|
+
best_score_so_far = initial_score(is_max_player)
|
|
21
|
+
|
|
22
|
+
if round_is_over(board, depth)
|
|
23
|
+
return score(board, depth)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
board.vacant_indices.each do |i|
|
|
27
|
+
new_board = board.make_move(i, current_players_symbol(is_max_player))
|
|
28
|
+
result = minimax(new_board, !is_max_player, depth - 1, alpha, beta)
|
|
29
|
+
best_score_so_far = update_score(is_max_player, i, best_score_so_far, score_from(result))
|
|
30
|
+
|
|
31
|
+
alpha = update_alpha(is_max_player, best_score_so_far, alpha)
|
|
32
|
+
beta = update_beta(is_max_player, best_score_so_far, beta)
|
|
33
|
+
|
|
34
|
+
if alpha > beta
|
|
35
|
+
break
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
best_score_so_far
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
ALPHA = -2
|
|
46
|
+
BETA = 2
|
|
47
|
+
|
|
48
|
+
def update_alpha(is_max_player, best_score_so_far, alpha)
|
|
49
|
+
if is_max_player && score_from(best_score_so_far) > alpha
|
|
50
|
+
alpha = score_from(best_score_so_far)
|
|
51
|
+
end
|
|
52
|
+
alpha
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def score_from(score)
|
|
56
|
+
score.first.first
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def update_beta(is_max_player, best_score_so_far, beta)
|
|
60
|
+
if !is_max_player && score_from(best_score_so_far) < beta
|
|
61
|
+
beta = score_from(best_score_so_far)
|
|
62
|
+
end
|
|
63
|
+
beta
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def round_is_over(board, depth)
|
|
67
|
+
@winner = board.winning_symbol
|
|
68
|
+
!@winner.nil? || depth == 0
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def score(board, depth)
|
|
72
|
+
if @winner.nil?
|
|
73
|
+
return {0 => nil}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if maximizing_player_won?(@winner)
|
|
77
|
+
return {(1 + depth) => nil}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if minimizing_player_won?(@winner)
|
|
81
|
+
return {(-1 - depth) => nil}
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def maximizing_player_won?(winners_symbol)
|
|
86
|
+
symbols_are_equal?(winners_symbol, game_symbol)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def minimizing_player_won?(winners_symbol)
|
|
90
|
+
symbols_are_equal?(winners_symbol, PlayerSymbols::opponent(game_symbol))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def symbols_are_equal?(symbol1, symbol2)
|
|
94
|
+
symbol1 == symbol2
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def initial_score(is_max_player)
|
|
98
|
+
if is_max_player
|
|
99
|
+
best_score_so_far = {ALPHA => nil}
|
|
100
|
+
else
|
|
101
|
+
best_score_so_far = {BETA => nil}
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def current_players_symbol(is_max_player)
|
|
106
|
+
is_max_player == true ? game_symbol : PlayerSymbols::opponent(game_symbol)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def update_score(is_max_player, move, score, result_score)
|
|
110
|
+
if is_max_player && (result_score >= score_from(score))
|
|
111
|
+
return {result_score => move}
|
|
112
|
+
elsif !is_max_player && (result_score < score_from(score))
|
|
113
|
+
return {result_score => move}
|
|
114
|
+
end
|
|
115
|
+
return score
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/board.rb
CHANGED
data/lib/game.rb
CHANGED
|
@@ -7,21 +7,29 @@ class Game
|
|
|
7
7
|
|
|
8
8
|
def play
|
|
9
9
|
while game_in_progress?
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
current_player = players[player_symbol]
|
|
11
|
+
@board = board.make_move(current_player.choose_move(board), player_symbol)
|
|
12
12
|
end
|
|
13
13
|
board
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
def play_specific(move)
|
|
17
|
+
@board = board.make_move(move, player_symbol)
|
|
18
|
+
play
|
|
19
|
+
end
|
|
20
|
+
|
|
16
21
|
private
|
|
17
22
|
|
|
18
23
|
attr_reader :board, :players
|
|
19
24
|
|
|
20
|
-
def
|
|
21
|
-
|
|
25
|
+
def player_symbol
|
|
26
|
+
number_of_x = board.grid_for_display.flatten.count(PlayerSymbols::X)
|
|
27
|
+
number_of_o = board.grid_for_display.flatten.count(PlayerSymbols::O)
|
|
28
|
+
next_players_symbol = number_of_x > number_of_o ? PlayerSymbols::O : PlayerSymbols::X
|
|
22
29
|
end
|
|
23
30
|
|
|
24
|
-
def
|
|
25
|
-
players.
|
|
31
|
+
def game_in_progress?
|
|
32
|
+
players[player_symbol].ready? && board.free_spaces? && !board.winning_combination?
|
|
26
33
|
end
|
|
34
|
+
|
|
27
35
|
end
|
data/lib/player_options.rb
CHANGED
data/lib/ttt-core/version.rb
CHANGED
data/spec/board_spec.rb
CHANGED
|
@@ -23,12 +23,12 @@ RSpec.describe Board do
|
|
|
23
23
|
|
|
24
24
|
it "can get symbol at given position" do
|
|
25
25
|
board = Board.new([nil, nil, nil, nil, X, O, nil, nil, nil])
|
|
26
|
-
expect(board.
|
|
26
|
+
expect(board.symbol_at(4)).to be :X
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
it "can be updated at a given position" do
|
|
30
30
|
updated_board = board.make_move(1, X)
|
|
31
|
-
expect(updated_board.
|
|
31
|
+
expect(updated_board.symbol_at(1)).to eq(X)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
it "has free spaces" do
|
data/spec/game_spec.rb
CHANGED
|
@@ -2,11 +2,34 @@ require 'board'
|
|
|
2
2
|
require 'game'
|
|
3
3
|
require 'player'
|
|
4
4
|
require 'player_symbols'
|
|
5
|
+
require 'player_options'
|
|
5
6
|
|
|
6
7
|
RSpec.describe Game do
|
|
7
8
|
let(:player_x_spy) { instance_double(FakePlayer).as_null_object }
|
|
8
9
|
let(:player_o_spy) { instance_double(FakePlayer).as_null_object }
|
|
9
10
|
|
|
11
|
+
it "game is played with specific move" do
|
|
12
|
+
allow(player_o_spy).to receive(:ready?).and_return(false)
|
|
13
|
+
|
|
14
|
+
game = Game.new(Board.new, { PlayerSymbols::X => player_x_spy, PlayerSymbols::O => player_o_spy })
|
|
15
|
+
updated_board = game.play_specific(3)
|
|
16
|
+
|
|
17
|
+
expect(updated_board.symbol_at(3)).to be (PlayerSymbols::X)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "game is started with a specific move" do
|
|
21
|
+
allow(player_x_spy).to receive(:ready?).and_return(false)
|
|
22
|
+
allow(player_o_spy).to receive(:choose_move).and_return(8)
|
|
23
|
+
allow(player_o_spy).to receive(:ready?).and_return(true, false)
|
|
24
|
+
|
|
25
|
+
game = Game.new(Board.new, { PlayerSymbols::X => player_x_spy, PlayerSymbols::O => player_o_spy })
|
|
26
|
+
updated_board = game.play_specific(3)
|
|
27
|
+
|
|
28
|
+
expect(updated_board.symbol_at(3)).to be (PlayerSymbols::X)
|
|
29
|
+
expect(updated_board.symbol_at(8)).to be (PlayerSymbols::O)
|
|
30
|
+
expect(updated_board.vacant_indices.size).to eq 7
|
|
31
|
+
end
|
|
32
|
+
|
|
10
33
|
it "continues until player is not ready" do
|
|
11
34
|
allow(player_x_spy).to receive(:ready?).and_return(true, false)
|
|
12
35
|
allow(player_o_spy).to receive(:ready?).and_return(true)
|
|
@@ -14,31 +37,32 @@ RSpec.describe Game do
|
|
|
14
37
|
expect(player_x_spy).to receive(:choose_move).exactly(1).times.and_return(2)
|
|
15
38
|
expect(player_o_spy).to receive(:choose_move).exactly(1).times.and_return(8)
|
|
16
39
|
|
|
17
|
-
Game.new(Board.new,
|
|
40
|
+
Game.new(Board.new, { PlayerSymbols::X => player_x_spy, PlayerSymbols::O => player_o_spy }).play
|
|
18
41
|
end
|
|
19
42
|
|
|
20
43
|
it "players take turn until there is no space on board" do
|
|
21
44
|
expect(player_x_spy).to receive(:choose_move).exactly(5).times.and_return(0, 1, 4, 5, 6)
|
|
22
45
|
expect(player_o_spy).to receive(:choose_move).exactly(4).times.and_return(2, 3, 7, 8)
|
|
23
46
|
|
|
24
|
-
Game.new(Board.new,
|
|
47
|
+
Game.new(Board.new, { PlayerSymbols::X => player_x_spy, PlayerSymbols::O => player_o_spy }).play
|
|
25
48
|
|
|
26
49
|
end
|
|
27
50
|
|
|
28
51
|
it "game ends when a winning combination is formed" do
|
|
52
|
+
allow(player_x_spy).to receive(:ready?).and_return(true)
|
|
29
53
|
allow(player_x_spy).to receive(:choose_move).once.and_return(2)
|
|
30
54
|
allow(player_x_spy).to receive(:game_symbol).and_return(PlayerSymbols::X)
|
|
31
55
|
|
|
32
|
-
board = Board.new([PlayerSymbols::X, PlayerSymbols::X, nil, PlayerSymbols::O, nil,
|
|
33
|
-
updated_board = Game.new(board,
|
|
56
|
+
board = Board.new([PlayerSymbols::X, PlayerSymbols::X, nil, PlayerSymbols::O, nil, PlayerSymbols::O, nil, nil, nil])
|
|
57
|
+
updated_board = Game.new(board, { PlayerSymbols::X => player_x_spy, PlayerSymbols::O => player_o_spy }).play
|
|
34
58
|
|
|
35
|
-
expect(updated_board.
|
|
59
|
+
expect(updated_board.symbol_at(2)).to be (PlayerSymbols::X)
|
|
36
60
|
expect(player_o_spy).to_not have_received(:choose_move)
|
|
37
61
|
end
|
|
38
62
|
|
|
39
63
|
it "game ends when there are no free spaces on the board" do
|
|
40
64
|
full_board = Board.new([PlayerSymbols::X, PlayerSymbols::X, PlayerSymbols::O, PlayerSymbols::O, PlayerSymbols::O, PlayerSymbols::X, PlayerSymbols::X, PlayerSymbols::O, PlayerSymbols::X])
|
|
41
|
-
game = Game.new(full_board,
|
|
65
|
+
game = Game.new(full_board, { PlayerSymbols::X => player_x_spy, PlayerSymbols::O => player_o_spy }).play
|
|
42
66
|
|
|
43
67
|
expect(player_o_spy).to_not have_received(:choose_move)
|
|
44
68
|
expect(player_x_spy).to_not have_received(:choose_move)
|
data/spec/player_options_spec.rb
CHANGED
|
@@ -19,7 +19,7 @@ RSpec.describe PlayerOptions do
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
it "gets player type for id" do
|
|
22
|
-
expect(PlayerOptions::
|
|
22
|
+
expect(PlayerOptions::player_type_for_id(1)).to eq (PlayerOptions::HUMAN_VS_HUMAN)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
it "gets player options for display" do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ttt-core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Georgina McFadyen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2016-02-
|
|
11
|
+
date: 2016-02-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|