tic_tac_toe_nhu 0.0.2
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 +3 -0
- data/.ruby-version +1 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +39 -0
- data/README.md +19 -0
- data/Rakefile +15 -0
- data/bin/tic_tac_toe +6 -0
- data/coverage/.last_run.json +5 -0
- data/coverage/.resultset.json +502 -0
- data/features/step_definitions/tictactoe_steps.rb +6 -0
- data/features/support/env.rb +2 -0
- data/features/tictactoe_start_game.feature +11 -0
- data/features/tictactoe_submit_move.feature +9 -0
- data/lib/tic_tac_toe/board.rb +99 -0
- data/lib/tic_tac_toe/game.rb +41 -0
- data/lib/tic_tac_toe/game_factory.rb +55 -0
- data/lib/tic_tac_toe/main.rb +47 -0
- data/lib/tic_tac_toe/player.rb +15 -0
- data/lib/tic_tac_toe/player_factory.rb +17 -0
- data/lib/tic_tac_toe/rules.rb +32 -0
- data/lib/tic_tac_toe/strategy/console_user.rb +26 -0
- data/lib/tic_tac_toe/strategy/minimax.rb +83 -0
- data/lib/tic_tac_toe/ui/console.rb +60 -0
- data/spec/integration/tictactoe/tic_tac_toe.rb +7 -0
- data/spec/mocks/game.rb +11 -0
- data/spec/mocks/game_factory.rb +8 -0
- data/spec/mocks/player_factory.rb +8 -0
- data/spec/mocks/rules.rb +9 -0
- data/spec/mocks/strategy/dynamic.rb +19 -0
- data/spec/mocks/ui/console.rb +15 -0
- data/spec/tic_tac_toe/board_spec.rb +186 -0
- data/spec/tic_tac_toe/game_factory_spec.rb +78 -0
- data/spec/tic_tac_toe/game_spec.rb +94 -0
- data/spec/tic_tac_toe/main_spec.rb +104 -0
- data/spec/tic_tac_toe/player_factory_mock.rb +10 -0
- data/spec/tic_tac_toe/player_factory_spec.rb +59 -0
- data/spec/tic_tac_toe/player_spec.rb +32 -0
- data/spec/tic_tac_toe/rules_spec.rb +185 -0
- data/spec/tic_tac_toe/spec_helper.rb +9 -0
- data/spec/tic_tac_toe/strategy/console_user_spec.rb +34 -0
- data/spec/tic_tac_toe/strategy/minimax_spec.rb +163 -0
- data/spec/tic_tac_toe/strategy/mock.rb +19 -0
- data/spec/tic_tac_toe/ui/console_spec.rb +93 -0
- data/tic_tac_toe_nhu.gemspec +11 -0
- metadata +86 -0
@@ -0,0 +1,99 @@
|
|
1
|
+
module TicTacToe
|
2
|
+
class MoveNotAvailableError < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class Board
|
6
|
+
attr_reader :squares, :size, :unique_marked_values
|
7
|
+
|
8
|
+
def initialize(size = 3)
|
9
|
+
@size = size
|
10
|
+
reset
|
11
|
+
end
|
12
|
+
|
13
|
+
def reset
|
14
|
+
@squares = Array.new(size**2)
|
15
|
+
@unique_marked_values = []
|
16
|
+
@moves_history = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def mark(move, value)
|
20
|
+
if move_available?(move)
|
21
|
+
squares[move] = value
|
22
|
+
@unique_marked_values << value if !@unique_marked_values.include?(value)
|
23
|
+
else
|
24
|
+
raise MoveNotAvailableError
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def clear(move)
|
29
|
+
squares[move] = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def filled?
|
33
|
+
available_moves.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def number_of_moves
|
37
|
+
squares.size - available_moves.size
|
38
|
+
end
|
39
|
+
|
40
|
+
def rows
|
41
|
+
(0...size).inject([]) do |result, row|
|
42
|
+
result << squares[row*size, size]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def columns
|
47
|
+
result = []
|
48
|
+
(0...size).each do |col|
|
49
|
+
result << squares.values_at(* squares.each_index.select do |i|
|
50
|
+
(col - i) % 3 == 0
|
51
|
+
end)
|
52
|
+
end
|
53
|
+
result
|
54
|
+
end
|
55
|
+
|
56
|
+
def diagonals
|
57
|
+
left_diagonal = diagonal_from_top_left
|
58
|
+
right_diagonal = diagonal_from_top_right
|
59
|
+
[left_diagonal, right_diagonal]
|
60
|
+
end
|
61
|
+
|
62
|
+
def available_moves
|
63
|
+
result = []
|
64
|
+
(0...squares.size).each do |move|
|
65
|
+
result << move if move_available?(move)
|
66
|
+
end
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
def move_available?(move)
|
72
|
+
return false if out_of_range?(move)
|
73
|
+
return false if marked?(move)
|
74
|
+
|
75
|
+
true
|
76
|
+
end
|
77
|
+
|
78
|
+
def out_of_range?(move)
|
79
|
+
return true if move < 0
|
80
|
+
return true if move >= squares.size
|
81
|
+
return false
|
82
|
+
end
|
83
|
+
|
84
|
+
def marked?(move)
|
85
|
+
squares[move]
|
86
|
+
end
|
87
|
+
|
88
|
+
def diagonal_from_top_right
|
89
|
+
result = squares.values_at(*squares.each_index.select do |i|
|
90
|
+
i % (size - 1) == 0
|
91
|
+
end)
|
92
|
+
result[1, size]
|
93
|
+
end
|
94
|
+
|
95
|
+
def diagonal_from_top_left
|
96
|
+
squares.values_at(*squares.each_index.select {|i| i % (size + 1) == 0})
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'tic_tac_toe/rules'
|
2
|
+
|
3
|
+
module TicTacToe
|
4
|
+
class Game
|
5
|
+
attr_reader :current_player, :board
|
6
|
+
|
7
|
+
def initialize(board, player1, player2)
|
8
|
+
@board = board
|
9
|
+
@current_player = @player1 = player1
|
10
|
+
@player2 = player2
|
11
|
+
@rules = TicTacToe::Rules.new(@board)
|
12
|
+
end
|
13
|
+
|
14
|
+
def make_move
|
15
|
+
player_move = @current_player.move
|
16
|
+
if player_move
|
17
|
+
@board.mark(player_move, @current_player.value)
|
18
|
+
change_player
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def over?
|
23
|
+
@rules.game_over?
|
24
|
+
end
|
25
|
+
|
26
|
+
def winner
|
27
|
+
player(@rules.winner)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def change_player
|
32
|
+
@current_player = (@current_player == @player1) ? @player2 : @player1
|
33
|
+
end
|
34
|
+
|
35
|
+
def player(mark)
|
36
|
+
return @player1 if mark == @player1.value
|
37
|
+
return @player2 if mark == @player2.value
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'tic_tac_toe/game'
|
2
|
+
require 'tic_tac_toe/board'
|
3
|
+
require 'tic_tac_toe/player_factory'
|
4
|
+
|
5
|
+
module TicTacToe
|
6
|
+
class GameFactory
|
7
|
+
def initialize(player_factory = TicTacToe::PlayerFactory.new)
|
8
|
+
@player_factory = player_factory
|
9
|
+
end
|
10
|
+
|
11
|
+
def types
|
12
|
+
["You vs Computer", "Computer vs You", "You vs Friend", "Computer vs Computer"]
|
13
|
+
end
|
14
|
+
|
15
|
+
def create(type_index, board)
|
16
|
+
case type_index
|
17
|
+
when 1
|
18
|
+
human_computer_game(board)
|
19
|
+
when 2
|
20
|
+
computer_human_game(board)
|
21
|
+
when 3
|
22
|
+
human_human_game(board)
|
23
|
+
when 4
|
24
|
+
computer_computer_game(board)
|
25
|
+
else
|
26
|
+
raise ArgumentError, "Type does not exist. Please select a number corresponding to the game type."
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def computer_human_game(board)
|
32
|
+
computer = @player_factory.computer(board)
|
33
|
+
human = @player_factory.human
|
34
|
+
TicTacToe::Game.new(board, computer, human)
|
35
|
+
end
|
36
|
+
|
37
|
+
def human_computer_game(board)
|
38
|
+
computer = @player_factory.computer(board)
|
39
|
+
human = @player_factory.human
|
40
|
+
TicTacToe::Game.new(board, human, computer)
|
41
|
+
end
|
42
|
+
|
43
|
+
def human_human_game(board)
|
44
|
+
human1 = @player_factory.human
|
45
|
+
human2 = @player_factory.human("Friend", "O")
|
46
|
+
TicTacToe::Game.new(board, human1, human2)
|
47
|
+
end
|
48
|
+
|
49
|
+
def computer_computer_game(board)
|
50
|
+
computer1 = @player_factory.computer(board, "X", "O")
|
51
|
+
computer2 = @player_factory.computer(board, "O", "X")
|
52
|
+
TicTacToe::Game.new(board, computer1, computer2)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'tic_tac_toe/ui/console'
|
2
|
+
require 'tic_tac_toe/game_factory'
|
3
|
+
require 'tic_tac_toe/board'
|
4
|
+
|
5
|
+
module TicTacToe
|
6
|
+
class Main
|
7
|
+
attr_writer :rules
|
8
|
+
|
9
|
+
def initialize(ui = TicTacToe::Console.new, game_factory = TicTacToe::GameFactory.new)
|
10
|
+
@ui = ui
|
11
|
+
@board = TicTacToe::Board.new
|
12
|
+
@game_factory = game_factory
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
@ui.display_welcome_message
|
17
|
+
game_type = @ui.game_type
|
18
|
+
@game = @game_factory.create(game_type, @board)
|
19
|
+
|
20
|
+
play until @game.over?
|
21
|
+
@ui.display_board(@board)
|
22
|
+
display_result
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def play
|
27
|
+
@ui.display_board(@board)
|
28
|
+
player = @game.current_player
|
29
|
+
@ui.display_player_turn(player)
|
30
|
+
begin
|
31
|
+
@game.make_move
|
32
|
+
rescue MoveNotAvailableError
|
33
|
+
@ui.display_square_not_available
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def display_result
|
38
|
+
winner = @game.winner
|
39
|
+
if winner
|
40
|
+
@ui.display_winner(winner)
|
41
|
+
else
|
42
|
+
@ui.display_tied_game
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'tic_tac_toe/strategy/minimax'
|
2
|
+
require 'tic_tac_toe/strategy/console_user'
|
3
|
+
require 'tic_tac_toe/player'
|
4
|
+
|
5
|
+
module TicTacToe
|
6
|
+
class PlayerFactory
|
7
|
+
|
8
|
+
def human(name = "User", value = "X")
|
9
|
+
TicTacToe::Player.new(name, value, TicTacToe::Strategy::ConsoleUser.new)
|
10
|
+
end
|
11
|
+
|
12
|
+
def computer(board, value = "O", opponent_value = "X")
|
13
|
+
ai = TicTacToe::Strategy::Minimax.new(board, value, opponent_value)
|
14
|
+
TicTacToe::Player.new("Computer", value, ai)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module TicTacToe
|
2
|
+
class Rules
|
3
|
+
def initialize(board)
|
4
|
+
@board = board
|
5
|
+
end
|
6
|
+
|
7
|
+
def game_over?
|
8
|
+
return true if winner
|
9
|
+
return true if @board.filled?
|
10
|
+
false
|
11
|
+
end
|
12
|
+
|
13
|
+
def tied?
|
14
|
+
!winner && @board.filled?
|
15
|
+
end
|
16
|
+
|
17
|
+
def winner
|
18
|
+
@board.unique_marked_values.detect {|p| win?(p)}
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def win?(player)
|
23
|
+
square_sets.any? do |squares|
|
24
|
+
squares.all? {|square| square == player}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def square_sets
|
29
|
+
@board.rows + @board.columns + @board.diagonals
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module TicTacToe
|
2
|
+
module Strategy
|
3
|
+
class ConsoleUser
|
4
|
+
|
5
|
+
def initialize(input = STDIN, output = STDOUT)
|
6
|
+
@input = input
|
7
|
+
@output = output
|
8
|
+
end
|
9
|
+
|
10
|
+
def move
|
11
|
+
move = nil
|
12
|
+
|
13
|
+
until move =~ NUMBER
|
14
|
+
@output.puts("Please enter a square number that is not marked: ")
|
15
|
+
move = @input.gets
|
16
|
+
end
|
17
|
+
|
18
|
+
move.to_i
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
NUMBER = /\d/
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'tic_tac_toe/rules'
|
2
|
+
|
3
|
+
module TicTacToe
|
4
|
+
module Strategy
|
5
|
+
class Minimax
|
6
|
+
def initialize(board, player, opponent)
|
7
|
+
@board = board
|
8
|
+
@player = player
|
9
|
+
@opponent = opponent
|
10
|
+
@rules = TicTacToe::Rules.new(@board)
|
11
|
+
end
|
12
|
+
|
13
|
+
def move
|
14
|
+
if first_move?
|
15
|
+
first_move
|
16
|
+
else
|
17
|
+
move = minimax(@player)
|
18
|
+
move.move
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
WINNING_SCORE = 1
|
24
|
+
LOSING_SCORE = -1
|
25
|
+
TIE = 0
|
26
|
+
MIDDLE_SQUARE = 4
|
27
|
+
|
28
|
+
def first_move?
|
29
|
+
moves_count = @board.number_of_moves
|
30
|
+
moves_count == 0 || moves_count == 1
|
31
|
+
end
|
32
|
+
|
33
|
+
def first_move
|
34
|
+
return MIDDLE_SQUARE if @board.available_moves.include?(MIDDLE_SQUARE)
|
35
|
+
0
|
36
|
+
end
|
37
|
+
|
38
|
+
def minimax(player)
|
39
|
+
moves = []
|
40
|
+
@board.available_moves.each do |move|
|
41
|
+
@board.mark(move, player)
|
42
|
+
moves << player_move(player, move)
|
43
|
+
@board.clear(move)
|
44
|
+
found_best_move?(moves[-1])
|
45
|
+
end
|
46
|
+
best_move(moves)
|
47
|
+
end
|
48
|
+
|
49
|
+
def player_move(player, move)
|
50
|
+
if @rules.game_over?
|
51
|
+
PlayerMove.new(move, score(player), 0)
|
52
|
+
else
|
53
|
+
child_move = minimax(opponent(player))
|
54
|
+
PlayerMove.new(move, -child_move.score, child_move.depth += 1)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def found_best_move?(move)
|
59
|
+
move.score == WINNING_SCORE and move.depth == 0
|
60
|
+
end
|
61
|
+
|
62
|
+
def score(player)
|
63
|
+
winner = @rules.winner
|
64
|
+
return WINNING_SCORE if winner == player
|
65
|
+
return LOSING_SCORE if winner == opponent(player)
|
66
|
+
return TIE if @rules.tied?
|
67
|
+
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def opponent(player)
|
72
|
+
(player == @player) ? @opponent : @player
|
73
|
+
end
|
74
|
+
|
75
|
+
def best_move(moves)
|
76
|
+
sorted_moves = moves.sort{ |a, b| [a.score, a.depth] <=> [b.score, b.depth]}
|
77
|
+
sorted_moves.max_by {|m| m.score}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
PlayerMove = Struct.new(:move, :score, :depth)
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'tic_tac_toe/game_factory'
|
2
|
+
|
3
|
+
module TicTacToe
|
4
|
+
class Console
|
5
|
+
|
6
|
+
def initialize(input = STDIN, output=STDOUT)
|
7
|
+
@input = input
|
8
|
+
@output = output
|
9
|
+
end
|
10
|
+
|
11
|
+
def display_welcome_message
|
12
|
+
@output.puts("Welcome to Tic Tac Toe!")
|
13
|
+
end
|
14
|
+
|
15
|
+
def display_board(board)
|
16
|
+
@output.puts(build_board(board))
|
17
|
+
end
|
18
|
+
|
19
|
+
def game_type
|
20
|
+
@output.puts("Please select a game type.")
|
21
|
+
@output.puts(game_type_list)
|
22
|
+
type = @input.gets
|
23
|
+
type.to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
def display_winner(winner)
|
27
|
+
@output.puts("#{winner.name}(#{winner.value}) win!")
|
28
|
+
end
|
29
|
+
|
30
|
+
def display_tied_game
|
31
|
+
@output.puts("It's a tied!")
|
32
|
+
end
|
33
|
+
|
34
|
+
def display_square_not_available
|
35
|
+
@output.puts("Square is not available. Please enter a different square.")
|
36
|
+
end
|
37
|
+
|
38
|
+
def display_player_turn(player)
|
39
|
+
@output.puts("It's #{player.name}(#{player.value}) turn.")
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def build_board(board)
|
44
|
+
result = ""
|
45
|
+
board.squares.each_with_index do |value, index|
|
46
|
+
result << "| #{value || index} "
|
47
|
+
result << "|\n" if (index + 1) % board.size == 0
|
48
|
+
end
|
49
|
+
result
|
50
|
+
end
|
51
|
+
|
52
|
+
def game_type_list
|
53
|
+
result = ""
|
54
|
+
TicTacToe::GameFactory.new.types.each_with_index do |value, index|
|
55
|
+
result << "#{index + 1} - #{value}\n"
|
56
|
+
end
|
57
|
+
result
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/spec/mocks/game.rb
ADDED
data/spec/mocks/rules.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'surrogate/rspec'
|
2
|
+
|
3
|
+
class MockConsole
|
4
|
+
|
5
|
+
Surrogate.endow(self)
|
6
|
+
|
7
|
+
define :display_welcome_message
|
8
|
+
define(:display_board) {|board|}
|
9
|
+
define(:display) {|message|}
|
10
|
+
define :game_type
|
11
|
+
define(:display_winner){|winner|}
|
12
|
+
define :display_tied_game
|
13
|
+
define :display_square_not_available
|
14
|
+
define(:display_player_turn) {|player|}
|
15
|
+
end
|