Here's my first shot at implementing Tic Tac Toe in Ruby. After watching Gary Bernhardt's Functional Core, Imperative Shell, I thought this would be a great exercise in trying out the ideology presented in that screencast. Tear my code apart.
Link to GitHub repo (includes video of usage)
tic_tac_toe.rb -- the procedural shell
require_relative "board"
require_relative "cell"
require "pry"
def initialize_board
@board = Board.new
end
def choose_characters
puts "X or O?"
@player_character = gets.chomp
@computer_character = case @player_character
when "X" then "O"
when "O" then "X"
end
end
def start_game
initialize_board
choose_characters
play
end
def play
loop do
puts "Make a move: "
move = gets.chomp
board_index = Board::POSITIONS[move]
winner = @player_character
@board.board[board_index].value = @player_character
if [email protected]?
@board.available_cells.sample.value = @computer_character
winner = @computer_character if @board.winner?
end
if @board.winner?
p "#{winner.upcase} WINS!!!"
@board.display
puts "Do you want to play again?"
start_game if gets.chomp == "Yes"
else
@board.display
play
end
break if @board.winner?
end
end
start_game
board.rb
class Board
attr_accessor :board
WINNING_COMBINATIONS = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
]
POSITIONS = {
"top left" => 0,
"top middle" => 1,
"top right" => 2,
"middle left" => 3,
"center" => 4,
"middle right" => 5,
"bottom left" => 6,
"bottom middle" => 7,
"bottom right" => 8
}
def initialize
@board = Array.new(9) { Cell.new }
end
def winner?
winner = false
WINNING_COMBINATIONS.each do |combination|
first_cell = board[combination[0]].value
second_cell = board[combination[1]].value
third_cell = board[combination[2]].value
consideration = [first_cell, second_cell, third_cell]
if consideration.uniq.length == 1 && first_cell.is_a?(String)
winner = true
end
end
winner
end
def available_cells
board.select { |cell| cell.value == :blank }
end
def display
rows = board.each_slice(3).to_a
rows.each do |row|
p row.map(&:value)
end
end
end
cell.rb
class Cell
attr_accessor :value
def initialize(options = {})
@value = options.fetch(:value, :blank)
end
end
-
\$\begingroup\$ Does this code live in a repository somewhere that is easy to grab and work with? \$\endgroup\$vgoff– vgoff2015年04月30日 04:43:57 +00:00Commented Apr 30, 2015 at 4:43
-
\$\begingroup\$ Yes! It's here: github.com/dylanerichards/tic-tac-toe. There's even a video of me demonstrating use of the application. I'll update the original post. \$\endgroup\$Dylan Richards– Dylan Richards2015年04月30日 05:14:23 +00:00Commented Apr 30, 2015 at 5:14
-
\$\begingroup\$ Now that is some very useful information to have right there. :) You even have specs... Was having some difficulties running it without referring to the code to know what the expected inputs were. \$\endgroup\$vgoff– vgoff2015年04月30日 05:20:47 +00:00Commented Apr 30, 2015 at 5:20
-
\$\begingroup\$ The fact that you had to explain the moves in your video by stating verbally what the list of valid locations are, means that there is not enough of a prompt, there should be something indicating move options, just like (and probably less reason to be) your prompt showing the very well know and expected "X" and "O" options for player. \$\endgroup\$vgoff– vgoff2015年04月30日 06:15:49 +00:00Commented Apr 30, 2015 at 6:15
-
\$\begingroup\$ "9 openings (8 indexes)" 0 is an index as well. You have 9 indices, (or the other valid English plural, 'indexes') to be sure. Otherwise you are missing a valid placement on a tic-tac-toe board. \$\endgroup\$vgoff– vgoff2015年04月30日 06:54:38 +00:00Commented Apr 30, 2015 at 6:54
1 Answer 1
You require pry, and you don't seem to use it. Perhaps it is being used for troubleshooting, and the binding.pry
or other code, has been removed. In that case it may be more appropriate to simply use the -rpry
option as in ruby -rpry file.rb
. Then you don't have to remember to remove that line, just the troubleshooting line. (Or even better, set up a system that lets you leave things in but turn them off and on from an option in the system call. Perhaps -w or -v flags. Perhaps an environment variable. A line in with this in mind may look like this: binding.pry if $VERBOSE
And a require 'pry' if $VERBOSE
)
Looking at your loop do
tells me that you have an infinite loop. But you have a well defined exit strategy, there is no need for an infinite loop. Using something like until @board.winner?
will run the block at least once, and will stop when your method returns true/truthy value. You also call 'play' from inside the loop inside of play, but it is already going to start from the beginning of the loop.
If you had folders bin/
, lib/
, I would expect some of the code in your tic-tac-toe.rb
file to be in maybe bin/tic-tac-toe
. The start
equivalent for sure. This will allow you to use the lib/tic-tac-toe.rb
file in a way that someone that requires that library would expect. Right now, you are unable to require that library file without starting the game.
The interface of using words like top left
seems clumsy and error prone, even if it is nice. Creating a numbered or lettered menu or presenting the grid with numbers or letters could make play easier, less typing to do, less error prone. Perhaps a lettered 'x, y' grid. Presenting that grid would also document the game play, and make initial play more straight forward.
Your "choose_character" method does two things, it prompts and processes the choice. The @player_character and @computer_character is repetitive, confusing in a way. Simply having @player and @computer is probably clear enough. @player.character might be better, though I think we consider that letter a 'marker', as the player mark may be 'X' and the AI mark may be 'O'.
Looking at cell.rb
you could be using the new named arguments in Ruby 2.0.
In the Board file you can likely use the all?
Enumerable method, if an array has all?
the markers that match the current players marker, then that player is the winner.