erics_tic_tac_toe 0.1.0 → 0.5.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.
- 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
|