tic_tac_toes 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 +23 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +8 -0
- data/Rakefile +25 -0
- data/bin/tic_tac_toes +23 -0
- data/lib/command_line/io.rb +27 -0
- data/lib/command_line/menu.rb +54 -0
- data/lib/command_line/runner.rb +37 -0
- data/lib/database/pg_wrapper.rb +72 -0
- data/lib/tic_tac_toes/board.rb +62 -0
- data/lib/tic_tac_toes/easy_ai.rb +9 -0
- data/lib/tic_tac_toes/hard_ai.rb +57 -0
- data/lib/tic_tac_toes/history.rb +27 -0
- data/lib/tic_tac_toes/io_interface.rb +96 -0
- data/lib/tic_tac_toes/medium_ai.rb +11 -0
- data/lib/tic_tac_toes/player.rb +20 -0
- data/lib/tic_tac_toes/player_factory.rb +24 -0
- data/lib/tic_tac_toes/rules.rb +64 -0
- data/lib/tic_tac_toes/strings.rb +103 -0
- data/lib/tic_tac_toes/version.rb +3 -0
- data/spec/command_line/menu_spec.rb +103 -0
- data/spec/command_line/runner_spec.rb +119 -0
- data/spec/database/pg_wrapper_spec.rb +56 -0
- data/spec/tic_tac_toes/board_spec.rb +104 -0
- data/spec/tic_tac_toes/easy_ai_spec.rb +20 -0
- data/spec/tic_tac_toes/hard_ai_spec.rb +103 -0
- data/spec/tic_tac_toes/history_spec.rb +41 -0
- data/spec/tic_tac_toes/io_interface_spec.rb +175 -0
- data/spec/tic_tac_toes/medium_ai_spec.rb +22 -0
- data/spec/tic_tac_toes/player_factory_spec.rb +34 -0
- data/spec/tic_tac_toes/player_spec.rb +34 -0
- data/spec/tic_tac_toes/rules_spec.rb +159 -0
- data/spec/tic_tac_toes/spec_helper.rb +13 -0
- data/spec/tic_tac_toes/strings_spec.rb +50 -0
- data/tic_tac_toes.gemspec +24 -0
- metadata +151 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 18a1a5f19c58b04046be154ce6371a79b5c613f2
|
4
|
+
data.tar.gz: d856bf4e9b49e86407f173c5a303c0d920202938
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0424458bf16942f7728584c4b4ec040164ce468f8c7a5fb31a0c671c871de601b2d945d46912cce466ab646afce7503dd0ad1d43350e427aa33a0fd4b0fba158
|
7
|
+
data.tar.gz: ce07f69665def752ea0f74c959764bab06fdf683bcbe984d4971693df77ad8ce741b48651d728ce3be3382cc1a0a78f13a1433e12accf5c1d41d4f7ce8a54ee6
|
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.DS_Store
|
5
|
+
.config
|
6
|
+
.yardoc
|
7
|
+
Gemfile.lock
|
8
|
+
InstalledFiles
|
9
|
+
_yardoc
|
10
|
+
coverage
|
11
|
+
doc/
|
12
|
+
lib/bundler/man
|
13
|
+
pkg
|
14
|
+
rdoc
|
15
|
+
spec/reports
|
16
|
+
test/tmp
|
17
|
+
test/version_tmp
|
18
|
+
tmp
|
19
|
+
*.bundle
|
20
|
+
*.so
|
21
|
+
*.o
|
22
|
+
*.a
|
23
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 bspatafora
|
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
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'pg'
|
3
|
+
|
4
|
+
task :create_tables do
|
5
|
+
connection = establish_connection
|
6
|
+
connection.exec("CREATE TABLE games (
|
7
|
+
id serial primary key,
|
8
|
+
board_size integer,
|
9
|
+
winner varchar)")
|
10
|
+
connection.exec("CREATE TABLE moves (
|
11
|
+
game integer REFERENCES games (id),
|
12
|
+
number integer,
|
13
|
+
token varchar,
|
14
|
+
space integer)")
|
15
|
+
end
|
16
|
+
|
17
|
+
task :destroy_tables do
|
18
|
+
connection = establish_connection
|
19
|
+
connection.exec("DROP TABLE moves")
|
20
|
+
connection.exec("DROP TABLE games")
|
21
|
+
end
|
22
|
+
|
23
|
+
def establish_connection
|
24
|
+
PG.connect(dbname: "tic_tac_toes")
|
25
|
+
end
|
data/bin/tic_tac_toes
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
|
5
|
+
require 'command_line/io'
|
6
|
+
require 'command_line/menu'
|
7
|
+
require 'database/pg_wrapper'
|
8
|
+
require 'tic_tac_toes/io_interface'
|
9
|
+
require 'tic_tac_toes/rules'
|
10
|
+
require 'tic_tac_toes/history'
|
11
|
+
|
12
|
+
require 'command_line/runner'
|
13
|
+
|
14
|
+
database = "tic_tac_toes"
|
15
|
+
|
16
|
+
io = CommandLine::IO
|
17
|
+
io_interface = TicTacToes::IOInterface.new(io)
|
18
|
+
menu = CommandLine::Menu.new(io_interface)
|
19
|
+
rules = TicTacToes::Rules
|
20
|
+
database_interface = Database::PGWrapper.new(database)
|
21
|
+
history = TicTacToes::History.new(database_interface)
|
22
|
+
|
23
|
+
CommandLine::Runner.new(io_interface, menu, rules, history).run
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module CommandLine
|
2
|
+
module IO
|
3
|
+
def self.solicit_input
|
4
|
+
gets.chomp
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.display(message)
|
8
|
+
puts message
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.display_red(message)
|
12
|
+
puts red(message)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.red(message)
|
16
|
+
colorize(message, 31)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.blue(message)
|
20
|
+
colorize(message, 34)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.colorize(message, color_code)
|
24
|
+
"\e[#{color_code}m#{message}\e[0m"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'tic_tac_toes/board'
|
2
|
+
require 'tic_tac_toes/player_factory'
|
3
|
+
require 'tic_tac_toes/rules'
|
4
|
+
|
5
|
+
module CommandLine
|
6
|
+
class Menu
|
7
|
+
def initialize(io_interface)
|
8
|
+
@io_interface = io_interface
|
9
|
+
@player_factory = TicTacToes::PlayerFactory.new(io_interface)
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_board
|
13
|
+
TicTacToes::Board.new(row_size: get_row_size)
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_players
|
17
|
+
taken_tokens = []
|
18
|
+
human_token = get_token(:human, taken_tokens)
|
19
|
+
taken_tokens << human_token
|
20
|
+
computer_token = get_token(:computer, taken_tokens)
|
21
|
+
difficulty = get_difficulty
|
22
|
+
|
23
|
+
human_player = @player_factory.generate_human_player(human_token)
|
24
|
+
computer_player = @player_factory.generate_computer_player(computer_token, difficulty)
|
25
|
+
[human_player, computer_player]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def get_row_size
|
31
|
+
loop do
|
32
|
+
row_size = @io_interface.get_row_size
|
33
|
+
break row_size if TicTacToes::Rules.row_size_valid?(row_size)
|
34
|
+
@io_interface.invalid_row_size_error
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_token(player, taken_tokens)
|
39
|
+
loop do
|
40
|
+
token = @io_interface.get_token(player)
|
41
|
+
break token if TicTacToes::Rules.token_valid?(token, taken_tokens)
|
42
|
+
@io_interface.invalid_token_error
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_difficulty
|
47
|
+
loop do
|
48
|
+
difficulty = @io_interface.get_difficulty
|
49
|
+
break difficulty if TicTacToes::Rules.difficulty_valid?(difficulty)
|
50
|
+
@io_interface.invalid_difficulty_error
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module CommandLine
|
2
|
+
class Runner
|
3
|
+
def initialize(io_interface, menu, rules, history)
|
4
|
+
@io_interface = io_interface
|
5
|
+
@menu = menu
|
6
|
+
@rules = rules
|
7
|
+
@history = history
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
board, players = @menu.get_board, @menu.get_players
|
12
|
+
@history.record_board_size(board.size)
|
13
|
+
|
14
|
+
take_turn(board, players) until @rules.game_over?(board, players)
|
15
|
+
end_game(board, players)
|
16
|
+
end
|
17
|
+
|
18
|
+
def take_turn(board, players)
|
19
|
+
@io_interface.draw_board(board)
|
20
|
+
@io_interface.thinking_notification if players.first.needs_to_think
|
21
|
+
|
22
|
+
move = players.first.make_move(board, players)
|
23
|
+
@history.record_move(move)
|
24
|
+
players.rotate!
|
25
|
+
end
|
26
|
+
|
27
|
+
def end_game(board, players)
|
28
|
+
@io_interface.draw_board(board)
|
29
|
+
|
30
|
+
winner = @rules.determine_winner(board, players)
|
31
|
+
|
32
|
+
@history.record_winner(winner)
|
33
|
+
@history.persist
|
34
|
+
@io_interface.game_over_notification(winner)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'pg'
|
2
|
+
require 'tic_tac_toes/history'
|
3
|
+
|
4
|
+
module Database
|
5
|
+
class PGWrapper
|
6
|
+
def initialize(database)
|
7
|
+
@database = database
|
8
|
+
end
|
9
|
+
|
10
|
+
def record_game_history(history)
|
11
|
+
connection = establish_connection
|
12
|
+
record_board_size_and_winner(history, connection)
|
13
|
+
record_moves(history, connection)
|
14
|
+
end
|
15
|
+
|
16
|
+
def read_game_histories
|
17
|
+
connection = establish_connection
|
18
|
+
games_result = connection.exec("SELECT * FROM games")
|
19
|
+
games = []
|
20
|
+
|
21
|
+
games_result.each_row do |row|
|
22
|
+
game_id = row[0]
|
23
|
+
board_size = row[1].to_i
|
24
|
+
winner = row[2]
|
25
|
+
moves_result = connection.exec("SELECT * FROM moves WHERE game = #{game_id}")
|
26
|
+
|
27
|
+
history = TicTacToes::History.new(self)
|
28
|
+
|
29
|
+
moves_result.each_row do |row|
|
30
|
+
token, space = row[2], row[3].to_i
|
31
|
+
history.record_move([token, space])
|
32
|
+
end
|
33
|
+
history.record_board_size(board_size)
|
34
|
+
history.record_winner(winner)
|
35
|
+
|
36
|
+
games << history
|
37
|
+
end
|
38
|
+
|
39
|
+
games
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def establish_connection
|
45
|
+
PG.connect(dbname: @database)
|
46
|
+
end
|
47
|
+
|
48
|
+
def record_board_size_and_winner(history, connection)
|
49
|
+
connection.exec("INSERT INTO games (board_size, winner) VALUES (
|
50
|
+
#{history.board_size},
|
51
|
+
'#{history.winner}')")
|
52
|
+
end
|
53
|
+
|
54
|
+
def record_moves(history, connection)
|
55
|
+
game_id = read_game_id(connection)
|
56
|
+
|
57
|
+
history.moves.each_with_index do |move, index|
|
58
|
+
move_number = index + 1
|
59
|
+
connection.exec("INSERT INTO moves (game, number, token, space) VALUES (
|
60
|
+
#{game_id},
|
61
|
+
#{move_number},
|
62
|
+
'#{move.first}',
|
63
|
+
#{move.last})")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def read_game_id(connection)
|
68
|
+
result = connection.exec("SELECT currval(pg_get_serial_sequence('games','id'))")
|
69
|
+
result.getvalue(0,0)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module TicTacToes
|
2
|
+
class Board
|
3
|
+
attr_reader :row_size, :size
|
4
|
+
|
5
|
+
def initialize(row_size: 3)
|
6
|
+
@row_size = row_size
|
7
|
+
@size = @row_size**2
|
8
|
+
@spaces = Array.new(@size)
|
9
|
+
end
|
10
|
+
|
11
|
+
def place(player, space)
|
12
|
+
@spaces[space] = player if valid?(space)
|
13
|
+
end
|
14
|
+
|
15
|
+
def space(space)
|
16
|
+
@spaces[space]
|
17
|
+
end
|
18
|
+
|
19
|
+
def open_spaces
|
20
|
+
open_spaces = []
|
21
|
+
|
22
|
+
@spaces.each_with_index do |space, index|
|
23
|
+
open_spaces << index if space.nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
open_spaces
|
27
|
+
end
|
28
|
+
|
29
|
+
def rows
|
30
|
+
@spaces.each_slice(@row_size).to_a
|
31
|
+
end
|
32
|
+
|
33
|
+
def columns
|
34
|
+
rows.transpose
|
35
|
+
end
|
36
|
+
|
37
|
+
def diagonals
|
38
|
+
back_diagonal, front_diagonal = [], []
|
39
|
+
|
40
|
+
rows.each_with_index do |row, index|
|
41
|
+
back_diagonal << row[index]
|
42
|
+
front_diagonal << row[@row_size - (index + 1)]
|
43
|
+
end
|
44
|
+
|
45
|
+
[back_diagonal, front_diagonal]
|
46
|
+
end
|
47
|
+
|
48
|
+
def full?
|
49
|
+
@spaces.all? { |space| !space.nil? }
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def valid?(space)
|
55
|
+
space_empty = @spaces[space].nil?
|
56
|
+
board_range = 0..(@size - 1)
|
57
|
+
on_the_board = board_range.include? space
|
58
|
+
|
59
|
+
space_empty && on_the_board
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'tic_tac_toes/rules'
|
2
|
+
|
3
|
+
module TicTacToes
|
4
|
+
module HardAI
|
5
|
+
def self.make_move(board, players)
|
6
|
+
open_spaces = Hash[board.open_spaces.map { |space| [space, nil] }]
|
7
|
+
|
8
|
+
open_spaces.each do |space, score|
|
9
|
+
score = minimax(generate_board(players.first, space, board), :min, players)
|
10
|
+
open_spaces[space] = score
|
11
|
+
end
|
12
|
+
|
13
|
+
best_score = open_spaces.values.max
|
14
|
+
open_spaces.each { |space, score| return space if score == best_score }
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.minimax(board, current_player, players)
|
18
|
+
return score(board, players) if Rules.game_over?(board, players)
|
19
|
+
|
20
|
+
if current_player == :max
|
21
|
+
best_score = -1
|
22
|
+
board.open_spaces.each do |space|
|
23
|
+
score = minimax(generate_board(players.first, space, board), :min, players)
|
24
|
+
best_score = [best_score, score].max
|
25
|
+
end
|
26
|
+
best_score
|
27
|
+
|
28
|
+
elsif current_player == :min
|
29
|
+
best_score = 1
|
30
|
+
board.open_spaces.each do |space|
|
31
|
+
score = minimax(generate_board(players.last, space, board), :max, players)
|
32
|
+
best_score = [best_score, score].min
|
33
|
+
end
|
34
|
+
best_score
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.generate_board(player, space, board)
|
39
|
+
new_board = Marshal.load(Marshal.dump(board))
|
40
|
+
new_board.place(player, space)
|
41
|
+
new_board
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.score(board, players)
|
45
|
+
own_token, opponent_token = players.first.token, players.last.token
|
46
|
+
|
47
|
+
case Rules.determine_winner(board, players)
|
48
|
+
when own_token
|
49
|
+
1
|
50
|
+
when opponent_token
|
51
|
+
-1
|
52
|
+
else
|
53
|
+
0
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module TicTacToes
|
2
|
+
class History
|
3
|
+
attr_reader :board_size, :moves, :winner
|
4
|
+
|
5
|
+
def initialize(database_interface)
|
6
|
+
@database_interface = database_interface
|
7
|
+
end
|
8
|
+
|
9
|
+
def record_board_size(size)
|
10
|
+
@board_size = size
|
11
|
+
end
|
12
|
+
|
13
|
+
def record_move(move)
|
14
|
+
@moves ||= []
|
15
|
+
@moves << move
|
16
|
+
end
|
17
|
+
|
18
|
+
def record_winner(winner)
|
19
|
+
winner = "Draw" if winner.nil?
|
20
|
+
@winner = winner
|
21
|
+
end
|
22
|
+
|
23
|
+
def persist
|
24
|
+
@database_interface.record_game_history(self)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'tic_tac_toes/strings'
|
2
|
+
|
3
|
+
module TicTacToes
|
4
|
+
class IOInterface
|
5
|
+
def initialize(io)
|
6
|
+
@io = io
|
7
|
+
end
|
8
|
+
|
9
|
+
def make_move(_board, _players)
|
10
|
+
move_solicitation
|
11
|
+
|
12
|
+
Integer(@io.solicit_input)
|
13
|
+
rescue ArgumentError
|
14
|
+
not_an_integer_error
|
15
|
+
make_move(_board, _players)
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_row_size
|
19
|
+
row_size_solicitation
|
20
|
+
|
21
|
+
Integer(@io.solicit_input)
|
22
|
+
rescue ArgumentError
|
23
|
+
not_an_integer_error
|
24
|
+
get_row_size
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_token(player)
|
28
|
+
token_solicitation(player)
|
29
|
+
@io.solicit_input
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_difficulty
|
33
|
+
difficulty_solicitation
|
34
|
+
@io.solicit_input.downcase.to_sym
|
35
|
+
end
|
36
|
+
|
37
|
+
def draw_board(board)
|
38
|
+
@io.display(Strings.board(board))
|
39
|
+
end
|
40
|
+
|
41
|
+
def invalid_row_size_error
|
42
|
+
@io.display_red(Strings::INVALID_ROW_SIZE)
|
43
|
+
end
|
44
|
+
|
45
|
+
def invalid_token_error
|
46
|
+
@io.display_red(Strings::INVALID_TOKEN)
|
47
|
+
end
|
48
|
+
|
49
|
+
def invalid_difficulty_error
|
50
|
+
@io.display_red(Strings::INVALID_DIFFICULTY)
|
51
|
+
end
|
52
|
+
|
53
|
+
def invalid_move_error
|
54
|
+
@io.display_red(Strings::INVALID_MOVE)
|
55
|
+
end
|
56
|
+
|
57
|
+
def thinking_notification
|
58
|
+
@io.display_red(Strings::THINKING)
|
59
|
+
end
|
60
|
+
|
61
|
+
def game_over_notification(winner)
|
62
|
+
winner = "Nobody" if winner.nil?
|
63
|
+
@io.display(Strings.game_over_notification(winner))
|
64
|
+
end
|
65
|
+
|
66
|
+
def red(message)
|
67
|
+
@io.red(message)
|
68
|
+
end
|
69
|
+
|
70
|
+
def blue(message)
|
71
|
+
@io.blue(message)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def move_solicitation
|
77
|
+
@io.display(Strings::MOVE_SOLICITATION)
|
78
|
+
end
|
79
|
+
|
80
|
+
def row_size_solicitation
|
81
|
+
@io.display(Strings::ROW_SIZE_SOLICITATION)
|
82
|
+
end
|
83
|
+
|
84
|
+
def token_solicitation(player)
|
85
|
+
@io.display(Strings.token_solicitation(player))
|
86
|
+
end
|
87
|
+
|
88
|
+
def difficulty_solicitation
|
89
|
+
@io.display(Strings::DIFFICULTY_SOLICITATION)
|
90
|
+
end
|
91
|
+
|
92
|
+
def not_an_integer_error
|
93
|
+
@io.display_red(Strings::NOT_AN_INTEGER)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module TicTacToes
|
2
|
+
class Player
|
3
|
+
attr_reader :decider, :token, :needs_to_think
|
4
|
+
|
5
|
+
def initialize(decider, token, needs_to_think, io_interface)
|
6
|
+
@decider = decider
|
7
|
+
@token = token
|
8
|
+
@needs_to_think = needs_to_think
|
9
|
+
@io_interface = io_interface
|
10
|
+
end
|
11
|
+
|
12
|
+
def make_move(board, players)
|
13
|
+
loop do
|
14
|
+
space = @decider.make_move(board, players)
|
15
|
+
break [@token, space] if board.place(self, space)
|
16
|
+
@io_interface.invalid_move_error
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'tic_tac_toes/easy_ai'
|
2
|
+
require 'tic_tac_toes/hard_ai'
|
3
|
+
require 'tic_tac_toes/medium_ai'
|
4
|
+
require 'tic_tac_toes/player'
|
5
|
+
|
6
|
+
module TicTacToes
|
7
|
+
class PlayerFactory
|
8
|
+
AI_DIFFICULTIES = { easy: EasyAI, medium: MediumAI, hard: HardAI }
|
9
|
+
|
10
|
+
def initialize(io_interface)
|
11
|
+
@io_interface = io_interface
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate_human_player(token)
|
15
|
+
needs_to_think = false
|
16
|
+
Player.new(@io_interface, token, needs_to_think, @io_interface)
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate_computer_player(token, difficulty)
|
20
|
+
needs_to_think = true
|
21
|
+
Player.new(AI_DIFFICULTIES[difficulty], token, needs_to_think, @io_interface)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'tic_tac_toes/player_factory'
|
2
|
+
|
3
|
+
module TicTacToes
|
4
|
+
module Rules
|
5
|
+
ROW_SIZE_RANGE = (2..10)
|
6
|
+
|
7
|
+
def self.row_size_valid?(row_size)
|
8
|
+
row_size.between?(ROW_SIZE_RANGE.min, ROW_SIZE_RANGE.max)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.token_valid?(token, taken_tokens)
|
12
|
+
correct_length = token.length == 1
|
13
|
+
untaken = !taken_tokens.include?(token)
|
14
|
+
|
15
|
+
correct_length && untaken
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.difficulty_valid?(difficulty)
|
19
|
+
PlayerFactory::AI_DIFFICULTIES.include? difficulty
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.game_over?(board, players)
|
23
|
+
winner = !determine_winner(board, players).nil?
|
24
|
+
tie = board.full?
|
25
|
+
|
26
|
+
winner || tie
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.determine_winner(board, players)
|
30
|
+
winner = nil
|
31
|
+
|
32
|
+
players.each do |player|
|
33
|
+
player_has_won = win?(board, player)
|
34
|
+
winner = player.token if player_has_won
|
35
|
+
end
|
36
|
+
|
37
|
+
winner
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.win?(board, player)
|
41
|
+
diagonal_win?(board, player) ||
|
42
|
+
horizontal_win?(board, player) ||
|
43
|
+
vertical_win?(board, player)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def self.diagonal_win?(board, player)
|
49
|
+
set_win?(board.diagonals, player)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.horizontal_win?(board, player)
|
53
|
+
set_win?(board.rows, player)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.vertical_win?(board, player)
|
57
|
+
set_win?(board.columns, player)
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.set_win?(sets, player)
|
61
|
+
sets.any? { |set| set.all? { |space| space.token == player.token unless space.nil? } }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|