jbcden_ttt 0.0.1
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 +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +12 -0
- data/bin/ttt +5 -0
- data/jbcden_ttt.gemspec +23 -0
- data/lib/jbcden_ttt.rb +148 -0
- data/lib/jbcden_ttt/board.rb +51 -0
- data/lib/jbcden_ttt/board_mapper.rb +64 -0
- data/lib/jbcden_ttt/computer.rb +124 -0
- data/lib/jbcden_ttt/display_board.rb +46 -0
- data/lib/jbcden_ttt/game_state.rb +114 -0
- data/lib/jbcden_ttt/player.rb +26 -0
- data/lib/jbcden_ttt/tile.rb +37 -0
- data/lib/jbcden_ttt/version.rb +3 -0
- data/test/board_mapper_test.rb +38 -0
- data/test/board_test.rb +32 -0
- data/test/computer_test.rb +89 -0
- data/test/display_board_test.rb +19 -0
- data/test/game_state_test.rb +61 -0
- data/test/player_test.rb +22 -0
- data/test/test_helper.rb +4 -0
- data/test/tile_test.rb +52 -0
- metadata +105 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e190a50dcf6cc00e3708c46d1791dc622fef6a9b
|
4
|
+
data.tar.gz: a08f60ed3d99e4c6ff5d83b27647aa27aa0b5e72
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7c56cf50cb4451678ee89256caa65089662b524c5477e3d9017f121706fdebc948ab5f4e7657e833433ec0c027dfb1dfb172704aba7e2aa059cef14eb441e59f
|
7
|
+
data.tar.gz: dda78fbd248ed911730650a91dd0df111dfefeef76621f090a09a9d6ba3a8c526cf292fb459f0facde66e8334d797c46b57e28dda9c010d884da5a42ed3adaff
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Jacob Chae
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# JbcdenTtt
|
2
|
+
|
3
|
+
## An unbeatable game of tic-tac-toe!
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
- Simply run:
|
8
|
+
```bash
|
9
|
+
gem install jbcden_ttt
|
10
|
+
```
|
11
|
+
|
12
|
+
- And then play!:
|
13
|
+
```bash
|
14
|
+
ttt
|
15
|
+
```
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
Please note that this gem does require Ruby 1.9 or later.
|
20
|
+
|
21
|
+
Also note that this gem is based on the repo:
|
22
|
+
|
23
|
+
https://github.com/jbcden/tic-tac-toe
|
24
|
+
|
25
|
+
## Contributing
|
26
|
+
|
27
|
+
1. Fork it ( https://github.com/jbcden/jbcden_ttt/fork )
|
28
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
29
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
30
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
31
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/ttt
ADDED
data/jbcden_ttt.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'jbcden_ttt/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "jbcden_ttt"
|
8
|
+
spec.version = JbcdenTtt::VERSION
|
9
|
+
spec.date = %q{2014-11-18}
|
10
|
+
spec.authors = ["Jacob Chae"]
|
11
|
+
spec.email = ["jbcden@gmail.com"]
|
12
|
+
spec.summary = %q{A basic command line based game of tic tac toe.}
|
13
|
+
spec.homepage = "https://github.com/jbcden/tic-tac-toe"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = ["ttt"]
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
data/lib/jbcden_ttt.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
require "jbcden_ttt/version"
|
2
|
+
require 'jbcden_ttt/board'
|
3
|
+
require 'jbcden_ttt/board_mapper'
|
4
|
+
require 'jbcden_ttt/computer'
|
5
|
+
require 'jbcden_ttt/display_board'
|
6
|
+
require 'jbcden_ttt/game_state'
|
7
|
+
require 'jbcden_ttt/player'
|
8
|
+
require 'jbcden_ttt/tile'
|
9
|
+
|
10
|
+
module JbcdenTtt
|
11
|
+
class TicTacToe
|
12
|
+
attr_reader :board, :turn_num, :players, :computer, :player, :game
|
13
|
+
|
14
|
+
def self.start
|
15
|
+
new.play
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@board = Board.new(3,3)
|
20
|
+
@turn_num = 0
|
21
|
+
@players = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def play
|
25
|
+
trap('INT') do
|
26
|
+
puts 'exiting!'
|
27
|
+
exit!
|
28
|
+
end
|
29
|
+
print_intro
|
30
|
+
choose_human
|
31
|
+
set_players_order
|
32
|
+
initialize_game_state
|
33
|
+
game_loop
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def print_intro
|
38
|
+
clear_screen
|
39
|
+
|
40
|
+
print "Welcome to my Tic Tac Toe game!"
|
41
|
+
end
|
42
|
+
|
43
|
+
def choose_human
|
44
|
+
error = ""
|
45
|
+
while 1
|
46
|
+
begin
|
47
|
+
clear_screen
|
48
|
+
puts "Which player would you like to be? (\"x\" or \"o\"): "
|
49
|
+
puts error unless error.empty?
|
50
|
+
human_symbol = gets.chomp
|
51
|
+
@player = Player.new(human_symbol)
|
52
|
+
break
|
53
|
+
rescue Player::InvalidCharacterError => e
|
54
|
+
error = e.message
|
55
|
+
retry
|
56
|
+
end
|
57
|
+
end
|
58
|
+
@computer = Computer.new(choose_computer(human_symbol))
|
59
|
+
end
|
60
|
+
|
61
|
+
def set_players_order
|
62
|
+
if player.symbol == "x"
|
63
|
+
@players << @player
|
64
|
+
@players << @computer
|
65
|
+
else
|
66
|
+
@players << @computer
|
67
|
+
@players << @player
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize_game_state
|
72
|
+
@game = GameState.new(@board, @turn_num, @players.first)
|
73
|
+
end
|
74
|
+
|
75
|
+
def game_loop
|
76
|
+
while !@game.end_state?
|
77
|
+
clear_board
|
78
|
+
|
79
|
+
current_player = @players[@turn_num % 2]
|
80
|
+
|
81
|
+
take_turn(current_player)
|
82
|
+
|
83
|
+
@turn_num += 1
|
84
|
+
@game = GameState.new(@board, @turn_num, @players[@turn_num % 2])
|
85
|
+
end
|
86
|
+
|
87
|
+
clear_board
|
88
|
+
|
89
|
+
puts "The winner is: #{game.end_state?}"
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def take_turn(current_player)
|
95
|
+
if current_player.human?
|
96
|
+
human_move(current_player)
|
97
|
+
else
|
98
|
+
move = current_player.calculate_best_move(@board, @game)
|
99
|
+
computer.make_move(@board, move)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def human_move(current_player)
|
104
|
+
puts
|
105
|
+
error = ""
|
106
|
+
while 1
|
107
|
+
begin
|
108
|
+
clear_board
|
109
|
+
print_message
|
110
|
+
puts error unless error.empty?
|
111
|
+
|
112
|
+
move = gets.chomp
|
113
|
+
current_player.make_move(@board, move)
|
114
|
+
break
|
115
|
+
rescue BoardMapper::InvalidCoordinateError => e
|
116
|
+
error = e.message
|
117
|
+
rescue Tile::InvalidActionError => e
|
118
|
+
error = e.message
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def choose_computer(human)
|
124
|
+
if human == "x"
|
125
|
+
return "o"
|
126
|
+
else
|
127
|
+
return "x"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def clear_screen
|
132
|
+
puts "\033c"
|
133
|
+
end
|
134
|
+
|
135
|
+
def print_board
|
136
|
+
puts DisplayBoard.call(@board)
|
137
|
+
end
|
138
|
+
|
139
|
+
def clear_board
|
140
|
+
clear_screen
|
141
|
+
print_board
|
142
|
+
end
|
143
|
+
|
144
|
+
def print_message
|
145
|
+
puts "Please choose a square to mark."
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative 'tile'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module JbcdenTtt
|
5
|
+
class Board
|
6
|
+
extend Forwardable
|
7
|
+
attr_reader :width, :height, :board
|
8
|
+
attr_writer :board
|
9
|
+
def initialize(width, height)
|
10
|
+
@width = width
|
11
|
+
@height = height
|
12
|
+
@board = Array.new(height)
|
13
|
+
|
14
|
+
init_board_array
|
15
|
+
init_board
|
16
|
+
end
|
17
|
+
|
18
|
+
def display
|
19
|
+
DisplayBoard.call(board)
|
20
|
+
end
|
21
|
+
|
22
|
+
def_delegators :@board, :[], :[]=, :each, :first, :size, :last
|
23
|
+
private
|
24
|
+
|
25
|
+
def init_board_array
|
26
|
+
board.each_with_index do |row, index|
|
27
|
+
board[index] = Array.new(width)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def init_board
|
32
|
+
board.each_with_index do |row, rownum|
|
33
|
+
row.each_with_index do |column, index|
|
34
|
+
set_rows(row, rownum, index)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_rows(row, rownum, index)
|
40
|
+
if rownum != board.size - 1
|
41
|
+
set_tile(row, rownum, index, '_')
|
42
|
+
else
|
43
|
+
set_tile(row, rownum, index, ' ')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def set_tile(row, rownum, index, tile_char)
|
48
|
+
row[index] = Tile.new(tile_char, rownum, index)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module JbcdenTtt
|
2
|
+
class BoardMapper
|
3
|
+
class InvalidCoordinateError < StandardError
|
4
|
+
def initialize(msg="An invalid coordinate was passed in, please try again")
|
5
|
+
super
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
ROW_MAPPER = {
|
10
|
+
"a" => 0, "b" => 1, "c" => 2, "d" => 3, "e" => 4, "f" => 5, "g" => 6, "h" => 7,
|
11
|
+
"i" => 8, "j" => 9, "k" => 10, "l" => 11, "m" => 12, "n" => 13, "o" => 14, "p" => 15,
|
12
|
+
"q" => 16, "r" => 17, "s" => 18, "t" => 19, "u" => 20, "v" => 21, "w" => 22, "x" => 23,
|
13
|
+
"y" => 24, "z" => 25
|
14
|
+
}
|
15
|
+
|
16
|
+
def self.map_coordinate(x, y)
|
17
|
+
str = ""
|
18
|
+
str << ROW_MAPPER.key(x)
|
19
|
+
str << (y+1).to_s
|
20
|
+
|
21
|
+
str
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.map_string(board, location)
|
25
|
+
|
26
|
+
row = extract_row(location)
|
27
|
+
col = extract_column(location)
|
28
|
+
|
29
|
+
if is_valid?(board, row, col)
|
30
|
+
board[ROW_MAPPER[row]][col]
|
31
|
+
else
|
32
|
+
raise InvalidCoordinateError
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def self.extract_column(location)
|
38
|
+
column = location.scan(/\d+\Z/).first
|
39
|
+
raise InvalidCoordinateError if column.nil?
|
40
|
+
get_col(
|
41
|
+
Integer(column)
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.extract_row(location)
|
46
|
+
row = location.scan(/\A[a-z]+/).first
|
47
|
+
raise InvalidCoordinateError if row.nil?
|
48
|
+
raise InvalidCoordinateError unless ROW_MAPPER.has_key?(row)
|
49
|
+
row
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.get_col(col_num)
|
53
|
+
col_num - 1
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.is_valid?(board, row, col)
|
57
|
+
(board.height) > ROW_MAPPER[row] && (board.width) > col
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.row_size
|
61
|
+
@row_size ||= board.first.size
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module JbcdenTtt
|
2
|
+
class Computer
|
3
|
+
attr_reader :symbol
|
4
|
+
|
5
|
+
def initialize(symbol)
|
6
|
+
@symbol = symbol
|
7
|
+
end
|
8
|
+
|
9
|
+
def calculate_best_move(board, game)
|
10
|
+
unless new_board?(game.board)
|
11
|
+
mini_max(game, symbol, 0)
|
12
|
+
BoardMapper.map_coordinate(@move.xval, @move.yval)
|
13
|
+
else
|
14
|
+
tile = corners(board).sample
|
15
|
+
BoardMapper.map_coordinate(tile.xval, tile.yval)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def make_move(board, coordinate)
|
20
|
+
tile = BoardMapper.map_string(board, coordinate)
|
21
|
+
tile.mark(symbol)
|
22
|
+
end
|
23
|
+
|
24
|
+
def mini_max(game, current_player, depth)
|
25
|
+
return evaluate(game, depth) if game.end_state?
|
26
|
+
scores = []
|
27
|
+
moves = []
|
28
|
+
depth += 1
|
29
|
+
|
30
|
+
available_moves(game.board).each do |tile|
|
31
|
+
new_state = get_new_state(tile, game, current_player)
|
32
|
+
|
33
|
+
# I got this idea from the post mentioned in the README
|
34
|
+
scores << mini_max(new_state, next_player(current_player), depth)
|
35
|
+
moves << tile
|
36
|
+
end
|
37
|
+
return choose_move(current_player, scores, moves)
|
38
|
+
end
|
39
|
+
|
40
|
+
def available_moves(board)
|
41
|
+
unmarked_tiles(board)
|
42
|
+
end
|
43
|
+
|
44
|
+
def human?
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def new_board?(board)
|
50
|
+
unmarked_tiles(board).size == (board.width*board.height)
|
51
|
+
end
|
52
|
+
|
53
|
+
def corners(board)
|
54
|
+
corners = []
|
55
|
+
|
56
|
+
corners << board.first[0]
|
57
|
+
corners << board.first.last
|
58
|
+
|
59
|
+
corners << board.last.first
|
60
|
+
corners << board.last.last
|
61
|
+
end
|
62
|
+
|
63
|
+
def initial_game_state(board, turn_num, symbol)
|
64
|
+
GameState.new(board, turn_num, symbol)
|
65
|
+
end
|
66
|
+
|
67
|
+
def choose_move(current_player, scores, moves)
|
68
|
+
if current_player == symbol
|
69
|
+
max_index = scores.each_with_index.max[1]
|
70
|
+
@move = moves[max_index]
|
71
|
+
return scores[max_index]
|
72
|
+
else
|
73
|
+
min_index = scores.each_with_index.min[1]
|
74
|
+
@move = moves[min_index]
|
75
|
+
return scores[min_index]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def evaluate(game, depth)
|
80
|
+
if game.end_state? == symbol
|
81
|
+
return 10 - depth
|
82
|
+
elsif game.end_state? == opponent
|
83
|
+
return depth - 10
|
84
|
+
else
|
85
|
+
return 0
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_new_state(tile, game, current_player)
|
90
|
+
# copy board class and deep copy the inner board array
|
91
|
+
# Marshal is necessary b/c board array does not contain
|
92
|
+
# Plain Old Ruby Objects
|
93
|
+
temp_board = game.board.dup
|
94
|
+
board_array = Marshal.load(Marshal.dump(game.board.board))
|
95
|
+
temp_board.board = board_array
|
96
|
+
|
97
|
+
temp_board[tile.xval][tile.yval].mark(current_player)
|
98
|
+
|
99
|
+
GameState.new(temp_board, game.turn_num+1, current_player)
|
100
|
+
end
|
101
|
+
|
102
|
+
def next_player(current_player)
|
103
|
+
if current_player == "x"
|
104
|
+
"o"
|
105
|
+
else
|
106
|
+
"x"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def opponent
|
111
|
+
next_player(symbol)
|
112
|
+
end
|
113
|
+
|
114
|
+
def unmarked_tiles(board)
|
115
|
+
unmarked = []
|
116
|
+
board.each do |row|
|
117
|
+
row.each do |tile|
|
118
|
+
unmarked << tile unless tile.marked?
|
119
|
+
end
|
120
|
+
end
|
121
|
+
unmarked
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module JbcdenTtt
|
2
|
+
class DisplayBoard
|
3
|
+
|
4
|
+
ROW_NAMES = [
|
5
|
+
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
|
6
|
+
"n", "o", "p", "q", "r", "t", "u", "v", "w", "x", "y", "z"
|
7
|
+
]
|
8
|
+
|
9
|
+
def self.call(board)
|
10
|
+
str = ""
|
11
|
+
row_num = 0
|
12
|
+
str << print_column_numbers(board.first.size)
|
13
|
+
|
14
|
+
board.each do |row|
|
15
|
+
str << print_row(row, row_num)
|
16
|
+
row_num += 1
|
17
|
+
end
|
18
|
+
str
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def self.print_column_numbers(row_size)
|
23
|
+
str = " "
|
24
|
+
col = 1
|
25
|
+
count = row_size
|
26
|
+
|
27
|
+
row_size.times do
|
28
|
+
str << col.to_s << " "
|
29
|
+
col += 1
|
30
|
+
end
|
31
|
+
str << "\n"
|
32
|
+
str
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.print_row(row, row_num)
|
36
|
+
str = ""
|
37
|
+
str << ROW_NAMES[row_num] << " "
|
38
|
+
row.each_with_index do |column, index|
|
39
|
+
str << column.symbol
|
40
|
+
str << "|" unless index == row.size-1
|
41
|
+
end
|
42
|
+
str << "\n"
|
43
|
+
str
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module JbcdenTtt
|
2
|
+
class GameState
|
3
|
+
attr_reader :board, :turn_num, :current_player
|
4
|
+
def initialize(board, turn_num, current_player)
|
5
|
+
@board = board
|
6
|
+
@turn_num = turn_num
|
7
|
+
@player = current_player
|
8
|
+
end
|
9
|
+
|
10
|
+
def end_state?
|
11
|
+
if turn_num >= min_turns
|
12
|
+
row_win(board) || column_win(board) || diagonal_win(board) || full_board(board)
|
13
|
+
else
|
14
|
+
false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def full_board(board)
|
20
|
+
marked = []
|
21
|
+
board.each do |row|
|
22
|
+
row.each do |tile|
|
23
|
+
marked << tile if tile.marked?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
if marked.size == (board.width*board.height)
|
27
|
+
"cat"
|
28
|
+
else
|
29
|
+
false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def winner(xcount, ocount, board, method)
|
34
|
+
win_num = board.public_method(method.to_sym).call
|
35
|
+
cat = (board.width*board.height)/2 + 1
|
36
|
+
|
37
|
+
if xcount == win_num
|
38
|
+
"x"
|
39
|
+
elsif ocount == win_num
|
40
|
+
"o"
|
41
|
+
elsif ocount == cat || xcount == cat
|
42
|
+
"cat"
|
43
|
+
else
|
44
|
+
false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def diagonal_iterator(board, col_num, iterator)
|
49
|
+
xcount = 0
|
50
|
+
ocount = 0
|
51
|
+
|
52
|
+
board.each do |row|
|
53
|
+
xcount += 1 if row[col_num].symbol == "x"
|
54
|
+
ocount += 1 if row[col_num].symbol == "o"
|
55
|
+
col_num += iterator
|
56
|
+
end
|
57
|
+
winner(xcount, ocount, board, "height")
|
58
|
+
end
|
59
|
+
|
60
|
+
def right_diagonal(board)
|
61
|
+
diagonal_iterator(board, board.width - 1, -1)
|
62
|
+
end
|
63
|
+
|
64
|
+
def left_diagonal(board)
|
65
|
+
diagonal_iterator(board, 0, 1)
|
66
|
+
end
|
67
|
+
|
68
|
+
def diagonal_win(board)
|
69
|
+
left_diagonal(board) ||
|
70
|
+
right_diagonal(board)
|
71
|
+
end
|
72
|
+
|
73
|
+
def column_win(board)
|
74
|
+
xcount = 0
|
75
|
+
ocount = 0
|
76
|
+
col_num = 0
|
77
|
+
|
78
|
+
board.width.times do
|
79
|
+
board.each do |row|
|
80
|
+
xcount += 1 if row[col_num].symbol == "x"
|
81
|
+
ocount += 1 if row[col_num].symbol == "o"
|
82
|
+
end
|
83
|
+
if w = winner(xcount, ocount, board, "height")
|
84
|
+
return w
|
85
|
+
end
|
86
|
+
xcount = 0
|
87
|
+
ocount = 0
|
88
|
+
col_num += 1
|
89
|
+
end
|
90
|
+
false
|
91
|
+
end
|
92
|
+
|
93
|
+
def row_win(board)
|
94
|
+
xcount = 0
|
95
|
+
ocount = 0
|
96
|
+
board.each do |row|
|
97
|
+
row.each do |col|
|
98
|
+
xcount += 1 if col.symbol == "x"
|
99
|
+
ocount += 1 if col.symbol == "o"
|
100
|
+
end
|
101
|
+
if w = winner(xcount, ocount, board, "width")
|
102
|
+
return w
|
103
|
+
end
|
104
|
+
xcount = 0
|
105
|
+
ocount = 0
|
106
|
+
end
|
107
|
+
false
|
108
|
+
end
|
109
|
+
|
110
|
+
def min_turns
|
111
|
+
5
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module JbcdenTtt
|
2
|
+
class Player
|
3
|
+
class InvalidCharacterError < StandardError
|
4
|
+
def initialize(msg='Only "x" and "o" are valid character choices.')
|
5
|
+
super
|
6
|
+
end
|
7
|
+
end
|
8
|
+
attr_reader :symbol
|
9
|
+
def initialize(symbol)
|
10
|
+
if symbol == "x" || symbol == "o"
|
11
|
+
@symbol = symbol
|
12
|
+
else
|
13
|
+
raise InvalidCharacterError
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def make_move(board, coordinate)
|
18
|
+
tile = BoardMapper.map_string(board, coordinate)
|
19
|
+
tile.mark(symbol)
|
20
|
+
end
|
21
|
+
|
22
|
+
def human?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module JbcdenTtt
|
2
|
+
class Tile
|
3
|
+
class InvalidActionError < StandardError
|
4
|
+
def initialize(msg="This tile has already been marked")
|
5
|
+
super
|
6
|
+
end
|
7
|
+
end
|
8
|
+
attr_reader :symbol, :xval, :yval
|
9
|
+
attr_writer :marked, :symbol
|
10
|
+
|
11
|
+
def initialize(symbol, xval, yval)
|
12
|
+
@symbol = symbol
|
13
|
+
@xval = xval
|
14
|
+
@yval = yval
|
15
|
+
end
|
16
|
+
|
17
|
+
def marked?
|
18
|
+
@marked ||= false
|
19
|
+
end
|
20
|
+
|
21
|
+
def mark(new_symbol)
|
22
|
+
if markable?
|
23
|
+
@symbol = new_symbol
|
24
|
+
@marked = true
|
25
|
+
else
|
26
|
+
raise InvalidActionError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def markable?
|
32
|
+
# we need to accept the ' ' because the symbol for the bottom left and right
|
33
|
+
# corners of the board is ' '. Must remember to look at this again later.
|
34
|
+
@symbol == '_' || @symbol == ' '
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative './test_helper'
|
2
|
+
require 'board'
|
3
|
+
require 'board_mapper'
|
4
|
+
|
5
|
+
module JbcdenTtt
|
6
|
+
class BoardMapperTest < MiniTest::Test
|
7
|
+
def setup
|
8
|
+
@board = Board.new(3,3)
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_coordinate_strings_are_mapped_correctly
|
12
|
+
expected = @board[0][0]
|
13
|
+
actual = BoardMapper.map_string(@board, "a1")
|
14
|
+
|
15
|
+
assert_equal expected, actual
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_it_throws_an_error_for_invalid_column
|
19
|
+
assert_raises(BoardMapper::InvalidCoordinateError) {
|
20
|
+
BoardMapper.map_string(@board, "a4")
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_it_throws_an_error_for_invalid_row
|
25
|
+
assert_raises(BoardMapper::InvalidCoordinateError) {
|
26
|
+
BoardMapper.map_string(@board, "d3")
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_coordinates_can_be_mapped_into_strings
|
31
|
+
actual = BoardMapper.map_coordinate(2,2)
|
32
|
+
actual2 = BoardMapper.map_coordinate(6, 7)
|
33
|
+
|
34
|
+
assert_equal "c3", actual
|
35
|
+
assert_equal "g8", actual2
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/test/board_test.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative './test_helper'
|
2
|
+
require 'board'
|
3
|
+
|
4
|
+
module JbcdenTtt
|
5
|
+
class BoardTest < MiniTest::Test
|
6
|
+
def test_board_takes_a_height_and_length
|
7
|
+
board = Board.new(3,3)
|
8
|
+
assert_equal board.height, 3
|
9
|
+
assert_equal board.width, 3
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_internal_board_array_dimensions
|
13
|
+
actual = Board.new(4,5)
|
14
|
+
assert_equal 4, actual[0].size
|
15
|
+
assert_equal 5, actual.size
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_board_is_set_correctly
|
19
|
+
actual = Board.new(5,6)
|
20
|
+
|
21
|
+
assert_equal '_', actual[0][0].symbol
|
22
|
+
assert_equal '_', actual[1][1].symbol
|
23
|
+
assert_equal '_', actual[2][2].symbol
|
24
|
+
assert_equal '_', actual[3][3].symbol
|
25
|
+
assert_equal '_', actual[4][4].symbol
|
26
|
+
|
27
|
+
assert_equal ' ', actual[5][0].symbol
|
28
|
+
assert_equal ' ', actual[5][2].symbol
|
29
|
+
assert_equal ' ', actual[5][4].symbol
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require_relative './test_helper'
|
2
|
+
require 'computer'
|
3
|
+
require 'board'
|
4
|
+
require 'game_state'
|
5
|
+
|
6
|
+
module JbcdenTtt
|
7
|
+
class ComputerTest < MiniTest::Test
|
8
|
+
def setup
|
9
|
+
@board = Board.new(3,3)
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_can_get_list_of_available_moves
|
13
|
+
computer = Computer.new("x")
|
14
|
+
|
15
|
+
@board[0][1].mark("x")
|
16
|
+
@board[1][2].mark("x")
|
17
|
+
@board[2][1].mark("x")
|
18
|
+
|
19
|
+
available_moves = computer.available_moves(@board)
|
20
|
+
|
21
|
+
refute_includes available_moves, @board[0][1]
|
22
|
+
refute_includes available_moves, @board[1][2]
|
23
|
+
refute_includes available_moves, @board[2][1]
|
24
|
+
|
25
|
+
assert_includes available_moves, @board[0][0]
|
26
|
+
assert_includes available_moves, @board[0][2]
|
27
|
+
assert_includes available_moves, @board[1][0]
|
28
|
+
assert_includes available_moves, @board[1][1]
|
29
|
+
assert_includes available_moves, @board[2][0]
|
30
|
+
assert_includes available_moves, @board[2][2]
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_select_winning_move
|
34
|
+
computer = Computer.new("x")
|
35
|
+
|
36
|
+
@board[0][0].mark("o")
|
37
|
+
@board[2][1].mark("o")
|
38
|
+
@board[2][2].mark("o")
|
39
|
+
|
40
|
+
@board[0][2].mark("x")
|
41
|
+
@board[1][0].mark("x")
|
42
|
+
@board[2][0].mark("x")
|
43
|
+
|
44
|
+
game = GameState.new(@board, 7, computer.symbol)
|
45
|
+
|
46
|
+
move = computer.calculate_best_move(@board, game)
|
47
|
+
|
48
|
+
assert_equal "b2", move
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_select_winning_move_depth
|
52
|
+
computer = Computer.new("o")
|
53
|
+
|
54
|
+
@board[2][0].mark("x")
|
55
|
+
@board[2][1].mark("x")
|
56
|
+
|
57
|
+
@board[0][1].mark("o")
|
58
|
+
@board[1][2].mark("o")
|
59
|
+
@board[2][2].mark("o")
|
60
|
+
|
61
|
+
game = GameState.new(@board, 7, computer.symbol)
|
62
|
+
|
63
|
+
move = computer.calculate_best_move(@board, game)
|
64
|
+
|
65
|
+
assert_equal "a3", move
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_computer_marks_best_move
|
69
|
+
computer = Computer.new("o")
|
70
|
+
|
71
|
+
@board[2][0].mark("x")
|
72
|
+
@board[2][1].mark("x")
|
73
|
+
|
74
|
+
@board[0][1].mark("o")
|
75
|
+
@board[1][2].mark("o")
|
76
|
+
@board[2][2].mark("o")
|
77
|
+
|
78
|
+
game = GameState.new(@board, 7, computer.symbol)
|
79
|
+
|
80
|
+
move = computer.calculate_best_move(@board, game)
|
81
|
+
computer.make_move(@board, move)
|
82
|
+
|
83
|
+
tile = BoardMapper.map_string(@board, move)
|
84
|
+
|
85
|
+
assert_equal true, @board[tile.xval][tile.yval].marked?
|
86
|
+
assert_equal "o", @board[tile.xval][tile.yval].symbol
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative './test_helper'
|
2
|
+
require 'board'
|
3
|
+
require 'display_board'
|
4
|
+
|
5
|
+
module JbcdenTtt
|
6
|
+
class DisplayBoardTest < MiniTest::Test
|
7
|
+
def test_board_display
|
8
|
+
actual = Board.new(3,3).display
|
9
|
+
result = <<'BOARD'
|
10
|
+
1 2 3
|
11
|
+
a _|_|_
|
12
|
+
b _|_|_
|
13
|
+
c | |
|
14
|
+
BOARD
|
15
|
+
|
16
|
+
assert_equal result, actual
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative './test_helper'
|
2
|
+
require 'game_state'
|
3
|
+
|
4
|
+
module JbcdenTtt
|
5
|
+
class GameStateTest < MiniTest::Test
|
6
|
+
def setup
|
7
|
+
@board = Board.new(3,3)
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_will_always_return_false_if_min_turns_is_not_met
|
11
|
+
@board[1][0].mark("x")
|
12
|
+
@board[1][1].mark("x")
|
13
|
+
@board[1][2].mark("x")
|
14
|
+
game1 = GameState.new(@board, 1, "x")
|
15
|
+
game2 = GameState.new(@board, 2, "x")
|
16
|
+
game3 = GameState.new(@board, 3, "x")
|
17
|
+
game4 = GameState.new(@board, 4, "x")
|
18
|
+
|
19
|
+
assert_equal false, game1.end_state?
|
20
|
+
assert_equal false, game2.end_state?
|
21
|
+
assert_equal false, game3.end_state?
|
22
|
+
assert_equal false, game4.end_state?
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_will_end_when_a_row_win_condition_is_met
|
26
|
+
@board[1][0].mark("x")
|
27
|
+
@board[1][1].mark("x")
|
28
|
+
@board[1][2].mark("x")
|
29
|
+
game = GameState.new(@board, 5, "x")
|
30
|
+
|
31
|
+
assert_equal "x", game.end_state?
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_will_end_when_a_column_win_condition_is_met
|
35
|
+
@board[0][2].mark("x")
|
36
|
+
@board[1][2].mark("x")
|
37
|
+
@board[2][2].mark("x")
|
38
|
+
game = GameState.new(@board, 5, "x")
|
39
|
+
|
40
|
+
assert_equal "x", game.end_state?
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_will_end_when_a_left_diagonal_win_condition_is_met
|
44
|
+
@board[0][0].mark("x")
|
45
|
+
@board[1][1].mark("x")
|
46
|
+
@board[2][2].mark("x")
|
47
|
+
game = GameState.new(@board, 5, "x")
|
48
|
+
|
49
|
+
assert_equal "x", game.end_state?
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_will_end_when_a_right_diagonal_win_condition_is_met
|
53
|
+
@board[0][2].mark("x")
|
54
|
+
@board[1][1].mark("x")
|
55
|
+
@board[2][0].mark("x")
|
56
|
+
game = GameState.new(@board, 5, "x")
|
57
|
+
|
58
|
+
assert_equal "x", game.end_state?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/test/player_test.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'player'
|
2
|
+
|
3
|
+
module JbcdenTtt
|
4
|
+
class PlayerTest < MiniTest::Test
|
5
|
+
def test_a_player_has_a_symbol
|
6
|
+
player = Player.new("x")
|
7
|
+
|
8
|
+
assert_equal "x", player.symbol
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_a_player_has_a_turn_and_can_pick_a_move
|
12
|
+
board = Board.new(3,3)
|
13
|
+
player = Player.new("x")
|
14
|
+
|
15
|
+
assert_equal false, board[0][1].marked?
|
16
|
+
|
17
|
+
player.make_move(board, "a2")
|
18
|
+
assert_equal true, board[0][1].marked?
|
19
|
+
assert_equal player.symbol, board[0][1].symbol
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/test/test_helper.rb
ADDED
data/test/tile_test.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require_relative './test_helper'
|
2
|
+
require 'tile'
|
3
|
+
|
4
|
+
module JbcdenTtt
|
5
|
+
class TileTest < MiniTest::Test
|
6
|
+
def test_it_has_a_character
|
7
|
+
actual = Tile.new('|', 0, 0)
|
8
|
+
assert_equal '|', actual.symbol
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_it_initially_is_marked
|
12
|
+
actual = Tile.new('|', 0, 0)
|
13
|
+
assert_equal false, actual.marked?
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_can_mark_a_tile
|
17
|
+
tile = Tile.new('_', 0, 0)
|
18
|
+
tile.mark("x")
|
19
|
+
|
20
|
+
assert_equal "x", tile.symbol
|
21
|
+
refute_equal '_', tile.symbol
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_marking_a_tile_marks_it
|
25
|
+
tile = Tile.new('_', 0, 0)
|
26
|
+
assert_equal false, tile.marked?
|
27
|
+
|
28
|
+
tile.mark("x")
|
29
|
+
assert_equal true, tile.marked?
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_cannot_mark_pipe_tiles
|
33
|
+
tile = Tile.new('|', 0, 0)
|
34
|
+
|
35
|
+
assert_raises(Tile::InvalidActionError) {
|
36
|
+
tile.mark("x")
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_tile_knows_its_xcoordinate
|
41
|
+
tile = Tile.new('_', 5, 0)
|
42
|
+
|
43
|
+
assert_equal 5, tile.xval
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_tile_knows_its_ycoordinate
|
47
|
+
tile = Tile.new('_', 5, 0)
|
48
|
+
|
49
|
+
assert_equal 0, tile.yval
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jbcden_ttt
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jacob Chae
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-11-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- jbcden@gmail.com
|
44
|
+
executables:
|
45
|
+
- ttt
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- ".gitignore"
|
50
|
+
- Gemfile
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- bin/ttt
|
55
|
+
- jbcden_ttt.gemspec
|
56
|
+
- lib/jbcden_ttt.rb
|
57
|
+
- lib/jbcden_ttt/board.rb
|
58
|
+
- lib/jbcden_ttt/board_mapper.rb
|
59
|
+
- lib/jbcden_ttt/computer.rb
|
60
|
+
- lib/jbcden_ttt/display_board.rb
|
61
|
+
- lib/jbcden_ttt/game_state.rb
|
62
|
+
- lib/jbcden_ttt/player.rb
|
63
|
+
- lib/jbcden_ttt/tile.rb
|
64
|
+
- lib/jbcden_ttt/version.rb
|
65
|
+
- test/board_mapper_test.rb
|
66
|
+
- test/board_test.rb
|
67
|
+
- test/computer_test.rb
|
68
|
+
- test/display_board_test.rb
|
69
|
+
- test/game_state_test.rb
|
70
|
+
- test/player_test.rb
|
71
|
+
- test/test_helper.rb
|
72
|
+
- test/tile_test.rb
|
73
|
+
homepage: https://github.com/jbcden/tic-tac-toe
|
74
|
+
licenses:
|
75
|
+
- MIT
|
76
|
+
metadata: {}
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
requirements: []
|
92
|
+
rubyforge_project:
|
93
|
+
rubygems_version: 2.2.2
|
94
|
+
signing_key:
|
95
|
+
specification_version: 4
|
96
|
+
summary: A basic command line based game of tic tac toe.
|
97
|
+
test_files:
|
98
|
+
- test/board_mapper_test.rb
|
99
|
+
- test/board_test.rb
|
100
|
+
- test/computer_test.rb
|
101
|
+
- test/display_board_test.rb
|
102
|
+
- test/game_state_test.rb
|
103
|
+
- test/player_test.rb
|
104
|
+
- test/test_helper.rb
|
105
|
+
- test/tile_test.rb
|