jbcden_ttt 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|