aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTeddy Wing2015-04-16 02:13:04 -0400
committerTeddy Wing2015-04-16 02:13:04 -0400
commit6984fc31370564332ceab60c203cd6f1e695d61a (patch)
treea53d6b3ecf3c1ba4f35fcd245c30757e1a266e11
parentfa951e520b7637d9e67c31a0a8b61733e253f90b (diff)
parent93351518faf3bb4f63b4a3cc1ef28f5c06e09f01 (diff)
downloadtic-tac-toe-master.tar.bz2
Merge branch 'rspec-style'HEADmaster
-rw-r--r--.gitignore1
-rw-r--r--LICENSE21
-rw-r--r--README.md69
-rw-r--r--Rakefile4
-rw-r--r--board.rb75
-rw-r--r--main.rb35
-rw-r--r--player.rb19
-rw-r--r--spec/board_spec.rb147
-rw-r--r--spec/player_spec.rb47
-rw-r--r--spec/spec_helper.rb2
-rw-r--r--test/board_test.rb15
-rw-r--r--test/sample_test.rb8
-rw-r--r--test/test_helper.rb3
13 files changed, 444 insertions, 2 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1f6786d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+private/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e62f2ad
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Teddy Wing
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..111cdc2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,69 @@
+tic-tac-toe
+===========
+
+A simple incomplete 2-player tic-tac-toe game that runs in the console.
+
+Written originally as a possible vehicle or exercise for teaching test-driven
+development.
+
+
+## Requirements
+Developed with Ruby 2.1.2. Appears to work on Ruby 1.9.3.
+
+
+## Running
+ $ ruby main.rb
+
+
+## Example
+ $ ruby main.rb
+ ...
+ ...
+ ...
+
+ Player X move - Enter coordinates (e.g. 0,2): 1,2
+ ----
+ ...
+ ..X
+ ...
+
+ Player O move - Enter coordinates (e.g. 0,2): 0,0
+ ----
+ O..
+ ..X
+ ...
+
+ Player X move - Enter coordinates (e.g. 0,2): 0,1
+ ----
+ OX.
+ ..X
+ ...
+
+ Player O move - Enter coordinates (e.g. 0,2): 1,0
+ ----
+ OX.
+ O.X
+ ...
+
+ Player X move - Enter coordinates (e.g. 0,2): 2,1
+ ----
+ OX.
+ O.X
+ .X.
+
+ Player O move - Enter coordinates (e.g. 0,2): 2,0
+ ----
+ OX.
+ O.X
+ OX.
+ Player O wins
+
+
+## Known Issues
+* Entering invalid coordinates (whether a coordinate outside the board or a
+ value that isn't a coordinate) causes an exception and exits immediately.
+* Game does not check for a tie
+
+
+## License
+Licensed under the MIT License. See the included LICENSE file.
diff --git a/Rakefile b/Rakefile
index b642467..8cc2e02 100644
--- a/Rakefile
+++ b/Rakefile
@@ -2,7 +2,9 @@ require 'rake'
require 'rake/testtask'
Rake::TestTask.new do |t|
- t.pattern = 'test/**/*_test.rb'
+ t.libs << '.'
+ t.libs << 'spec'
+ t.pattern = 'spec/**/*_spec.rb'
end
task :default => :test
diff --git a/board.rb b/board.rb
new file mode 100644
index 0000000..4bef0d3
--- /dev/null
+++ b/board.rb
@@ -0,0 +1,75 @@
+class Board
+ attr_accessor :current_player
+
+ def initialize
+ @board = [
+ ['.', '.', '.'],
+ ['.', '.', '.'],
+ ['.', '.', '.']
+ ]
+ end
+
+ def render
+ output = ''
+ @board.each {|row| output << row.join + "\n" }
+ output
+ end
+
+ # Raises an ArgumentError if integer conversion fails
+ def transform_coordinates(str)
+ coordinates = str.split(',')
+
+ begin
+ coordinates[0] = Integer(coordinates[0])
+ coordinates[1] = Integer(coordinates[1])
+
+ coordinates if coordinates.length > 1
+ rescue
+ end
+ end
+
+ def update_cell(row_index, column_index, value)
+ @board[row_index][column_index] = value
+ end
+
+ def winner?
+ initial = '.'
+
+ # Check horizontal
+ @board.each do |row|
+ return row[0] if array_items_equal(row) and row[0] != initial
+ end
+
+ # Check vertical
+ @board.transpose.each do |column|
+ return column[0] if array_items_equal(column) and column[0] != initial
+ end
+
+ # Check diagonals
+ descending = [
+ @board[0][0],
+ @board[1][1],
+ @board[2][2]
+ ]
+ if array_items_equal(descending) and descending[0] != initial
+ return descending[0]
+ end
+
+ ascending = [
+ @board[2][0],
+ @board[1][1],
+ @board[0][2]
+ ]
+ if array_items_equal(ascending) and ascending[0] != initial
+ return ascending[0]
+ end
+
+ nil
+ end
+
+ private
+
+ def array_items_equal(arr)
+ arr.uniq.length == 1
+ end
+end
diff --git a/main.rb b/main.rb
new file mode 100644
index 0000000..835d6c5
--- /dev/null
+++ b/main.rb
@@ -0,0 +1,35 @@
+require_relative 'board'
+require_relative 'player'
+
+board = Board.new
+player_1 = Player.new(Player::INSIGNIAS[:x], board)
+player_2 = Player.new(Player::INSIGNIAS[:o], board)
+board.current_player = player_1
+winner = nil
+
+begin
+ until winner
+ puts board.render
+ puts
+
+ print "Player #{board.current_player.insignia} move - " \
+ "Enter coordinates (e.g. 0,2): "
+
+ coordinates = gets.chomp
+ coordinates = board.transform_coordinates(coordinates)
+
+ board.current_player.move(coordinates)
+
+ board.current_player = board.current_player == player_1 ? player_2 : player_1
+
+ puts '----'
+
+ winner = board.winner?
+ if winner
+ puts board.render
+ puts "Player #{winner} wins"
+ end
+ end
+rescue Interrupt
+ puts
+end
diff --git a/player.rb b/player.rb
new file mode 100644
index 0000000..07301f8
--- /dev/null
+++ b/player.rb
@@ -0,0 +1,19 @@
+class Player
+ INSIGNIAS = {
+ :x => 'X',
+ :o => 'O'
+ }
+
+ attr_reader :insignia
+
+ def initialize(insignia, board)
+ @insignia = insignia
+ @board = board
+ end
+
+ def move(coordinates)
+ raise ArgumentError if coordinates.nil?
+
+ @board.update_cell(coordinates[0], coordinates[1], @insignia)
+ end
+end
diff --git a/spec/board_spec.rb b/spec/board_spec.rb
new file mode 100644
index 0000000..70caa81
--- /dev/null
+++ b/spec/board_spec.rb
@@ -0,0 +1,147 @@
+require 'spec_helper'
+require 'board'
+
+describe Board do
+ before do
+ @board = Board.new
+ end
+
+ it 'starts with a grid of dots' do
+ @board.instance_variable_get('@board').must_equal [
+ ['.', '.', '.'],
+ ['.', '.', '.'],
+ ['.', '.', '.']
+ ]
+ end
+
+ describe '#render' do
+ it 'must be a grid' do
+ @board.render.must_equal <<EOF
+...
+...
+...
+EOF
+ end
+
+ it 'must be the correct board' do
+ @board.instance_variable_set(:@board, [
+ ['.', 'X', 'O'],
+ ['X', '.', '.'],
+ ['.', '.', '.'],
+ ])
+ @board.render.must_equal <<EOF
+.XO
+X..
+...
+EOF
+ end
+ end
+
+ describe '#transform_coordinates' do
+ it 'converts string coordinates to an array' do
+ @board.transform_coordinates('0,4').must_equal [0, 4]
+ end
+
+ it "returns nil if coordinates don't match the format" do
+ @board.transform_coordinates('4').must_be_nil
+ @board.transform_coordinates('4 2').must_be_nil
+ @board.transform_coordinates('booyakacha').must_be_nil
+ @board.transform_coordinates('booya,kacha').must_be_nil
+ end
+ end
+
+ describe '#update_cell' do
+ it 'updates a given cell with a given value' do
+ value = 'X'
+ @board.update_cell(1, 2, value)
+ @board.instance_variable_get(:@board)[1][2].must_equal value
+ end
+ end
+
+ describe '#winner?' do
+ before do
+ @board = Board.new
+ end
+
+ it 'must be nil when no player has won' do
+ @board.winner?.must_be_nil
+ end
+
+ it 'must be the winning player' do
+ end
+
+ it 'counts horizontal matches as wins' do
+ @board.instance_variable_set(:@board, [
+ ['X', 'X', 'X'],
+ ['X', 'O', 'O'],
+ ['O', 'X', 'O'],
+ ])
+ @board.winner?.must_equal 'X'
+
+ @board.instance_variable_set(:@board, [
+ ['X', 'O', 'X'],
+ ['O', 'O', 'O'],
+ ['O', 'X', 'X'],
+ ])
+ @board.winner?.must_equal 'O'
+
+ @board.instance_variable_set(:@board, [
+ ['O', 'X', 'X'],
+ ['X', 'O', 'X'],
+ ['O', 'O', 'O'],
+ ])
+ @board.winner?.must_equal 'O'
+ end
+
+ it 'counts vertical matches as wins' do
+ @board.instance_variable_set(:@board, [
+ ['X', 'O', 'X'],
+ ['X', 'O', 'O'],
+ ['X', 'X', 'O'],
+ ])
+ @board.winner?.must_equal 'X'
+
+ @board.instance_variable_set(:@board, [
+ ['X', 'O', 'X'],
+ ['X', 'O', 'O'],
+ ['O', 'O', 'X'],
+ ])
+ @board.winner?.must_equal 'O'
+
+ @board.instance_variable_set(:@board, [
+ ['O', 'X', 'X'],
+ ['X', 'O', 'X'],
+ ['O', 'O', 'X'],
+ ])
+ @board.winner?.must_equal 'X'
+ end
+
+ it 'counts diagonal matches as wins' do
+ @board.instance_variable_set(:@board, [
+ ['O', 'X', 'X'],
+ ['X', 'O', 'O'],
+ ['O', 'X', 'O'],
+ ])
+ @board.winner?.must_equal 'O'
+
+ @board.instance_variable_set(:@board, [
+ ['X', 'O', 'X'],
+ ['O', 'X', 'O'],
+ ['O', 'X', 'X'],
+ ])
+ @board.winner?.must_equal 'X'
+ end
+ end
+
+ describe '#array_items_equal' do
+ it 'is true when all elements in the array are equal' do
+ Board.new.send(:array_items_equal,
+ ['X', 'X', 'X', 'X', 'X', 'X', 'X']).must_equal true
+ end
+
+ it 'is false when any element is not equal to the rest' do
+ Board.new.send(:array_items_equal,
+ ['X', 'O', 'X', 'X', 'X', 'X', 'X']).must_equal false
+ end
+ end
+end
diff --git a/spec/player_spec.rb b/spec/player_spec.rb
new file mode 100644
index 0000000..11bfd35
--- /dev/null
+++ b/spec/player_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+require 'board'
+require 'player'
+
+describe Player do
+ describe 'insignias' do
+ it 'knows what an X insignia is' do
+ Player::INSIGNIAS[:x].must_equal 'X'
+ end
+
+ it 'knows what an O insignia is' do
+ Player::INSIGNIAS[:o].must_equal 'O'
+ end
+ end
+
+ describe '#insignia' do
+ it 'must be the correct insignia' do
+ insignia = Player::INSIGNIAS[:o]
+ player = Player.new(insignia, Board.new)
+ player.insignia.must_equal insignia
+ end
+ end
+
+ describe '#move' do
+ before do
+ @board = Board.new
+ @player = Player.new(Player::INSIGNIAS[:x], @board)
+ end
+
+ it 'raises an ArgumentError given nil coordinates' do
+ -> { @player.move(nil) }.must_raise ArgumentError
+ end
+
+ it 'adds a piece to the correct coordinates on `board`' do
+ @player.move([1, 2])
+ @board.instance_variable_get(:@board)[1][2].must_equal \
+ @player.instance_variable_get(:@insignia)
+ end
+
+ it 'uses the correct insignia for the move' do
+ insignia = Player::INSIGNIAS[:o]
+ player = Player.new(insignia, @board)
+ player.move([0, 1])
+ @board.instance_variable_get(:@board)[0][1].must_equal insignia
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..1655612
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,2 @@
+require 'minitest/spec'
+require 'minitest/autorun'
diff --git a/test/board_test.rb b/test/board_test.rb
new file mode 100644
index 0000000..8aeaeab
--- /dev/null
+++ b/test/board_test.rb
@@ -0,0 +1,15 @@
+require 'test_helper'
+
+class TestBoard < Minitest::Unit::TestCase
+ def setup
+ @board = Board.new
+ end
+
+ def test_render
+ assert_equal @board.render, <<EOS
+...
+...
+...
+EOS
+ end
+end
diff --git a/test/sample_test.rb b/test/sample_test.rb
index 88fbd6c..9948773 100644
--- a/test/sample_test.rb
+++ b/test/sample_test.rb
@@ -1,4 +1,4 @@
-require 'minitest/autorun'
+require 'test_helper'
class TestSample < Minitest::Unit::TestCase
def setup
@@ -8,3 +8,9 @@ class TestSample < Minitest::Unit::TestCase
assert_equal 1 + 1, 2
end
end
+
+class TestBoard < Minitest::Unit::TestCase
+ def test_board
+ Board.new
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 0000000..361f8b2
--- /dev/null
+++ b/test/test_helper.rb
@@ -0,0 +1,3 @@
+require 'minitest/autorun'
+
+require 'board'