erics_tic_tac_toe 0.1.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  pkg/
2
+ coverage/
data/README.markdown CHANGED
@@ -4,12 +4,11 @@ Tic Tac Toe
4
4
  How to play
5
5
  ----
6
6
 
7
- irb -Ilib
8
- require 'game'
9
- Game.new.run
7
+ gem install erics_tic_tac_toe
8
+ tic_tac_toe
10
9
 
11
- Whats coming
12
- ----
10
+ TODO
11
+ ---
13
12
 
14
- Gem and Bin executable for playing games easily
15
- Play games on boards bigger than 3x3
13
+ * Use Curses for a nicer playing experience
14
+ * Colorize diffs so that its easier to see what move was just played
data/Rakefile CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'rake'
2
+ include Rake::DSL
2
3
  require 'bundler'
3
4
  require 'rake/testtask'
4
5
 
@@ -6,7 +7,7 @@ Bundler::GemHelper.install_tasks
6
7
 
7
8
  Rake::TestTask.new do |t|
8
9
  t.libs << 'test'
9
- t.pattern = 'test/**/*_test.rb'
10
+ t.pattern = '**/*_test.rb'
10
11
  end
11
12
 
12
13
  task :default => :test
data/bin/tic_tac_toe CHANGED
@@ -6,7 +6,7 @@ begin
6
6
  rescue LoadError
7
7
  case $!.to_s
8
8
  when /tic_tac_toe/
9
- if !$:.include?(libdir)
9
+ if !$:.include?(libdir)
10
10
  warn "warn: #$!. trying again with #{libdir} on load path"
11
11
  $:.unshift(libdir)
12
12
  retry
@@ -15,4 +15,43 @@ rescue LoadError
15
15
  raise
16
16
  end
17
17
 
18
- TicTacToe::Game.new.run
18
+ def setup
19
+ game = TicTacToe::Game.new
20
+ terminal = TicTacToe::TerminalGame.new(game.board)
21
+
22
+ if terminal.computer_goes_first?
23
+ @player_1 = TicTacToe::ComputerPlayer.new(letter: 'o')
24
+ @player_2 = TicTacToe::HumanPlayer.new(letter: 'x')
25
+ else
26
+ @player_1 = TicTacToe::HumanPlayer.new(letter: 'o')
27
+ @player_2 = TicTacToe::HumanPlayer.new(letter: 'x')
28
+ end
29
+
30
+ game = TicTacToe::Game.new(nil, @player_1, @player_2)
31
+
32
+ [game, terminal]
33
+ end
34
+
35
+ game, terminal = setup
36
+
37
+ loop do
38
+ game.start
39
+ terminal = TicTacToe::TerminalGame.new(game.board)
40
+
41
+ if game.solved? || game.cats?
42
+ terminal.update_board
43
+ break unless terminal.play_again?
44
+ game, terminal = setup
45
+ end
46
+
47
+ terminal.update_board
48
+
49
+ move = terminal.get_move_from_user
50
+ @player_1 = TicTacToe::HumanPlayer.new(letter: @player_1.letter, move: move)
51
+ @player_2 = TicTacToe::ComputerPlayer.new(letter: @player_2.letter)
52
+
53
+ grid = game.grid
54
+ game = TicTacToe::Game.new(grid, @player_1, @player_2)
55
+
56
+ end
57
+
data/lib/tic_tac_toe.rb CHANGED
@@ -7,7 +7,8 @@ module TicTacToe
7
7
  end
8
8
 
9
9
  # Internal Project Requires
10
- require 'tic-tac-toe/board'
11
- require 'tic-tac-toe/solver'
12
- require 'tic-tac-toe/game'
13
-
10
+ require 'tic_tac_toe/board'
11
+ require 'tic_tac_toe/game'
12
+ require 'tic_tac_toe/player'
13
+ require 'tic_tac_toe/presentors/game_presenter'
14
+ require 'tic_tac_toe/presentors/player_presenter'
@@ -0,0 +1,153 @@
1
+ module TicTacToe
2
+ # The main class for managing the state of the game board
3
+ # The board data is represented at a two dimensional array
4
+ # It provides helper methods for access the data
5
+ class Board
6
+
7
+ attr_accessor :grid
8
+
9
+ def initialize(size=3)
10
+ #[[ nil, nil, nil],
11
+ # [ nil, nil, nil],
12
+ # [ nil, nil, nil]]
13
+ @grid = Array.new(size) { Array.new(size) { nil } }
14
+ end
15
+
16
+ def get_cell(row, column)
17
+ @grid[column][row]
18
+ end
19
+
20
+ # Returns the corners of the empty cells
21
+ def empty_positions(&block)
22
+ positions = []
23
+ each_position do |row, column|
24
+ next if get_cell(row, column)
25
+ yield(row, column) if block_given?
26
+ positions << [row, column]
27
+ end
28
+ positions
29
+ end
30
+
31
+ # Plays a letter at a position, unless that position has already been taken
32
+ def play_at(row, column, letter)
33
+ @grid[column][row] ||= letter
34
+ self
35
+ end
36
+
37
+ # Returns true if the grid is empty
38
+ def empty?
39
+ each_cell do |cell|
40
+ return false if cell
41
+ end
42
+ true
43
+ end
44
+
45
+ # Returns true if the grid only has one element
46
+ def only_one?
47
+ counter = 0
48
+ each_cell do |cell|
49
+ counter += 1 if cell
50
+ return false if counter >= 2
51
+ end
52
+ counter == 1
53
+ end
54
+
55
+ # Returns true if every cell is set to a value
56
+ def full?
57
+ each_cell do |cell|
58
+ return false unless cell
59
+ end
60
+ true
61
+ end
62
+
63
+ # Returns true if the board has a wining pattern
64
+ def solved?
65
+ letter = won_across?
66
+ return letter if letter
67
+ letter = won_up_and_down?
68
+ return letter if letter
69
+ letter = won_diagonally?
70
+ return letter if letter
71
+ false
72
+ end
73
+
74
+ def winner
75
+ solved?
76
+ end
77
+
78
+ def to_s
79
+ output = ["-----------\n"]
80
+ @grid.each_with_index do |row, i|
81
+ row.each_with_index do |cell, j|
82
+ output << "#{cell || ((i*@grid.size)+j+1)} | #{"\n" if j==@grid.size-1}"
83
+ end
84
+ end
85
+ output << ["-----------\n\n"]
86
+ output.join
87
+ end
88
+
89
+ # Preform a deep clone of the board
90
+ def clone
91
+ board = Board.new
92
+ board.grid = @grid.map { |row| row.map { |cell| cell } }
93
+ board
94
+ end
95
+
96
+ def size
97
+ @grid.size
98
+ end
99
+
100
+ private
101
+
102
+ def each_cell(&block)
103
+ @grid.each do |row|
104
+ row.each do |cell|
105
+ yield(cell)
106
+ end
107
+ end
108
+ end
109
+
110
+ def each_position(&block)
111
+ @grid.each_with_index do |row, y|
112
+ row.each_with_index do |cell, x|
113
+ yield(y, x)
114
+ end
115
+ end
116
+ end
117
+
118
+ def won_across?(grid = @grid)
119
+ winning_letter = false
120
+ grid.each do |row|
121
+ winning_letter = row[0] if winning_group?(row)
122
+ end
123
+ winning_letter
124
+ end
125
+
126
+ def won_up_and_down?
127
+ won_across?(@grid.transpose)
128
+ end
129
+
130
+ def won_diagonally?
131
+ right_limit = @grid.size-1
132
+ base = (0..right_limit)
133
+
134
+ letter = create_and_check_group(base) { |i| @grid[i][i] }
135
+ return letter if letter
136
+ letter = create_and_check_group(base) { |i| @grid[i][right_limit-i] }
137
+ return letter if letter
138
+
139
+ false
140
+ end
141
+
142
+ def create_and_check_group(base, &block)
143
+ group = base.collect { |i| yield(i) }
144
+ return group[0] if winning_group?(group)
145
+ false
146
+ end
147
+
148
+ def winning_group?(group)
149
+ !group.any? { |cell| cell.nil? } && group.uniq.size == 1
150
+ end
151
+
152
+ end
153
+ end
@@ -0,0 +1,70 @@
1
+ #This is a computer that will play a perfect game of tic-tac-toe
2
+ #Author: Eric Koslow
3
+
4
+ require_relative 'game_types/terminal_game'
5
+ require_relative 'strategies/three_by_three_strategy'
6
+ require_relative 'strategies/minimax_strategy'
7
+ require_relative 'players/human_player'
8
+ require_relative 'players/computer_player'
9
+
10
+ module TicTacToe
11
+ # The main director of the program
12
+ # Directs its gametype when to retrieve information from the user
13
+ class Game
14
+ attr_reader :board
15
+
16
+ attr_reader :player_moves
17
+
18
+ def initialize(board=nil, player_1=nil, player_2=nil)
19
+ @board = Board.new
20
+ @board.grid = board || [[nil,nil,nil],[nil,nil,nil],[nil,nil,nil]]
21
+ @player_1 = player_1
22
+ @player_2 = player_2
23
+ @current_player = @player_1 && @player_1.has_next_move? ? @player_1 : @player_2
24
+ end
25
+
26
+ def start
27
+ while @current_player && (move = @current_player.get_move(@board))
28
+ move = number_to_cords(move) unless move.is_a?(Array)
29
+
30
+ @board.play_at(*move, @current_player.letter)
31
+ break if over?
32
+
33
+ switch_player
34
+ end
35
+ end
36
+
37
+ def grid
38
+ @board.grid
39
+ end
40
+
41
+ def solved?
42
+ @board.solved?
43
+ end
44
+
45
+ def cats?
46
+ @board.full? && !solved?
47
+ end
48
+
49
+ def winner
50
+ @board.winner
51
+ end
52
+
53
+ private
54
+
55
+ def over?
56
+ solved? || cats?
57
+ end
58
+
59
+ def number_to_cords(num)
60
+ num = num.to_i
61
+ num -= 1
62
+ [num % @board.size, num/@board.size]
63
+ end
64
+
65
+ def switch_player
66
+ @current_player = @current_player == @player_1 ? @player_2 : @player_1
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,86 @@
1
+ module TicTacToe
2
+ # Terminal GameType
3
+ # Interacts with the user through the terminal
4
+ # Uses puts for output, and gets for input
5
+ class TerminalGame
6
+ # Internal Error used when a user tries pulling an illegal move
7
+ class IllegalMove < RuntimeError
8
+ end
9
+
10
+ def initialize(board, io=Kernel)
11
+ @board = board
12
+ @io = io
13
+ end
14
+
15
+ def computer_goes_first?
16
+ input = get_input("Would you like to play first or second? (f/s)")
17
+
18
+ return true unless input =~ /^(f|first)$/i
19
+ false
20
+ end
21
+
22
+ def get_move_from_user
23
+ cords = get_cords_from_user
24
+
25
+ raise IllegalMove.new("That cell is already taken") unless empty_cell?(cords)
26
+
27
+ cords
28
+ rescue IllegalMove => error
29
+ display_text "Illegal Move: #{error.message}. Please try again"
30
+ retry
31
+ end
32
+
33
+ def play_again?
34
+ input = get_input "Play again? (y/n)"
35
+ input =~ /^(y|yes)$/i
36
+ end
37
+
38
+ def select(choices)
39
+ loop do
40
+ output = "Select the solver:\n"
41
+ choices.each_with_index { |choice, i| output += "#{i+1}: #{choice}\n" }
42
+
43
+ input = get_input(output)
44
+
45
+ if input =~ /^\d+$/ and input.to_i <= choices.size
46
+ return choices[input.to_i-1]
47
+ end
48
+
49
+ display_text("Not a valid choice")
50
+ end
51
+ end
52
+
53
+ def update_board
54
+ @io.puts @board
55
+ end
56
+
57
+ def display_text(text)
58
+ @io.puts text
59
+ end
60
+
61
+ private
62
+
63
+ def get_input(text)
64
+ @io.print text + ' '
65
+ @io.gets.chomp
66
+ end
67
+
68
+ def empty_cell?(cords)
69
+ !@board.get_cell(*cords)
70
+ end
71
+
72
+ def get_cords_from_user
73
+ input = get_input("Your move (1-#{@board.size**2}):")
74
+
75
+ raise IllegalMove.new("That is not a real location") unless input =~ /^\d+$/
76
+
77
+ cord_from_num(input.to_i)
78
+ end
79
+
80
+ def cord_from_num(num)
81
+ num -= 1
82
+ [num % @board.size, num/@board.size]
83
+ end
84
+ end
85
+
86
+ end
@@ -0,0 +1,13 @@
1
+ module TicTacToe
2
+ class Player
3
+ HUMAN = 'human'
4
+ COMPUTER = 'computer'
5
+
6
+ def self.build(params)
7
+ return HumanPlayer.new(params) if params['type'] == HUMAN
8
+
9
+ ComputerPlayer.new(params)
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ module TicTacToe
2
+
3
+ class ComputerPlayer
4
+ attr_reader :letter
5
+
6
+ def initialize(params, solver=MinimaxStrategy)
7
+ @letter = params['letter'] || params[:letter]
8
+ @solver = solver
9
+ end
10
+
11
+ def get_move(board)
12
+ @solver.new(board, @letter).solve
13
+ end
14
+
15
+ def has_next_move?
16
+ true
17
+ end
18
+
19
+ def type
20
+ Player::COMPUTER
21
+ end
22
+
23
+ end
24
+
25
+
26
+ end