tic_tac_toes 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 +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
|