tic_tac_toes 0.0.9 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/bin/tic_tac_toes +2 -6
- data/lib/tic_tac_toes/command_line/menu.rb +3 -3
- data/lib/tic_tac_toes/command_line/runner.rb +6 -4
- data/lib/tic_tac_toes/core/game_state.rb +8 -0
- data/lib/tic_tac_toes/core/history.rb +1 -0
- data/lib/tic_tac_toes/core/move_strategies/easy_ai.rb +2 -4
- data/lib/tic_tac_toes/core/move_strategies/hard_ai.rb +27 -27
- data/lib/tic_tac_toes/core/move_strategies/human.rb +2 -2
- data/lib/tic_tac_toes/core/move_strategies/medium_ai.rb +2 -2
- data/lib/tic_tac_toes/core/player.rb +3 -3
- data/lib/tic_tac_toes/core/rules.rb +1 -1
- data/lib/tic_tac_toes/ui/adapter.rb +5 -5
- data/lib/tic_tac_toes/version.rb +1 -1
- data/spec/tic_tac_toes/command_line/menu_spec.rb +1 -3
- data/spec/tic_tac_toes/command_line/runner_spec.rb +0 -34
- data/spec/tic_tac_toes/core/game_state_spec.rb +54 -7
- data/spec/tic_tac_toes/core/history_spec.rb +4 -4
- data/spec/tic_tac_toes/core/move_strategies/easy_ai_spec.rb +14 -7
- data/spec/tic_tac_toes/core/move_strategies/hard_ai_spec.rb +43 -28
- data/spec/tic_tac_toes/core/move_strategies/human_spec.rb +9 -9
- data/spec/tic_tac_toes/core/move_strategies/medium_ai_spec.rb +14 -9
- data/spec/tic_tac_toes/core/player_spec.rb +17 -14
- data/spec/tic_tac_toes/core/rules_spec.rb +3 -2
- data/spec/tic_tac_toes/ui/adapter_spec.rb +9 -9
- metadata +2 -8
- data/lib/tic_tac_toes/core/board_factory.rb +0 -11
- data/lib/tic_tac_toes/core/game_state_factory.rb +0 -15
- data/spec/tic_tac_toes/core/board_factory_spec.rb +0 -13
- data/spec/tic_tac_toes/core/game_state_factory_spec.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b7b828672ca3ca2908fc62b6f7cf25622a16322
|
4
|
+
data.tar.gz: 704c24b5374e84549277e659401621ee7044ee75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9a5b96876e221e2b43c7e5996be97ea4e14af904017d6f7e5f64e3ae978ab08b71ef3f194ad25ffe690a785ca8a8168943c290b396c6c414a957dd042be566d
|
7
|
+
data.tar.gz: 14ff8a5d98dd83cadd2821a3321a758a716eb81783d617539777093c4eae479a5b7057fb5bb364a169169d089b58bd902a03352feb6f96c8bd5ef62e7133009f
|
data/Gemfile.lock
CHANGED
data/bin/tic_tac_toes
CHANGED
@@ -5,13 +5,11 @@ $LOAD_PATH << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
|
5
5
|
require 'tic_tac_toes/command_line/prompt'
|
6
6
|
require 'tic_tac_toes/core/io'
|
7
7
|
|
8
|
-
require 'tic_tac_toes/core/board_factory'
|
9
8
|
require 'tic_tac_toes/core/player_factory'
|
10
9
|
require 'tic_tac_toes/command_line/menu'
|
11
10
|
|
12
11
|
require 'tic_tac_toes/database/pg_wrapper'
|
13
12
|
require 'tic_tac_toes/core/history'
|
14
|
-
require 'tic_tac_toes/core/game_state_factory'
|
15
13
|
|
16
14
|
require 'tic_tac_toes/command_line/runner'
|
17
15
|
|
@@ -20,12 +18,10 @@ database = "tic_tac_toes"
|
|
20
18
|
prompt = TicTacToes::CommandLine::Prompt
|
21
19
|
io = TicTacToes::Core::IO.new(prompt)
|
22
20
|
|
23
|
-
board_factory = TicTacToes::Core::BoardFactory.new
|
24
21
|
player_factory = TicTacToes::Core::PlayerFactory.new(io)
|
25
|
-
menu = TicTacToes::CommandLine::Menu.new(io,
|
22
|
+
menu = TicTacToes::CommandLine::Menu.new(io, player_factory)
|
26
23
|
|
27
24
|
database_wrapper = TicTacToes::Database::PGWrapper.new(database)
|
28
25
|
history = TicTacToes::Core::History.new(database_wrapper)
|
29
|
-
game_state_factory = TicTacToes::Core::GameStateFactory.new(history)
|
30
26
|
|
31
|
-
TicTacToes::CommandLine::Runner.new(io, menu,
|
27
|
+
TicTacToes::CommandLine::Runner.new(io, menu, history).run
|
@@ -1,16 +1,16 @@
|
|
1
|
+
require 'tic_tac_toes/core/board'
|
1
2
|
require 'tic_tac_toes/core/rules'
|
2
3
|
|
3
4
|
module TicTacToes
|
4
5
|
module CommandLine
|
5
6
|
class Menu
|
6
|
-
def initialize(io,
|
7
|
+
def initialize(io, player_factory)
|
7
8
|
@io = io
|
8
|
-
@board_factory = board_factory
|
9
9
|
@player_factory = player_factory
|
10
10
|
end
|
11
11
|
|
12
12
|
def get_board
|
13
|
-
|
13
|
+
TicTacToes::Core::Board.new(row_size: get_row_size)
|
14
14
|
end
|
15
15
|
|
16
16
|
def get_players
|
@@ -1,15 +1,17 @@
|
|
1
|
+
require 'tic_tac_toes/core/game_state'
|
2
|
+
|
1
3
|
module TicTacToes
|
2
4
|
module CommandLine
|
3
5
|
class Runner
|
4
|
-
def initialize(io, menu,
|
6
|
+
def initialize(io, menu, history)
|
5
7
|
@io = io
|
6
8
|
@menu = menu
|
7
|
-
@
|
9
|
+
@history = history
|
8
10
|
end
|
9
11
|
|
10
12
|
def run
|
11
13
|
board, players = @menu.get_board, @menu.get_players
|
12
|
-
game_state =
|
14
|
+
game_state = TicTacToes::Core::GameState.new(board, players, @history)
|
13
15
|
|
14
16
|
take_turn(game_state) until game_state.game_over?
|
15
17
|
end_game(game_state)
|
@@ -21,7 +23,7 @@ module TicTacToes
|
|
21
23
|
@io.draw_board(game_state.board)
|
22
24
|
@io.thinking_notification if game_state.current_player.needs_to_think
|
23
25
|
|
24
|
-
move = game_state.current_player.place_and_return_move(game_state
|
26
|
+
move = game_state.current_player.place_and_return_move(game_state)
|
25
27
|
game_state.turn_over(move)
|
26
28
|
end
|
27
29
|
|
@@ -14,10 +14,18 @@ module TicTacToes
|
|
14
14
|
@history.record_board_size(@board.size)
|
15
15
|
end
|
16
16
|
|
17
|
+
def place_move(space)
|
18
|
+
@board.place(current_player, space)
|
19
|
+
end
|
20
|
+
|
17
21
|
def current_player
|
18
22
|
@players.first
|
19
23
|
end
|
20
24
|
|
25
|
+
def next_player
|
26
|
+
@players[1]
|
27
|
+
end
|
28
|
+
|
21
29
|
def turn_over(move)
|
22
30
|
@history.record_move(move)
|
23
31
|
@players.rotate!
|
@@ -4,14 +4,18 @@ module TicTacToes
|
|
4
4
|
module Core
|
5
5
|
module MoveStrategies
|
6
6
|
module HardAI
|
7
|
-
|
7
|
+
ALPHA, BETA = -1, 1
|
8
|
+
|
9
|
+
def self.move(game_state)
|
10
|
+
board = game_state.board
|
11
|
+
|
8
12
|
return 0 if nine_board_first_move?(board)
|
9
13
|
return second_move(board) if nine_board_second_move?(board)
|
10
14
|
|
11
15
|
open_spaces = Hash[board.open_spaces.map { |space| [space, nil] }]
|
12
16
|
|
13
17
|
open_spaces.each do |space, score|
|
14
|
-
score = minimax(
|
18
|
+
score = minimax(generate_game_state(space, game_state), :min, ALPHA, BETA)
|
15
19
|
open_spaces[space] = score
|
16
20
|
end
|
17
21
|
|
@@ -21,43 +25,39 @@ module TicTacToes
|
|
21
25
|
|
22
26
|
private
|
23
27
|
|
24
|
-
def self.minimax(
|
25
|
-
return score(
|
28
|
+
def self.minimax(game_state, current_player, alpha, beta)
|
29
|
+
return score(game_state) if game_state.game_over?
|
26
30
|
|
27
31
|
if current_player == :max
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
best_score = [best_score, score].max
|
32
|
+
game_state.board.open_spaces.each do |space|
|
33
|
+
alpha = [alpha, minimax(generate_game_state(space, game_state), :min, alpha, beta)].max
|
34
|
+
break if beta <= alpha
|
32
35
|
end
|
33
|
-
|
34
|
-
|
36
|
+
alpha
|
35
37
|
elsif current_player == :min
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
best_score = [best_score, score].min
|
38
|
+
game_state.board.open_spaces.each do |space|
|
39
|
+
beta = [beta, minimax(generate_game_state(space, game_state), :max, alpha, beta)].min
|
40
|
+
break if beta <= alpha
|
40
41
|
end
|
41
|
-
|
42
|
+
beta
|
42
43
|
end
|
43
44
|
end
|
44
45
|
|
45
|
-
def self.
|
46
|
-
|
47
|
-
|
48
|
-
|
46
|
+
def self.generate_game_state(space, game_state)
|
47
|
+
new_game_state = Marshal.load(Marshal.dump(game_state))
|
48
|
+
new_game_state.place_move(space)
|
49
|
+
new_game_state.turn_over([])
|
50
|
+
new_game_state
|
49
51
|
end
|
50
52
|
|
51
|
-
def self.score(
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
def self.score(game_state)
|
54
|
+
winner = game_state.determine_winner
|
55
|
+
if winner.nil?
|
56
|
+
0
|
57
|
+
elsif winner.move_strategy == self
|
56
58
|
1
|
57
|
-
when opponent_token
|
58
|
-
-1
|
59
59
|
else
|
60
|
-
|
60
|
+
-1
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
@@ -6,13 +6,13 @@ module TicTacToes
|
|
6
6
|
@io = io
|
7
7
|
end
|
8
8
|
|
9
|
-
def move(
|
9
|
+
def move(game_state)
|
10
10
|
@io.move_solicitation
|
11
11
|
|
12
12
|
Integer(@io.solicit_input)
|
13
13
|
rescue ArgumentError
|
14
14
|
@io.not_an_integer_error
|
15
|
-
move(
|
15
|
+
move(game_state)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
@@ -5,9 +5,9 @@ module TicTacToes
|
|
5
5
|
module Core
|
6
6
|
module MoveStrategies
|
7
7
|
module MediumAI
|
8
|
-
def self.move(
|
8
|
+
def self.move(game_state)
|
9
9
|
move_strategy = [EasyAI, HardAI, HardAI].sample
|
10
|
-
move_strategy.move(
|
10
|
+
move_strategy.move(game_state)
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
@@ -10,10 +10,10 @@ module TicTacToes
|
|
10
10
|
@io = io
|
11
11
|
end
|
12
12
|
|
13
|
-
def place_and_return_move(
|
13
|
+
def place_and_return_move(game_state)
|
14
14
|
loop do
|
15
|
-
space = @move_strategy.move(
|
16
|
-
break [@token, space] if
|
15
|
+
space = @move_strategy.move(game_state)
|
16
|
+
break [@token, space] if game_state.place_move(space)
|
17
17
|
@io.invalid_move_error
|
18
18
|
end
|
19
19
|
end
|
@@ -4,7 +4,7 @@ module TicTacToes
|
|
4
4
|
def self.make_move(game_state, move, listener)
|
5
5
|
unless move.nil?
|
6
6
|
move = move.to_i
|
7
|
-
game_state.
|
7
|
+
game_state.place_move(move)
|
8
8
|
end
|
9
9
|
|
10
10
|
if game_state.game_over?
|
@@ -13,7 +13,7 @@ module TicTacToes
|
|
13
13
|
end
|
14
14
|
|
15
15
|
game_state.turn_over(move)
|
16
|
-
game_state.current_player.place_and_return_move(game_state
|
16
|
+
game_state.current_player.place_and_return_move(game_state)
|
17
17
|
|
18
18
|
if game_state.game_over?
|
19
19
|
tell_listener_game_is_over(game_state, listener)
|
@@ -26,13 +26,13 @@ module TicTacToes
|
|
26
26
|
private
|
27
27
|
|
28
28
|
def self.tell_listener_game_is_over(game_state, listener)
|
29
|
-
|
29
|
+
winning_player = game_state.determine_winner
|
30
30
|
|
31
|
-
case
|
31
|
+
case winning_player
|
32
32
|
when nil
|
33
33
|
listener.game_ended_in_draw(game_state)
|
34
34
|
else
|
35
|
-
listener.game_ended_in_winner(game_state,
|
35
|
+
listener.game_ended_in_winner(game_state, winning_player.token)
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
data/lib/tic_tac_toes/version.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'tic_tac_toes/command_line/menu'
|
2
|
-
require 'tic_tac_toes/core/board_factory'
|
3
2
|
require 'tic_tac_toes/core/player_factory'
|
4
3
|
require 'tic_tac_toes/core/io'
|
5
4
|
require 'tic_tac_toes/core/move_strategies/human'
|
@@ -7,9 +6,8 @@ require 'tic_tac_toes/core/move_strategies/medium_ai'
|
|
7
6
|
|
8
7
|
describe TicTacToes::CommandLine::Menu do
|
9
8
|
let(:io) { double }
|
10
|
-
let(:board_factory) { TicTacToes::Core::BoardFactory.new }
|
11
9
|
let(:player_factory) { TicTacToes::Core::PlayerFactory.new(io) }
|
12
|
-
let(:menu) { TicTacToes::CommandLine::Menu.new(io,
|
10
|
+
let(:menu) { TicTacToes::CommandLine::Menu.new(io, player_factory) }
|
13
11
|
|
14
12
|
describe '#get_board' do
|
15
13
|
let(:invalid_row_size) { 11 }
|
@@ -1,38 +1,4 @@
|
|
1
1
|
require 'tic_tac_toes/command_line/runner'
|
2
2
|
|
3
3
|
describe TicTacToes::CommandLine::Runner do
|
4
|
-
describe '#run' do
|
5
|
-
let(:game_state) { double(board: 'board', players: 'players', game_over: true) }
|
6
|
-
|
7
|
-
let(:io) { double(draw_board: true,
|
8
|
-
thinking_notification: true,
|
9
|
-
game_over_notification: true) }
|
10
|
-
let(:menu) { double(get_board: true,
|
11
|
-
get_players: true) }
|
12
|
-
let(:game_state) { double(board: 'board',
|
13
|
-
players: 'players',
|
14
|
-
game_over: true,
|
15
|
-
game_over?: true,
|
16
|
-
determine_winner: true) }
|
17
|
-
let(:game_state_factory) { double(generate_game_state: game_state) }
|
18
|
-
|
19
|
-
let(:runner) { TicTacToes::CommandLine::Runner.new(io, menu, game_state_factory) }
|
20
|
-
|
21
|
-
it 'gets an initial game state' do
|
22
|
-
expect(game_state_factory).to receive(:generate_game_state)
|
23
|
-
runner.run
|
24
|
-
end
|
25
|
-
|
26
|
-
it 'takes turns until the game is over' do
|
27
|
-
allow(game_state).to receive(:game_over?).and_return(false, true)
|
28
|
-
|
29
|
-
expect(runner).to receive(:take_turn).once
|
30
|
-
runner.run
|
31
|
-
end
|
32
|
-
|
33
|
-
it 'ends the game when the game is over' do
|
34
|
-
expect(runner).to receive(:end_game)
|
35
|
-
runner.run
|
36
|
-
end
|
37
|
-
end
|
38
4
|
end
|
@@ -13,6 +13,41 @@ describe TicTacToes::Core::GameState do
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
+
describe '#place_move' do
|
17
|
+
let(:x) { double("human player", token: "x") }
|
18
|
+
let(:o) { double("computer player", token: "o") }
|
19
|
+
let(:players) { [x, o] }
|
20
|
+
|
21
|
+
let(:history) { double(record_board_size: true) }
|
22
|
+
|
23
|
+
it 'returns nil if the board’s space is not nil' do
|
24
|
+
board = TicTacToes::TestBoardGenerator.generate([ x, nil, nil,
|
25
|
+
nil, nil, nil,
|
26
|
+
nil, nil, nil])
|
27
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
28
|
+
|
29
|
+
expect(game_state.place_move(0)).to eq(nil)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'returns nil if it isn’t in its board’s spaces range' do
|
33
|
+
board = TicTacToes::TestBoardGenerator.generate([nil, nil, nil,
|
34
|
+
nil, nil, nil,
|
35
|
+
nil, nil, nil])
|
36
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
37
|
+
expect(game_state.place_move(9)).to eq(nil)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'places the current player at the space if the space is nil and in the board’s spaces range' do
|
41
|
+
board = TicTacToes::TestBoardGenerator.generate([nil, nil, nil,
|
42
|
+
nil, nil, nil,
|
43
|
+
nil, nil, nil])
|
44
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
45
|
+
|
46
|
+
game_state.place_move(0)
|
47
|
+
expect(game_state.board.space(0)).to eq(game_state.current_player)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
16
51
|
describe '#current_player' do
|
17
52
|
it 'returns the first item of its players array' do
|
18
53
|
history = double(record_board_size: true)
|
@@ -24,6 +59,17 @@ describe TicTacToes::Core::GameState do
|
|
24
59
|
end
|
25
60
|
end
|
26
61
|
|
62
|
+
describe '#next_player' do
|
63
|
+
it 'returns the second item of its players array' do
|
64
|
+
history = double(record_board_size: true)
|
65
|
+
players = ['first_player', 'second_player']
|
66
|
+
game_state = TicTacToes::Core::GameState.new('board', players, history)
|
67
|
+
|
68
|
+
next_player = game_state.next_player
|
69
|
+
expect(next_player).to eq('second_player')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
27
73
|
describe '#turn_over' do
|
28
74
|
it 'records the last move' do
|
29
75
|
move = double
|
@@ -47,21 +93,21 @@ describe TicTacToes::Core::GameState do
|
|
47
93
|
|
48
94
|
describe '#game_over' do
|
49
95
|
it 'records the winner' do
|
50
|
-
|
96
|
+
winning_player = double
|
51
97
|
history = double(record_board_size: true, persist: true)
|
52
98
|
game_state = TicTacToes::Core::GameState.new('board', 'players', history)
|
53
99
|
|
54
|
-
expect(history).to receive(:record_winner).with(
|
55
|
-
game_state.game_over(
|
100
|
+
expect(history).to receive(:record_winner).with(winning_player)
|
101
|
+
game_state.game_over(winning_player)
|
56
102
|
end
|
57
103
|
|
58
104
|
it 'persists its history' do
|
59
|
-
|
105
|
+
winning_player = double(token: 'x')
|
60
106
|
history = double(record_board_size: true, record_winner: true)
|
61
107
|
game_state = TicTacToes::Core::GameState.new('board', 'players', history)
|
62
108
|
|
63
109
|
expect(history).to receive(:persist)
|
64
|
-
game_state.game_over(
|
110
|
+
game_state.game_over(winning_player)
|
65
111
|
end
|
66
112
|
end
|
67
113
|
|
@@ -76,6 +122,7 @@ describe TicTacToes::Core::GameState do
|
|
76
122
|
board = TicTacToes::TestBoardGenerator.generate([x, x, o,
|
77
123
|
o, o, nil,
|
78
124
|
x, o, x])
|
125
|
+
|
79
126
|
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
80
127
|
expect(game_state.game_over?).to be false
|
81
128
|
end
|
@@ -113,7 +160,8 @@ describe TicTacToes::Core::GameState do
|
|
113
160
|
winning_token = "o"
|
114
161
|
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
115
162
|
|
116
|
-
|
163
|
+
winning_player = game_state.determine_winner
|
164
|
+
expect(winning_player.token).to eq(winning_token)
|
117
165
|
end
|
118
166
|
|
119
167
|
it "returns nil if there is not a winner" do
|
@@ -125,5 +173,4 @@ describe TicTacToes::Core::GameState do
|
|
125
173
|
expect(game_state.determine_winner).to be_nil
|
126
174
|
end
|
127
175
|
end
|
128
|
-
|
129
176
|
end
|
@@ -23,11 +23,11 @@ describe TicTacToes::Core::History do
|
|
23
23
|
end
|
24
24
|
|
25
25
|
describe '#record_winner' do
|
26
|
-
it "records the passed winner" do
|
27
|
-
|
26
|
+
it "records the token of the passed winner" do
|
27
|
+
winner = double(token: 'O')
|
28
28
|
|
29
|
-
history.record_winner(
|
30
|
-
expect(history.winner).to eq(token)
|
29
|
+
history.record_winner(winner)
|
30
|
+
expect(history.winner).to eq(winner.token)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
@@ -1,18 +1,25 @@
|
|
1
1
|
require 'tic_tac_toes/test_board_generator'
|
2
|
+
require 'tic_tac_toes/core/game_state'
|
2
3
|
require 'tic_tac_toes/core/move_strategies/easy_ai'
|
4
|
+
require 'tic_tac_toes/core/player_factory'
|
3
5
|
|
4
6
|
describe TicTacToes::Core::MoveStrategies::EasyAI do
|
5
7
|
describe '#move' do
|
6
|
-
let(:players) { double("players") }
|
7
|
-
let(:easy_ai) { TicTacToes::Core::MoveStrategies::EasyAI }
|
8
|
-
|
9
8
|
it "returns a randomly-selected valid move" do
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
player_factory = TicTacToes::Core::PlayerFactory.new('unused_io')
|
10
|
+
x = player_factory.generate_human_player('x')
|
11
|
+
o = player_factory.generate_computer_player('o', :hard)
|
12
|
+
players = [x, o]
|
13
|
+
|
14
|
+
board = TicTacToes::TestBoardGenerator.generate([ o, nil, nil,
|
15
|
+
nil, x, nil,
|
16
|
+
nil, x, nil])
|
17
|
+
|
18
|
+
history = double(record_board_size: true)
|
19
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
13
20
|
valid_moves = [1, 2, 3, 5, 6, 8]
|
14
21
|
|
15
|
-
move =
|
22
|
+
move = TicTacToes::Core::MoveStrategies::EasyAI.move(game_state)
|
16
23
|
expect(valid_moves).to include(move)
|
17
24
|
end
|
18
25
|
end
|
@@ -1,129 +1,144 @@
|
|
1
1
|
require 'tic_tac_toes/test_board_generator'
|
2
2
|
require 'tic_tac_toes/core/move_strategies/hard_ai'
|
3
|
+
require 'tic_tac_toes/core/game_state'
|
3
4
|
require 'tic_tac_toes/core/player'
|
4
5
|
|
5
6
|
describe TicTacToes::Core::MoveStrategies::HardAI do
|
7
|
+
let(:alpha) { TicTacToes::Core::MoveStrategies::HardAI::ALPHA }
|
8
|
+
let(:beta) { TicTacToes::Core::MoveStrategies::HardAI::BETA }
|
6
9
|
let(:hard_ai) { TicTacToes::Core::MoveStrategies::HardAI }
|
7
10
|
let(:x) { TicTacToes::Core::Player.new("human", "x", false, "io") }
|
8
11
|
let(:o) { TicTacToes::Core::Player.new(hard_ai, "o", true, "io") }
|
9
12
|
let(:players) { [o, x] }
|
10
|
-
|
13
|
+
let(:history) { TicTacToes::UI::NullHistory.new }
|
11
14
|
|
12
15
|
describe '#move' do
|
13
|
-
it
|
16
|
+
it 'returns the best move' do
|
14
17
|
board = TicTacToes::TestBoardGenerator.generate([x, nil, nil,
|
15
18
|
o, o, nil,
|
16
19
|
x, nil, x])
|
20
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
17
21
|
best_move = 5
|
18
22
|
|
19
|
-
expect(hard_ai.move(
|
23
|
+
expect(hard_ai.move(game_state)).to eql(best_move)
|
20
24
|
end
|
21
25
|
|
22
|
-
context
|
26
|
+
context 'when playing on a 3x3 board' do
|
23
27
|
it 'returns 0 when making the first move' do
|
24
28
|
board = TicTacToes::TestBoardGenerator.generate([nil, nil, nil,
|
25
29
|
nil, nil, nil,
|
26
30
|
nil, nil, nil])
|
31
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
27
32
|
|
28
|
-
expect(hard_ai.move(
|
33
|
+
expect(hard_ai.move(game_state)).to eq(0)
|
29
34
|
end
|
30
35
|
|
31
|
-
it
|
36
|
+
it 'returns 4 when the opponent’s first move was a corner' do
|
32
37
|
board = TicTacToes::TestBoardGenerator.generate([nil, nil, nil,
|
33
38
|
nil, nil, nil,
|
34
39
|
nil, nil, x])
|
40
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
35
41
|
|
36
|
-
expect(hard_ai.move(
|
42
|
+
expect(hard_ai.move(game_state)).to eq(4)
|
37
43
|
end
|
38
44
|
|
39
|
-
it
|
45
|
+
it 'returns 4 when the opponent’s first move was an edge' do
|
40
46
|
board = TicTacToes::TestBoardGenerator.generate([nil, nil, nil,
|
41
47
|
nil, nil, x,
|
42
48
|
nil, nil, nil])
|
49
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
43
50
|
|
44
|
-
expect(hard_ai.move(
|
51
|
+
expect(hard_ai.move(game_state)).to eq(4)
|
45
52
|
end
|
46
53
|
|
47
|
-
it
|
54
|
+
it 'returns 0 when the opponent’s first move was the center' do
|
48
55
|
board = TicTacToes::TestBoardGenerator.generate([nil, nil, nil,
|
49
56
|
nil, x, nil,
|
50
57
|
nil, nil, nil])
|
58
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
51
59
|
|
52
|
-
expect(hard_ai.move(
|
60
|
+
expect(hard_ai.move(game_state)).to eq(0)
|
53
61
|
end
|
54
62
|
end
|
55
63
|
end
|
56
64
|
|
57
65
|
|
58
66
|
describe '#minimax' do
|
59
|
-
it
|
67
|
+
it 'returns the correct score for a pre-win board' do
|
60
68
|
board = TicTacToes::TestBoardGenerator.generate([x, nil, nil,
|
61
69
|
o, o, nil,
|
62
70
|
x, nil, x])
|
71
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
63
72
|
win_score = 1
|
64
73
|
|
65
|
-
expect(hard_ai.minimax(
|
74
|
+
expect(hard_ai.minimax(game_state, :max, alpha, beta)).to eql(win_score)
|
66
75
|
end
|
67
76
|
|
68
|
-
it
|
77
|
+
it 'returns the correct score for a pre-loss board' do
|
69
78
|
board = TicTacToes::TestBoardGenerator.generate([ o, o, x,
|
70
79
|
nil, nil, nil,
|
71
80
|
x, nil, x])
|
81
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
72
82
|
loss_score = -1
|
73
83
|
|
74
|
-
expect(hard_ai.minimax(
|
84
|
+
expect(hard_ai.minimax(game_state, :max, alpha, beta)).to eql(loss_score)
|
75
85
|
end
|
76
86
|
|
77
|
-
it
|
87
|
+
it 'returns the correct score for a pre-draw board' do
|
78
88
|
board = TicTacToes::TestBoardGenerator.generate([x, x, o,
|
79
89
|
o, nil, x,
|
80
90
|
x, o, x])
|
91
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
81
92
|
draw_score = 0
|
82
93
|
|
83
|
-
expect(hard_ai.minimax(
|
94
|
+
expect(hard_ai.minimax(game_state, :max, alpha, beta)).to eql(draw_score)
|
84
95
|
end
|
85
96
|
end
|
86
97
|
|
87
98
|
|
88
|
-
describe '#
|
89
|
-
it
|
90
|
-
|
99
|
+
describe '#generate_game_state' do
|
100
|
+
it 'returns a game state based on a space and an existing game state' do
|
101
|
+
space, token = 3, 'o'
|
91
102
|
board = TicTacToes::TestBoardGenerator.generate([ x, nil, nil,
|
92
103
|
nil, o, nil,
|
93
104
|
x, nil, nil])
|
105
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
94
106
|
|
95
|
-
|
96
|
-
expect(
|
107
|
+
new_game_state = hard_ai.generate_game_state(space, game_state)
|
108
|
+
expect(new_game_state.board.space(space).token).to eq(token)
|
97
109
|
end
|
98
110
|
end
|
99
111
|
|
100
112
|
|
101
113
|
describe '#score' do
|
102
|
-
it
|
114
|
+
it 'returns the correct score when HardAI has won' do
|
103
115
|
board = TicTacToes::TestBoardGenerator.generate([ o, nil, nil,
|
104
116
|
nil, o, nil,
|
105
117
|
nil, nil, o])
|
118
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
106
119
|
win_score = 1
|
107
120
|
|
108
|
-
expect(hard_ai.score(
|
121
|
+
expect(hard_ai.score(game_state)).to eq(win_score)
|
109
122
|
end
|
110
123
|
|
111
|
-
it
|
124
|
+
it 'returns the correct score when no one has won' do
|
112
125
|
board = TicTacToes::TestBoardGenerator.generate([o, o, x,
|
113
126
|
x, x, o,
|
114
127
|
o, x, o])
|
128
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
115
129
|
draw_score = 0
|
116
130
|
|
117
|
-
expect(hard_ai.score(
|
131
|
+
expect(hard_ai.score(game_state)).to eql(draw_score)
|
118
132
|
end
|
119
133
|
|
120
|
-
it
|
134
|
+
it 'returns the correct score when the opponent has won' do
|
121
135
|
board = TicTacToes::TestBoardGenerator.generate([ x, nil, nil,
|
122
136
|
nil, x, nil,
|
123
137
|
nil, nil, x])
|
138
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
124
139
|
loss_score = -1
|
125
140
|
|
126
|
-
expect(hard_ai.score(
|
141
|
+
expect(hard_ai.score(game_state)).to eql(loss_score)
|
127
142
|
end
|
128
143
|
end
|
129
144
|
end
|
@@ -2,29 +2,29 @@ require 'tic_tac_toes/core/move_strategies/human'
|
|
2
2
|
|
3
3
|
describe TicTacToes::Core::MoveStrategies::Human do
|
4
4
|
describe '#move' do
|
5
|
-
let(:io) { double(
|
5
|
+
let(:io) { double('io', solicit_input: 0, move_solicitation: true, not_an_integer_error: true) }
|
6
6
|
let(:human) { TicTacToes::Core::MoveStrategies::Human.new(io) }
|
7
7
|
|
8
|
-
it
|
8
|
+
it 'displays a move solicitation' do
|
9
9
|
expect(io).to receive(:move_solicitation)
|
10
|
-
human.move(
|
10
|
+
human.move('game_state')
|
11
11
|
end
|
12
12
|
|
13
13
|
context 'when given not-integer-like input' do
|
14
|
-
let(:not_integer_like) {
|
15
|
-
let(:integer_like) {
|
14
|
+
let(:not_integer_like) { 'string' }
|
15
|
+
let(:integer_like) { '100' }
|
16
16
|
|
17
|
-
it
|
17
|
+
it 'displays a not an integer error' do
|
18
18
|
allow(io).to receive(:solicit_input).and_return(not_integer_like, integer_like)
|
19
19
|
|
20
20
|
expect(io).to receive(:not_an_integer_error).once
|
21
|
-
human.move(
|
21
|
+
human.move('game_state')
|
22
22
|
end
|
23
23
|
|
24
|
-
it
|
24
|
+
it 'only returns a move (converted to integer) once it gets integer-like input' do
|
25
25
|
allow(io).to receive(:solicit_input).and_return(not_integer_like, integer_like)
|
26
26
|
|
27
|
-
expect(human.move(
|
27
|
+
expect(human.move('game_state')).to eq(100)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
@@ -1,20 +1,25 @@
|
|
1
1
|
require 'tic_tac_toes/test_board_generator'
|
2
|
+
require 'tic_tac_toes/core/game_state'
|
2
3
|
require 'tic_tac_toes/core/move_strategies/medium_ai'
|
4
|
+
require 'tic_tac_toes/core/player_factory'
|
3
5
|
|
4
6
|
describe TicTacToes::Core::MoveStrategies::MediumAI do
|
5
|
-
let(:medium_ai) { TicTacToes::Core::MoveStrategies::MediumAI }
|
6
|
-
let(:x) { TicTacToes::Core::Player.new("human", "x", false, "io") }
|
7
|
-
let(:o) { TicTacToes::Core::Player.new(medium_ai, "o", true, "io") }
|
8
|
-
let(:players) { [x, o] }
|
9
|
-
|
10
7
|
describe '#move' do
|
11
8
|
it "returns a valid move (based on either EasyAI or HardAI)" do
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
player_factory = TicTacToes::Core::PlayerFactory.new('unused_io')
|
10
|
+
x = player_factory.generate_human_player('x')
|
11
|
+
o = player_factory.generate_computer_player('o', :hard)
|
12
|
+
players = [x, o]
|
13
|
+
|
14
|
+
board = TicTacToes::TestBoardGenerator.generate([ o, o, x,
|
15
|
+
nil, x, nil,
|
16
|
+
nil, x, nil])
|
17
|
+
|
18
|
+
history = TicTacToes::UI::NullHistory.new
|
19
|
+
game_state = TicTacToes::Core::GameState.new(board, players, history)
|
15
20
|
valid_moves = [3, 5, 6, 8]
|
16
21
|
|
17
|
-
move =
|
22
|
+
move = TicTacToes::Core::MoveStrategies::MediumAI.move(game_state)
|
18
23
|
expect(valid_moves).to include(move)
|
19
24
|
end
|
20
25
|
end
|
@@ -2,31 +2,34 @@ require 'tic_tac_toes/core/board'
|
|
2
2
|
require 'tic_tac_toes/core/player'
|
3
3
|
|
4
4
|
describe TicTacToes::Core::Player do
|
5
|
-
let(:board)
|
6
|
-
let(:io) { double("io", invalid_move_error: true) }
|
7
|
-
let(:players) { double("players") }
|
5
|
+
let(:board) { TicTacToes::Core::Board.new(row_size: 3) }
|
8
6
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
7
|
+
let(:io) { double(invalid_move_error: true) }
|
8
|
+
|
9
|
+
let(:move_strategy) { double }
|
10
|
+
let(:x) { TicTacToes::Core::Player.new(move_strategy, 'x', false, io) }
|
11
|
+
let(:o) { double }
|
12
|
+
let(:players) { [x, o] }
|
13
|
+
|
14
|
+
let(:history) { double(record_board_size: true) }
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
|
16
|
+
let(:game_state) { TicTacToes::Core::GameState.new(board, players, history) }
|
17
|
+
|
18
|
+
describe '#place_and_return_move' do
|
19
|
+
it 'only places/returns a move once it receives a valid move' do
|
17
20
|
invalid_space, valid_space = 9, 0
|
18
21
|
allow(move_strategy).to receive(:move).and_return(invalid_space, valid_space)
|
19
22
|
|
20
|
-
|
21
|
-
expect(board.space(valid_space)).to eq(
|
23
|
+
x.place_and_return_move(game_state)
|
24
|
+
expect(board.space(valid_space)).to eq(x)
|
22
25
|
end
|
23
26
|
|
24
|
-
it
|
27
|
+
it 'displays an invalid move error when given an invalid move' do
|
25
28
|
invalid_space, valid_space = 9, 0
|
26
29
|
allow(move_strategy).to receive(:move).and_return(invalid_space, valid_space)
|
27
30
|
|
28
31
|
expect(io).to receive(:invalid_move_error)
|
29
|
-
|
32
|
+
x.place_and_return_move(game_state)
|
30
33
|
end
|
31
34
|
end
|
32
35
|
end
|
@@ -86,13 +86,14 @@ describe TicTacToes::Core::Rules do
|
|
86
86
|
|
87
87
|
|
88
88
|
describe '#determine_winner' do
|
89
|
-
it "returns the winning
|
89
|
+
it "returns the winning player when there is a winner" do
|
90
90
|
board = TicTacToes::TestBoardGenerator.generate([ o, nil, nil,
|
91
91
|
nil, o, nil,
|
92
92
|
nil, nil, o])
|
93
93
|
winning_token = "o"
|
94
94
|
|
95
|
-
|
95
|
+
winning_player = rules.determine_winner(board, players)
|
96
|
+
expect(winning_player.token).to eql(winning_token)
|
96
97
|
end
|
97
98
|
|
98
99
|
it "returns nil if there is not a winner" do
|
@@ -5,12 +5,12 @@ require 'tic_tac_toes/ui/adapter'
|
|
5
5
|
|
6
6
|
describe TicTacToes::UI::Adapter do
|
7
7
|
describe '#make_move' do
|
8
|
-
let(:history)
|
9
|
-
|
8
|
+
let(:history) { TicTacToes::UI::NullHistory.new }
|
9
|
+
|
10
10
|
player_factory = TicTacToes::Core::PlayerFactory.new('unused_io')
|
11
|
-
let(:x)
|
12
|
-
let(:o)
|
13
|
-
let(:players)
|
11
|
+
let(:x) { player_factory.generate_human_player('x') }
|
12
|
+
let(:o) { player_factory.generate_computer_player('o', :hard) }
|
13
|
+
let(:players) { [x, o] }
|
14
14
|
|
15
15
|
context 'when the game is still in progress' do
|
16
16
|
it 'sends its listener #moves_were_made with the updated game state' do
|
@@ -25,8 +25,8 @@ describe TicTacToes::UI::Adapter do
|
|
25
25
|
TicTacToes::UI::Adapter.make_move(game_state, move, listener)
|
26
26
|
|
27
27
|
first_move, second_move = 2, 1
|
28
|
-
|
29
|
-
|
28
|
+
board.place(x, first_move)
|
29
|
+
board.place(o, second_move)
|
30
30
|
expect(listener).to have_received(:moves_were_made).with(game_state)
|
31
31
|
end
|
32
32
|
end
|
@@ -44,7 +44,7 @@ describe TicTacToes::UI::Adapter do
|
|
44
44
|
allow(listener).to receive(:game_ended_in_winner)
|
45
45
|
TicTacToes::UI::Adapter.make_move(game_state, move, listener)
|
46
46
|
|
47
|
-
|
47
|
+
board.place(x, 2)
|
48
48
|
expect(listener).to have_received(:game_ended_in_winner).with(game_state, winning_token)
|
49
49
|
end
|
50
50
|
end
|
@@ -61,7 +61,7 @@ describe TicTacToes::UI::Adapter do
|
|
61
61
|
allow(listener).to receive(:game_ended_in_draw)
|
62
62
|
TicTacToes::UI::Adapter.make_move(game_state, move, listener)
|
63
63
|
|
64
|
-
|
64
|
+
board.place(x, 8)
|
65
65
|
expect(listener).to have_received(:game_ended_in_draw).with(game_state)
|
66
66
|
end
|
67
67
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tic_tac_toes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Spatafora
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-07-
|
11
|
+
date: 2014-07-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -90,9 +90,7 @@ files:
|
|
90
90
|
- lib/tic_tac_toes/command_line/prompt.rb
|
91
91
|
- lib/tic_tac_toes/command_line/runner.rb
|
92
92
|
- lib/tic_tac_toes/core/board.rb
|
93
|
-
- lib/tic_tac_toes/core/board_factory.rb
|
94
93
|
- lib/tic_tac_toes/core/game_state.rb
|
95
|
-
- lib/tic_tac_toes/core/game_state_factory.rb
|
96
94
|
- lib/tic_tac_toes/core/history.rb
|
97
95
|
- lib/tic_tac_toes/core/io.rb
|
98
96
|
- lib/tic_tac_toes/core/move_strategies/easy_ai.rb
|
@@ -111,9 +109,7 @@ files:
|
|
111
109
|
- lib/tic_tac_toes/version.rb
|
112
110
|
- spec/tic_tac_toes/command_line/menu_spec.rb
|
113
111
|
- spec/tic_tac_toes/command_line/runner_spec.rb
|
114
|
-
- spec/tic_tac_toes/core/board_factory_spec.rb
|
115
112
|
- spec/tic_tac_toes/core/board_spec.rb
|
116
|
-
- spec/tic_tac_toes/core/game_state_factory_spec.rb
|
117
113
|
- spec/tic_tac_toes/core/game_state_spec.rb
|
118
114
|
- spec/tic_tac_toes/core/history_spec.rb
|
119
115
|
- spec/tic_tac_toes/core/io_spec.rb
|
@@ -157,9 +153,7 @@ summary: The game Tic-tac-toe, featuring an unbeatable AI
|
|
157
153
|
test_files:
|
158
154
|
- spec/tic_tac_toes/command_line/menu_spec.rb
|
159
155
|
- spec/tic_tac_toes/command_line/runner_spec.rb
|
160
|
-
- spec/tic_tac_toes/core/board_factory_spec.rb
|
161
156
|
- spec/tic_tac_toes/core/board_spec.rb
|
162
|
-
- spec/tic_tac_toes/core/game_state_factory_spec.rb
|
163
157
|
- spec/tic_tac_toes/core/game_state_spec.rb
|
164
158
|
- spec/tic_tac_toes/core/history_spec.rb
|
165
159
|
- spec/tic_tac_toes/core/io_spec.rb
|
@@ -1,15 +0,0 @@
|
|
1
|
-
require 'tic_tac_toes/core/game_state'
|
2
|
-
|
3
|
-
module TicTacToes
|
4
|
-
module Core
|
5
|
-
class GameStateFactory
|
6
|
-
def initialize(history)
|
7
|
-
@history = history
|
8
|
-
end
|
9
|
-
|
10
|
-
def generate_game_state(board, players)
|
11
|
-
GameState.new(board, players, @history)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
@@ -1,13 +0,0 @@
|
|
1
|
-
require 'tic_tac_toes/core/board_factory'
|
2
|
-
|
3
|
-
describe TicTacToes::Core::BoardFactory do
|
4
|
-
describe '#generate_board' do
|
5
|
-
it 'returns a board object generated from a row size' do
|
6
|
-
board_factory = TicTacToes::Core::BoardFactory.new
|
7
|
-
row_size = 5
|
8
|
-
|
9
|
-
board = board_factory.generate_board(row_size)
|
10
|
-
expect(board.row_size).to eq(row_size)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
require 'tic_tac_toes/core/game_state_factory'
|
2
|
-
|
3
|
-
describe TicTacToes::Core::GameStateFactory do
|
4
|
-
describe '#generate_game_state' do
|
5
|
-
it 'returns a game state object generated from a board object and player array' do
|
6
|
-
history = double(record_board_size: true)
|
7
|
-
game_state_factory = TicTacToes::Core::GameStateFactory.new(history)
|
8
|
-
board, players = double(size: 3), double
|
9
|
-
|
10
|
-
game_state = game_state_factory.generate_game_state(board, players)
|
11
|
-
expect(game_state.board).to eq(board)
|
12
|
-
expect(game_state.players).to eq(players)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|