diff options
| -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' | 
