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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.lock +39 -0
  6. data/README.md +19 -0
  7. data/Rakefile +15 -0
  8. data/bin/tic_tac_toe +6 -0
  9. data/coverage/.last_run.json +5 -0
  10. data/coverage/.resultset.json +502 -0
  11. data/features/step_definitions/tictactoe_steps.rb +6 -0
  12. data/features/support/env.rb +2 -0
  13. data/features/tictactoe_start_game.feature +11 -0
  14. data/features/tictactoe_submit_move.feature +9 -0
  15. data/lib/tic_tac_toe/board.rb +99 -0
  16. data/lib/tic_tac_toe/game.rb +41 -0
  17. data/lib/tic_tac_toe/game_factory.rb +55 -0
  18. data/lib/tic_tac_toe/main.rb +47 -0
  19. data/lib/tic_tac_toe/player.rb +15 -0
  20. data/lib/tic_tac_toe/player_factory.rb +17 -0
  21. data/lib/tic_tac_toe/rules.rb +32 -0
  22. data/lib/tic_tac_toe/strategy/console_user.rb +26 -0
  23. data/lib/tic_tac_toe/strategy/minimax.rb +83 -0
  24. data/lib/tic_tac_toe/ui/console.rb +60 -0
  25. data/spec/integration/tictactoe/tic_tac_toe.rb +7 -0
  26. data/spec/mocks/game.rb +11 -0
  27. data/spec/mocks/game_factory.rb +8 -0
  28. data/spec/mocks/player_factory.rb +8 -0
  29. data/spec/mocks/rules.rb +9 -0
  30. data/spec/mocks/strategy/dynamic.rb +19 -0
  31. data/spec/mocks/ui/console.rb +15 -0
  32. data/spec/tic_tac_toe/board_spec.rb +186 -0
  33. data/spec/tic_tac_toe/game_factory_spec.rb +78 -0
  34. data/spec/tic_tac_toe/game_spec.rb +94 -0
  35. data/spec/tic_tac_toe/main_spec.rb +104 -0
  36. data/spec/tic_tac_toe/player_factory_mock.rb +10 -0
  37. data/spec/tic_tac_toe/player_factory_spec.rb +59 -0
  38. data/spec/tic_tac_toe/player_spec.rb +32 -0
  39. data/spec/tic_tac_toe/rules_spec.rb +185 -0
  40. data/spec/tic_tac_toe/spec_helper.rb +9 -0
  41. data/spec/tic_tac_toe/strategy/console_user_spec.rb +34 -0
  42. data/spec/tic_tac_toe/strategy/minimax_spec.rb +163 -0
  43. data/spec/tic_tac_toe/strategy/mock.rb +19 -0
  44. data/spec/tic_tac_toe/ui/console_spec.rb +93 -0
  45. data/tic_tac_toe_nhu.gemspec +11 -0
  46. 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,15 @@
1
+ module TicTacToe
2
+ class Player
3
+ attr_reader :name, :value, :strategy
4
+
5
+ def initialize(name, value, strategy)
6
+ @name = name
7
+ @value = value
8
+ @strategy = strategy
9
+ end
10
+
11
+ def move
12
+ @strategy.move
13
+ end
14
+ end
15
+ 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
@@ -0,0 +1,7 @@
1
+ $LOAD_PATH << File.expand_path('../../lib', __FILE__)
2
+ require 'tic_tac_toe'
3
+ require 'tic_tac_toe/mock/player'
4
+
5
+ player = MockPlayer.new([8, 6, 3])
6
+ game = TicTacToe::GameFactory.computer_user_game
7
+ game.start
@@ -0,0 +1,11 @@
1
+ require 'surrogate/rspec'
2
+
3
+ class MockGame
4
+ Surrogate.endow(self)
5
+ define_reader :board
6
+ define_reader :current_player
7
+
8
+ define :make_move
9
+ define :winner
10
+ define(:over?){true}
11
+ end
@@ -0,0 +1,8 @@
1
+ require 'surrogate/rspec'
2
+
3
+ class MockGameFactory
4
+ Surrogate.endow(self)
5
+ define(:initialize) {|player_factory|}
6
+ define :types
7
+ define(:create) {|type, board|}
8
+ end
@@ -0,0 +1,8 @@
1
+ require 'surrogate/rspec'
2
+
3
+ class MockPlayerFactory
4
+ Surrogate.endow(self)
5
+
6
+ define(:human) {|name = "Sue", value = "X"|}
7
+ define(:computer) {|board, value = "X", opponent="O"|}
8
+ end
@@ -0,0 +1,9 @@
1
+ require 'surrogate/rspec'
2
+
3
+ class MockRules
4
+ Surrogate.endow(self)
5
+ define(:initialize) {|board|}
6
+ define(:game_over?) {true}
7
+ define(:tied?)
8
+ define(:winner)
9
+ end
@@ -0,0 +1,19 @@
1
+ class MockDynamicStrategy
2
+ attr_accessor :moves
3
+
4
+ def initialize(moves = [])
5
+ @moves = moves
6
+ end
7
+
8
+ def add_move(move)
9
+ @moves << move
10
+ end
11
+
12
+ def move
13
+ @moves.shift
14
+ end
15
+
16
+ def needs_input?
17
+ true
18
+ end
19
+ end
@@ -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