erics_tic_tac_toe 0.1.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.markdown +6 -7
- data/Rakefile +2 -1
- data/bin/tic_tac_toe +41 -2
- data/lib/tic_tac_toe.rb +5 -4
- data/lib/tic_tac_toe/board.rb +153 -0
- data/lib/tic_tac_toe/game.rb +70 -0
- data/lib/tic_tac_toe/game_types/terminal_game.rb +86 -0
- data/lib/tic_tac_toe/player.rb +13 -0
- data/lib/tic_tac_toe/players/computer_player.rb +26 -0
- data/lib/tic_tac_toe/players/human_player.rb +28 -0
- data/lib/tic_tac_toe/presentors/game_presenter.rb +16 -0
- data/lib/tic_tac_toe/presentors/player_presenter.rb +13 -0
- data/lib/tic_tac_toe/strategies/minimax_strategy.rb +91 -0
- data/lib/tic_tac_toe/strategies/three_by_three_strategy.rb +260 -0
- data/lib/{tic-tac-toe → tic_tac_toe}/version.rb +1 -1
- data/test/board_test.rb +40 -52
- data/test/game_presentor_test.rb +19 -0
- data/test/game_test.rb +79 -0
- data/test/player_presenter_test.rb +12 -0
- data/test/player_test.rb +57 -0
- data/test/potential_state_test.rb +53 -0
- data/test/solver_test.rb +169 -93
- data/test/terminal_game_test.rb +78 -0
- data/test/test_helper.rb +4 -0
- data/tic-tac-toe.gemspec +1 -1
- metadata +25 -11
- data/lib/tic-tac-toe/board.rb +0 -148
- data/lib/tic-tac-toe/game.rb +0 -62
- data/lib/tic-tac-toe/game_types/terminal_game.rb +0 -63
- data/lib/tic-tac-toe/solver.rb +0 -16
- data/lib/tic-tac-toe/strategies/threebythree_implementations/brute_force_implementation.rb +0 -197
- data/lib/tic-tac-toe/strategies/threebythree_stategy.rb +0 -30
- data/test/brute_force_implementation_test.rb +0 -25
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
class TerminalGameTest < MiniTest::Unit::TestCase
|
5
|
+
|
6
|
+
class IoMock
|
7
|
+
attr_reader :output
|
8
|
+
|
9
|
+
def initialize(output=nil)
|
10
|
+
@input = Array(output)
|
11
|
+
end
|
12
|
+
|
13
|
+
def gets
|
14
|
+
@input.pop
|
15
|
+
end
|
16
|
+
|
17
|
+
def puts(text); @output = text end
|
18
|
+
def print(text); @output = text end
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_computer_goes_first
|
22
|
+
assert TicTacToe::TerminalGame.new(nil, IoMock.new("s")).computer_goes_first?
|
23
|
+
refute TicTacToe::TerminalGame.new(nil, IoMock.new("f")).computer_goes_first?
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_get_move_from_user
|
27
|
+
game = TicTacToe::TerminalGame.new(TicTacToe::Board.new, IoMock.new("1"))
|
28
|
+
assert_equal [0, 0], game.get_move_from_user
|
29
|
+
|
30
|
+
game = TicTacToe::TerminalGame.new(TicTacToe::Board.new, IoMock.new("9"))
|
31
|
+
assert_equal [2, 2], game.get_move_from_user
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_handle_bad_input
|
35
|
+
game = TicTacToe::TerminalGame.new(TicTacToe::Board.new, IoMock.new(["1", "a"]))
|
36
|
+
assert_equal [0, 0], game.get_move_from_user
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_play_again
|
40
|
+
game = TicTacToe::TerminalGame.new(nil, IoMock.new("y"))
|
41
|
+
assert game.play_again?
|
42
|
+
|
43
|
+
game = TicTacToe::TerminalGame.new(nil, IoMock.new("n"))
|
44
|
+
refute game.play_again?
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_update_board
|
48
|
+
mock = IoMock.new
|
49
|
+
game = TicTacToe::TerminalGame.new("FooBar", mock)
|
50
|
+
game.update_board
|
51
|
+
|
52
|
+
assert_equal "FooBar", mock.output
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_display_text
|
56
|
+
mock = IoMock.new
|
57
|
+
game = TicTacToe::TerminalGame.new(nil, mock)
|
58
|
+
game.display_text("FooBar")
|
59
|
+
|
60
|
+
assert_equal "FooBar", mock.output
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_select_solver
|
64
|
+
mock = IoMock.new("1")
|
65
|
+
game = TicTacToe::TerminalGame.new(nil, mock)
|
66
|
+
assert_equal "FooBar", game.select(["FooBar", "BarFoo"])
|
67
|
+
|
68
|
+
mock = IoMock.new("2")
|
69
|
+
game = TicTacToe::TerminalGame.new(nil, mock)
|
70
|
+
assert_equal "BarFoo", game.select(["FooBar", "BarFoo"])
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_select_bad_choice
|
74
|
+
mock = IoMock.new(["1", 'x'])
|
75
|
+
game = TicTacToe::TerminalGame.new(nil, mock)
|
76
|
+
assert_equal "FooBar", game.select(["FooBar", "BarFoo"])
|
77
|
+
end
|
78
|
+
end
|
data/test/test_helper.rb
CHANGED
data/tic-tac-toe.gemspec
CHANGED
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: erics_tic_tac_toe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.
|
5
|
+
version: 0.5.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Eric Koslow
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2012-
|
13
|
+
date: 2012-08-15 00:00:00 -05:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -50,17 +50,26 @@ files:
|
|
50
50
|
- README.markdown
|
51
51
|
- Rakefile
|
52
52
|
- bin/tic_tac_toe
|
53
|
-
- lib/tic-tac-toe/board.rb
|
54
|
-
- lib/tic-tac-toe/game.rb
|
55
|
-
- lib/tic-tac-toe/game_types/terminal_game.rb
|
56
|
-
- lib/tic-tac-toe/solver.rb
|
57
|
-
- lib/tic-tac-toe/strategies/threebythree_implementations/brute_force_implementation.rb
|
58
|
-
- lib/tic-tac-toe/strategies/threebythree_stategy.rb
|
59
|
-
- lib/tic-tac-toe/version.rb
|
60
53
|
- lib/tic_tac_toe.rb
|
54
|
+
- lib/tic_tac_toe/board.rb
|
55
|
+
- lib/tic_tac_toe/game.rb
|
56
|
+
- lib/tic_tac_toe/game_types/terminal_game.rb
|
57
|
+
- lib/tic_tac_toe/player.rb
|
58
|
+
- lib/tic_tac_toe/players/computer_player.rb
|
59
|
+
- lib/tic_tac_toe/players/human_player.rb
|
60
|
+
- lib/tic_tac_toe/presentors/game_presenter.rb
|
61
|
+
- lib/tic_tac_toe/presentors/player_presenter.rb
|
62
|
+
- lib/tic_tac_toe/strategies/minimax_strategy.rb
|
63
|
+
- lib/tic_tac_toe/strategies/three_by_three_strategy.rb
|
64
|
+
- lib/tic_tac_toe/version.rb
|
61
65
|
- test/board_test.rb
|
62
|
-
- test/
|
66
|
+
- test/game_presentor_test.rb
|
67
|
+
- test/game_test.rb
|
68
|
+
- test/player_presenter_test.rb
|
69
|
+
- test/player_test.rb
|
70
|
+
- test/potential_state_test.rb
|
63
71
|
- test/solver_test.rb
|
72
|
+
- test/terminal_game_test.rb
|
64
73
|
- test/test_helper.rb
|
65
74
|
- tic-tac-toe.gemspec
|
66
75
|
has_rdoc: true
|
@@ -93,6 +102,11 @@ specification_version: 3
|
|
93
102
|
summary: A game of Tic Tac Toe
|
94
103
|
test_files:
|
95
104
|
- test/board_test.rb
|
96
|
-
- test/
|
105
|
+
- test/game_presentor_test.rb
|
106
|
+
- test/game_test.rb
|
107
|
+
- test/player_presenter_test.rb
|
108
|
+
- test/player_test.rb
|
109
|
+
- test/potential_state_test.rb
|
97
110
|
- test/solver_test.rb
|
111
|
+
- test/terminal_game_test.rb
|
98
112
|
- test/test_helper.rb
|
data/lib/tic-tac-toe/board.rb
DELETED
@@ -1,148 +0,0 @@
|
|
1
|
-
module TicTacToe
|
2
|
-
class Board
|
3
|
-
|
4
|
-
SIZE = 3.freeze
|
5
|
-
|
6
|
-
def initialize(grid=nil)
|
7
|
-
#[[ nil, nil, nil],
|
8
|
-
# [ nil, nil, nil],
|
9
|
-
# [ nil, nil, nil]]
|
10
|
-
@grid = grid || [ [nil] * SIZE ] * SIZE
|
11
|
-
end
|
12
|
-
|
13
|
-
def get_cell(x, y)
|
14
|
-
@grid[y][x]
|
15
|
-
end
|
16
|
-
|
17
|
-
# Returns the closest middle cell
|
18
|
-
def center_cell
|
19
|
-
mid = @grid.size/2
|
20
|
-
get_cell(mid, mid)
|
21
|
-
end
|
22
|
-
|
23
|
-
# Sets the closet middle cell if its not already set
|
24
|
-
def center_cell=(letter)
|
25
|
-
mid = @grid.size/2
|
26
|
-
play_at(mid, mid, letter)
|
27
|
-
end
|
28
|
-
|
29
|
-
# Returns the corners of the grid
|
30
|
-
def corners
|
31
|
-
[@grid[0][0],
|
32
|
-
@grid[0][SIZE-1],
|
33
|
-
@grid[SIZE-1][SIZE-1],
|
34
|
-
@grid[SIZE-1][0]]
|
35
|
-
end
|
36
|
-
|
37
|
-
# Returns the corners of the empty cells
|
38
|
-
def any_empty_position(&block)
|
39
|
-
SIZE.times do |y|
|
40
|
-
SIZE.times do |x|
|
41
|
-
next if get_cell(x, y)
|
42
|
-
yield(x, y)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
# Plays a letter at a position, unless that position has already been taken
|
48
|
-
def play_at(x, y, letter)
|
49
|
-
# Weird bug I found in ruby 1.9.3-p0
|
50
|
-
# Given a @grid of [ [nil,nil,nil], [nil,nil,nil], [nil,nil,nil] ]
|
51
|
-
# If you call @grid[0][0] = 'x'
|
52
|
-
# I aspect @grid to be [ ['x',nil,nil], [nil,nil,nil], [nil,nil,nil] ]
|
53
|
-
# What happens is that @grid is [ ['x',nil,nil], ['x',nil,nil], ['x',nil,nil] ]
|
54
|
-
# This is a workaround for that
|
55
|
-
inner = @grid[y].clone
|
56
|
-
inner[x] ||= letter
|
57
|
-
@grid[y] = inner
|
58
|
-
end
|
59
|
-
|
60
|
-
# Returns true if the grid is empty
|
61
|
-
def empty?
|
62
|
-
each_cell do |cell|
|
63
|
-
return false if cell
|
64
|
-
end
|
65
|
-
true
|
66
|
-
end
|
67
|
-
|
68
|
-
# Returns true if the grid only has one element
|
69
|
-
def only_one?
|
70
|
-
counter = 0
|
71
|
-
each_cell do |cell|
|
72
|
-
counter += 1 if cell
|
73
|
-
return false if counter >= 2
|
74
|
-
end
|
75
|
-
counter == 1
|
76
|
-
end
|
77
|
-
|
78
|
-
# Returns true if every cell is set to a value
|
79
|
-
def full?
|
80
|
-
each_cell do |cell|
|
81
|
-
return false unless cell
|
82
|
-
end
|
83
|
-
true
|
84
|
-
end
|
85
|
-
|
86
|
-
# Returns true if the board has a wining pattern
|
87
|
-
def solved?
|
88
|
-
return true if won_across?
|
89
|
-
return true if won_up_and_down?
|
90
|
-
return true if won_diagonally?
|
91
|
-
false
|
92
|
-
end
|
93
|
-
|
94
|
-
def to_s
|
95
|
-
output = ["-----------\n"]
|
96
|
-
@grid.each_with_index do |row, i|
|
97
|
-
row.each_with_index do |cell, j|
|
98
|
-
output << "#{cell || ((i*3)+j+1)} | #{"\n" if j==2}"
|
99
|
-
end
|
100
|
-
end
|
101
|
-
output << ["-----------\n\n"]
|
102
|
-
output.join
|
103
|
-
end
|
104
|
-
|
105
|
-
# Preform a deep clone of the board
|
106
|
-
# FIXME: This only works for SIZE=3
|
107
|
-
def clone
|
108
|
-
Board.new([[@grid[0][0], @grid[0][1], @grid[0][2] ],
|
109
|
-
[@grid[1][0], @grid[1][1], @grid[1][2] ],
|
110
|
-
[@grid[2][0], @grid[2][1], @grid[2][2] ]])
|
111
|
-
end
|
112
|
-
|
113
|
-
private
|
114
|
-
|
115
|
-
def each_cell(&block)
|
116
|
-
@grid.each do |row|
|
117
|
-
row.each do |cell|
|
118
|
-
yield(cell)
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
def won_across?(grid = @grid)
|
124
|
-
grid.any? do |row|
|
125
|
-
winning_group?(row)
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
def won_up_and_down?
|
130
|
-
won_across?(@grid.transpose)
|
131
|
-
end
|
132
|
-
|
133
|
-
def won_diagonally?
|
134
|
-
base = (0..@grid.size-1)
|
135
|
-
left_to_right = base.collect { |i| @grid[i][i]}
|
136
|
-
right_to_left = base.collect { |i| @grid[i][@grid.size-1-i]}
|
137
|
-
return true if winning_group?(left_to_right)
|
138
|
-
return true if winning_group?(right_to_left)
|
139
|
-
false
|
140
|
-
end
|
141
|
-
|
142
|
-
def winning_group?(group)
|
143
|
-
# Won when none of the cells are nil and they are all the same value
|
144
|
-
!group.any? { |cell| cell.nil? } && group.uniq.size == 1
|
145
|
-
end
|
146
|
-
|
147
|
-
end
|
148
|
-
end
|
data/lib/tic-tac-toe/game.rb
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
#This is a computer that will play a perfect game of tic-tac-toe
|
2
|
-
#Author: Eric Koslow
|
3
|
-
|
4
|
-
require_relative 'game_types/terminal_game'
|
5
|
-
|
6
|
-
module TicTacToe
|
7
|
-
class Game
|
8
|
-
def initialize(type=TerminalGame)
|
9
|
-
@board = Board.new
|
10
|
-
@game_type = type.new(@board)
|
11
|
-
end
|
12
|
-
|
13
|
-
def run
|
14
|
-
if @game_type.computer_goes_first?
|
15
|
-
@computer_letter = O
|
16
|
-
get_move_from_computer!
|
17
|
-
else
|
18
|
-
@computer_letter = X
|
19
|
-
end
|
20
|
-
|
21
|
-
@game_type.update_board
|
22
|
-
|
23
|
-
loop do
|
24
|
-
@game_type.get_move_from_user!
|
25
|
-
@game_type.update_board
|
26
|
-
break if game_over?
|
27
|
-
|
28
|
-
get_move_from_computer!
|
29
|
-
@game_type.update_board
|
30
|
-
break if game_over?
|
31
|
-
end
|
32
|
-
|
33
|
-
if @game_type.play_again?
|
34
|
-
new_game
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
def new_game
|
41
|
-
Game.new.run
|
42
|
-
end
|
43
|
-
|
44
|
-
def game_over?
|
45
|
-
if @board.solved?
|
46
|
-
@game_type.display_text("You lost!")
|
47
|
-
true
|
48
|
-
elsif @board.full?
|
49
|
-
@game_type.display_text("Cats game!")
|
50
|
-
true
|
51
|
-
else
|
52
|
-
false
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def get_move_from_computer!
|
57
|
-
Solver.new(@board, @computer_letter).next_move!
|
58
|
-
@game_type.display_text("Computer's move (#{@computer_letter}):")
|
59
|
-
end
|
60
|
-
|
61
|
-
end
|
62
|
-
end
|
@@ -1,63 +0,0 @@
|
|
1
|
-
module TicTacToe
|
2
|
-
class TerminalGame
|
3
|
-
class IllegalMove < RuntimeError
|
4
|
-
end
|
5
|
-
|
6
|
-
def initialize(board)
|
7
|
-
@board = board
|
8
|
-
end
|
9
|
-
|
10
|
-
def computer_goes_first?
|
11
|
-
print "Would you like to play first or second? (f/s) "
|
12
|
-
input = gets.chomp
|
13
|
-
unless input =~ /^(f|first)$/i
|
14
|
-
@human_letter = X
|
15
|
-
true
|
16
|
-
else
|
17
|
-
@human_letter = O
|
18
|
-
false
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def get_move_from_user!
|
23
|
-
print "Your move (#{@human_letter}) (1-9): "
|
24
|
-
input = gets.chomp
|
25
|
-
|
26
|
-
if input =~ /^\d$/
|
27
|
-
cords = cord_from_num(input.to_i)
|
28
|
-
if !@board.get_cell(*cords)
|
29
|
-
@board.play_at(*(cords+[@human_letter]))
|
30
|
-
else
|
31
|
-
raise IllegalMove.new("That cell is already taken")
|
32
|
-
end
|
33
|
-
else
|
34
|
-
raise IllegalMove.new("Must be a single number")
|
35
|
-
end
|
36
|
-
rescue IllegalMove => e
|
37
|
-
display_text "Illegal Move: #{e.message}. Please try again"
|
38
|
-
get_move_from_user!
|
39
|
-
end
|
40
|
-
|
41
|
-
def play_again?
|
42
|
-
print "Play again? (y/n) "
|
43
|
-
input = gets.chomp
|
44
|
-
input =~ /^(y|yes)$/i
|
45
|
-
end
|
46
|
-
|
47
|
-
def update_board
|
48
|
-
puts @board
|
49
|
-
end
|
50
|
-
|
51
|
-
def display_text(text)
|
52
|
-
puts text
|
53
|
-
end
|
54
|
-
|
55
|
-
private
|
56
|
-
|
57
|
-
def cord_from_num(num)
|
58
|
-
num -= 1
|
59
|
-
[num % Board::SIZE, num/Board::SIZE]
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
end
|
data/lib/tic-tac-toe/solver.rb
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
require_relative 'strategies/threebythree_stategy'
|
2
|
-
|
3
|
-
module TicTacToe
|
4
|
-
class Solver
|
5
|
-
|
6
|
-
def initialize(board, letter, strategy=ThreebythreeStrategy)
|
7
|
-
@strategy = strategy.new(board, letter)
|
8
|
-
end
|
9
|
-
|
10
|
-
# Performs the next perfect move and updates the board as a side effect
|
11
|
-
def next_move!
|
12
|
-
@strategy.solve!
|
13
|
-
end
|
14
|
-
|
15
|
-
end
|
16
|
-
end
|