diff options
| author | Teddy Wing | 2015-04-16 02:13:04 -0400 |
|---|---|---|
| committer | Teddy Wing | 2015-04-16 02:13:04 -0400 |
| commit | 6984fc31370564332ceab60c203cd6f1e695d61a (patch) | |
| tree | a53d6b3ecf3c1ba4f35fcd245c30757e1a266e11 | |
| parent | fa951e520b7637d9e67c31a0a8b61733e253f90b (diff) | |
| parent | 93351518faf3bb4f63b4a3cc1ef28f5c06e09f01 (diff) | |
| download | tic-tac-toe-master.tar.bz2 | |
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | LICENSE | 21 | ||||
| -rw-r--r-- | README.md | 69 | ||||
| -rw-r--r-- | Rakefile | 4 | ||||
| -rw-r--r-- | board.rb | 75 | ||||
| -rw-r--r-- | main.rb | 35 | ||||
| -rw-r--r-- | player.rb | 19 | ||||
| -rw-r--r-- | spec/board_spec.rb | 147 | ||||
| -rw-r--r-- | spec/player_spec.rb | 47 | ||||
| -rw-r--r-- | spec/spec_helper.rb | 2 | ||||
| -rw-r--r-- | test/board_test.rb | 15 | ||||
| -rw-r--r-- | test/sample_test.rb | 8 | ||||
| -rw-r--r-- | test/test_helper.rb | 3 |
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/ @@ -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. @@ -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 @@ -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' |
