snakes_and_ladders 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d0f2c16f3b1a14cc698be0bba9d33de259cdff5c
4
+ data.tar.gz: 48566fe26a8518f5e54f393cc1bdcf6053010384
5
+ SHA512:
6
+ metadata.gz: 7786ee623a842528548f0f3768dc8eaafdd4415b9e2fc2803648f162b6c27c5078cf0fcd201697c389ef126d7438be4728765fddb9a633ea7edc0c80ecbf507f
7
+ data.tar.gz: ce2ea4b033ae9e0766723033057332354be8816d40434c18ddbccd8c70fd89b2e2cf0684d1a8fb97c041cfbb9050410ad36f470b964fe83566b0c1b4eb5b27a0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --warnings
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ ruby "2.1.2"
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in snakes_and_ladders.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Mohamad El-Husseini
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # SnakesAndLadders
2
+
3
+ This gem is a CLI clone of the classic board game [Snakes and Ladders][1] (or Chutes and Ladders). It's written in Ruby, and uses the Object-oriented programming paradigm.
4
+
5
+ This gem uses features only available in **Ruby 2.1** and above, such as *keyword arguments*, and *required keyword arguments*. You will need Ruby 2.1 to run this gem.
6
+
7
+ ## Table of Contents
8
+ - [About the Game][game]
9
+ - [Installation][installation]
10
+ - [Playing the Game][playing]
11
+ - [Simulating Games][simulating]
12
+ - [Extensibility and Building Custom Boards][extensibility]
13
+ - [Rendering][rendering]
14
+ - [Contributing][contributing]
15
+
16
+ ## About the Game
17
+
18
+ From the Snakes and Ladders [Wikipedia entry][1]:
19
+
20
+ >Snakes and Ladders is an ancient Indian board game regarded today as a worldwide classic. It is played between two or more players on a gameboard having numbered, gridded squares. A number of "ladders" and "snakes" are pictured on the board, each connecting two specific board squares. The object of the game is to navigate one's game piece, according to die rolls, from the start (bottom square) to the finish (top square), helped or hindered by ladders and snakes respectively. The historic version had root in morality lessons, where a player's progression up the board represented a life journey complicated by virtues (ladders) and vices (snakes).
21
+
22
+ This implementation is feature complete except for one rule: Rolling three sixes consecutively will not send you back to square one. Like the original, landing on or rolling in excess of the last square will cause you to win.
23
+
24
+ ## Installation
25
+
26
+ Add this line to your application's Gemfile:
27
+
28
+ gem 'snakes_and_ladders'
29
+
30
+ And then execute:
31
+
32
+ $ bundle
33
+
34
+ Or install it yourself as:
35
+
36
+ $ gem install snakes_and_ladders
37
+
38
+ ## Playing
39
+
40
+ First, create some players. Unlike its physical counterpart, this implementation has no player limit; you can play with as many players as you want.
41
+
42
+ ````ruby
43
+ peach = SnakesAndLadders.new_player(name: "Peach", color: "Pink")
44
+ luigi = SnakesAndLadders.new_player(name: "Luigi", color: "Green")
45
+ ````
46
+
47
+ Now we need a game instance. The game ships with the classic board (based on Milton Bradley edition, 1952). You can create your own boards using the Grid class. A random board generator will soon become available.
48
+
49
+ ````ruby
50
+ game = SnakesAndLadders.classic([peach, luigi])
51
+ ````
52
+
53
+ Now let's roll the die to play.
54
+
55
+ ````ruby
56
+ game.play_turn
57
+ # => Peach rolls 1.
58
+ # => Peach moves to square 1 and takes a ladder!
59
+ # => Peach is on square 38.
60
+ game.play_turn
61
+ # => Luigi rolls 5.
62
+ # ...
63
+ ````
64
+
65
+ ## Simulating Games
66
+
67
+ You can simulate entire games from start to finish without having to call `play_turn`. At any time you can call `game.simulate`, and the game will play itself to the end. You can simulate games before and after they have started.
68
+
69
+ ````ruby
70
+ game.simulate
71
+ # => Peach rolls a 5!
72
+ # => Peach is on square 43.
73
+ # => Luigi rolls 2!
74
+ # => Luigi moves to square 80 and takes a ladder!
75
+ # => Luigi is on square 100.
76
+ # => Game over! Luigi wins in 14 turns. Congratulations!
77
+ ````
78
+
79
+ ## Extensibility
80
+
81
+ The game is fairly extensible and additional tiles to cell, snake, and ladder are pluggable. You can create additional tiles by inheriting from `SnakesAndLadders::Cell` and overriding the `enter` and `exit` methods where necessary.
82
+
83
+ ````ruby
84
+ module SnakesAndLadders
85
+ class Bowser < Cell
86
+ def initialize(location: location)
87
+ super
88
+ end
89
+
90
+ def enter(player, board)
91
+ player.kidnap!
92
+ puts "#{player} was kidnapped by evil Bower. Bwahahahaha!"
93
+ super
94
+ end
95
+ end
96
+ end
97
+ ````
98
+
99
+ In this example we created a new tile called Bowser. It kidnaps any player that lands on it. The implementation of kidnap is not important for the purpose of this example.
100
+
101
+ ### Creating Custom Boards
102
+
103
+ Our Bowser tile is not going to use itself. We have to build a board and tell it where to place Bowser tiles. We can do this using the packaged builder. The `Grid` class is responsible for mounting new boards. It accepts an array of tile mappings.
104
+
105
+ ````ruby
106
+ mappings = [
107
+ { class: :Portal, location: 1, destination: 38 },
108
+ { class: :Bowser, location: 5 },
109
+ { class: :Portal, location: 16, destination: 6 },
110
+ # ...
111
+ ]
112
+ ````
113
+
114
+ A mapping is just a hash of attributes that represent a tile. A mapping *must contain* `class` and `location` keys. You only have to create mappings for custom tiles. The grid will fill any missing locations with a normal `Cell` objects (this behaviour is also customisable by passing in a custom `default_tile` argument).
115
+
116
+ Our example mappings will produce the following board. The numbers denote tile numbers.
117
+
118
+ ````
119
+ | 1. Portal | 2. Cell | 3. Cell | 4. Cell | 5. Bowser | 6. Cell ... | 16. Portal |
120
+ ````
121
+
122
+ If you are wondering what `Portal` is, it's a class that represents snakes and ladders. Snakes and ladders have identical behaviour. Only their data differs. In the case of a snake, its destination is always smaller than its location. The opposite is true for ladders. So we can use the same class to represent both. You can think of them as portals because they transport players to other locations; *where to* is secondary.
123
+
124
+ Once we have our mappings we tell the grid to mount them.
125
+
126
+ ````ruby
127
+ tiles = SnakesAndLadders::Grid.new(size: 100, tile_mappings: mappings).build
128
+ board = SnakesAndLadders::Board.new(grid: tiles)
129
+ game = SnakesAndLadders::Game.new(board: board, players: players)
130
+ ````
131
+
132
+ Now our game will use our shiny new Bowser tile. Needless to say you should only create mappings for classes that you have built.
133
+
134
+ ## Rendering
135
+
136
+ The game has no built in renderer. But any instance of Game contains all the information needed to render the game, so it's relatively easy to create your own render.
137
+
138
+ ## Contributing
139
+
140
+ If you fancy a crack at creating a terminal renderer class, please go ahead. I'm also always looking for code reviews.
141
+
142
+ 1. Fork it
143
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
144
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
145
+ 4. Push to the branch (`git push origin my-new-feature`)
146
+ 5. Create new Pull Request
147
+
148
+
149
+ [1]: http://en.wikipedia.org/wiki/Snakes_and_Ladders
150
+ [game]: #about-the-game
151
+ [installation]: #installation
152
+ [playing]: #playing
153
+ [simulating]: #simulating-games
154
+ [extensibility]: #extensibility
155
+ [rendering]: #rendering
156
+ [contributing]: #contributing
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
7
+
8
+ task :console do
9
+ exec "irb -r snakes_and_ladders -I ./lib"
10
+ end
@@ -0,0 +1,26 @@
1
+ module SnakesAndLadders
2
+ class Board
3
+ attr_reader :grid
4
+
5
+ def initialize(grid: Grid.classic)
6
+ @grid = grid
7
+ end
8
+
9
+ def move(player, from, to)
10
+ if destination = get_cell(to)
11
+ if location = get_cell(from)
12
+ location.exit(player)
13
+ end
14
+ destination.enter(player, self)
15
+ end
16
+ end
17
+
18
+ def get_cell(index)
19
+ grid[index]
20
+ end
21
+
22
+ def size
23
+ grid.size
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ module SnakesAndLadders
2
+ class Cell
3
+ attr_reader :location, :players
4
+
5
+ def initialize(location:, players: [])
6
+ @location = location
7
+ @players = players
8
+ end
9
+
10
+ def exit(player)
11
+ players.delete(player)
12
+ end
13
+
14
+ def enter(player, board)
15
+ players.push(player) && player.position = location
16
+ puts "#{player} is on square #{location}."
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,58 @@
1
+ module SnakesAndLadders
2
+ class Game
3
+ attr_reader :board, :players, :winner, :turn, :player
4
+
5
+ def initialize(board:, players: [], turn: 0)
6
+ @board = board
7
+ @players = players
8
+ @turn = turn
9
+ end
10
+
11
+ def add_player(player)
12
+ players.push(player)
13
+ end
14
+
15
+ def play_turn
16
+ return if over?
17
+
18
+ init_turn
19
+
20
+ puts "#{player} rolls #{player.last_roll}!"
21
+
22
+ if board.move(player, player.position, player.destination_after_last_roll)
23
+ self.winner = player if won?
24
+ else
25
+ self.winner = player if will_win?
26
+ end
27
+ end
28
+
29
+ def over?
30
+ !!winner
31
+ end
32
+
33
+ def simulate
34
+ play_turn until over?
35
+ end
36
+
37
+ private
38
+
39
+ def init_turn
40
+ @player = players.at(turn % players.size)
41
+ @player.roll_die
42
+ @turn += 1
43
+ end
44
+
45
+ def won?
46
+ player.position.equal?(board.size)
47
+ end
48
+
49
+ def will_win?
50
+ player.destination_after_last_roll >= board.size
51
+ end
52
+
53
+ def winner=(player)
54
+ @winner = player
55
+ puts "Game over! #{winner} wins in #{winner.turns} turns. Congratulations!"
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,55 @@
1
+ module SnakesAndLadders
2
+ class Grid
3
+ attr_reader :size, :tile_mappings, :default_tile
4
+
5
+ CLASSIC_BOARD_MAPPINGS = [
6
+ { class: :Portal, location: 1, destination: 38 },
7
+ { class: :Portal, location: 4, destination: 14 },
8
+ { class: :Portal, location: 9, destination: 31 },
9
+ { class: :Portal, location: 16, destination: 6 },
10
+ { class: :Portal, location: 28, destination: 84 },
11
+ { class: :Portal, location: 36, destination: 44 },
12
+ { class: :Portal, location: 40, destination: 42 },
13
+ { class: :Portal, location: 47, destination: 26 },
14
+ { class: :Portal, location: 49, destination: 11 },
15
+ { class: :Portal, location: 51, destination: 67 },
16
+ { class: :Portal, location: 56, destination: 53 },
17
+ { class: :Portal, location: 62, destination: 19 },
18
+ { class: :Portal, location: 64, destination: 60 },
19
+ { class: :Portal, location: 71, destination: 81 },
20
+ { class: :Portal, location: 80, destination: 100 },
21
+ { class: :Portal, location: 87, destination: 24 },
22
+ { class: :Portal, location: 93, destination: 73 },
23
+ { class: :Portal, location: 95, destination: 75 },
24
+ { class: :Portal, location: 98, destination: 78 },
25
+ ]
26
+
27
+ def self.classic
28
+ mappings = Marshal.load(Marshal.dump(CLASSIC_BOARD_MAPPINGS))
29
+ new(size: 100, tile_mappings: mappings).build
30
+ end
31
+
32
+ def initialize(size:, tile_mappings: [], default_tile: :Cell)
33
+ @size = size
34
+ @tile_mappings = tile_mappings
35
+ @default_tile = default_tile
36
+
37
+ raise ArgumentError, "Board size must be equal to or greater than its tiles." if size < tile_mappings.size
38
+ end
39
+
40
+ def build
41
+ (1..size).each_with_object({}) { |index, grid| grid[index] = tile_at(index) }
42
+ end
43
+
44
+ private
45
+
46
+ def tile_at(index)
47
+ if mapping = tile_mappings.detect { |tile_map| tile_map[:location].equal?(index) }
48
+ klass = mapping.delete(:class)
49
+ SnakesAndLadders.const_get(klass).new(mapping)
50
+ else
51
+ SnakesAndLadders.const_get(default_tile).new(location: index)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,34 @@
1
+ module SnakesAndLadders
2
+ class Player
3
+ attr_reader :name, :color, :die_rolls
4
+
5
+ attr_accessor :position
6
+
7
+ def initialize(name:, color:, position: 0, die_rolls: [])
8
+ @name = name
9
+ @color = color
10
+ @position = position
11
+ @die_rolls = die_rolls
12
+ end
13
+
14
+ def to_s
15
+ name
16
+ end
17
+
18
+ def roll_die
19
+ die_rolls.push(rand 1..6).last
20
+ end
21
+
22
+ def turns
23
+ die_rolls.size
24
+ end
25
+
26
+ def last_roll
27
+ die_rolls.last
28
+ end
29
+
30
+ def destination_after_last_roll
31
+ position + last_roll
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ module SnakesAndLadders
2
+ class Portal < Cell
3
+ attr_reader :destination
4
+
5
+ def initialize(location:, destination:)
6
+ @destination = destination
7
+ super(location: location)
8
+
9
+ raise ArgumentError, "Location and destination can not be equal" if location.equal?(destination)
10
+ end
11
+
12
+ def enter(player, board)
13
+ puts "#{player} moves to square #{location} and takes a #{type}!"
14
+ board.move(player, location, destination)
15
+ end
16
+
17
+ def type
18
+ location > destination ? :snake : :ladder
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module SnakesAndLadders
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,22 @@
1
+ require "snakes_and_ladders/game"
2
+ require "snakes_and_ladders/board"
3
+ require "snakes_and_ladders/grid"
4
+ require "snakes_and_ladders/cell"
5
+ require "snakes_and_ladders/portal"
6
+ require "snakes_and_ladders/player"
7
+ require "snakes_and_ladders/version"
8
+
9
+ begin
10
+ require "pry"
11
+ rescue LoadError
12
+ end
13
+
14
+ module SnakesAndLadders
15
+ def self.classic(players)
16
+ SnakesAndLadders::Game.new(board: SnakesAndLadders::Board.new, players: players)
17
+ end
18
+
19
+ def self.new_player(name:, color:)
20
+ SnakesAndLadders::Player.new(name: name, color: color)
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'snakes_and_ladders/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "snakes_and_ladders"
8
+ spec.version = SnakesAndLadders::VERSION
9
+ spec.authors = ["Mohamad El-Husseini"]
10
+ spec.email = ["husseini.mel@gmail.com"]
11
+ spec.description = %q{A Ruby implementation of Snakes and Ladders (Chutes and Ladders) board game.}
12
+ spec.summary = %q{This is an object oriented Ruby implementation of the classic Snakes and Ladders (Chutes and Ladders) board game.}
13
+ spec.homepage = "https://github.com/abitdodgy/snakes_and_ladders"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "pry"
25
+ end
@@ -0,0 +1,74 @@
1
+ require "spec_helper"
2
+
3
+ module SnakesAndLadders
4
+ describe SnakesAndLadders::Board do
5
+ let(:luigi) { double(:player) }
6
+
7
+ let(:first_cell) { double(:cell) }
8
+ let(:second_cell) { double(:cell) }
9
+
10
+ let(:grid) { { 3 => first_cell, 6 => second_cell } }
11
+
12
+ let(:board) { Board.new(grid: grid) }
13
+
14
+ describe "#initialize" do
15
+ it "defaults to classic grid" do
16
+ expect { Board.new }.not_to raise_error
17
+ end
18
+
19
+ it "sets grid" do
20
+ board = Board.new(grid: "any")
21
+ expect(board.grid).to eq "any"
22
+ end
23
+ end
24
+
25
+ describe "#grid" do
26
+ it "returns the grid" do
27
+ expect(board.grid).to eq grid
28
+ end
29
+ end
30
+
31
+ describe "#size" do
32
+ it "returns grid size" do
33
+ expect(board.size).to eq 2
34
+ end
35
+ end
36
+
37
+ describe "#get_cell" do
38
+ it "returns object at index" do
39
+ expect(board.get_cell(3)).to eq first_cell
40
+ end
41
+
42
+ it "returns nil if there is no object at index" do
43
+ expect(board.get_cell(4)).to be_nil
44
+ end
45
+ end
46
+
47
+ describe "#move" do
48
+ context "when player is off the board" do
49
+ it "moves player to destination" do
50
+ expect(first_cell).to_not receive(:exit)
51
+ expect(second_cell).to receive(:enter).with luigi, board
52
+ board.move(luigi, 0, 6)
53
+ end
54
+ end
55
+
56
+ context "when player is on a cell" do
57
+ it "moves player between cells" do
58
+ expect(first_cell).to receive(:exit).with luigi
59
+ expect(second_cell).to receive(:enter).with luigi, board
60
+ board.move(luigi, 3, 6)
61
+ end
62
+ end
63
+
64
+ context "when movement exceeds board size" do
65
+ it "does not move player and returns false" do
66
+ expect(second_cell).not_to receive :exit
67
+ expect(second_cell).not_to receive :enter
68
+ result = board.move(luigi, 3, 7)
69
+ expect(result).to be_nil
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,57 @@
1
+ require "spec_helper"
2
+
3
+ module SnakesAndLadders
4
+ describe Cell do
5
+ let(:cell) { Cell.new(location: 1) }
6
+
7
+ describe "#initialize" do
8
+ it "raises an error without location" do
9
+ expect { Cell.new }.to raise_error(ArgumentError).with_message("missing keyword: location")
10
+ end
11
+
12
+ it "sets location" do
13
+ expect(cell.location).to eq 1
14
+ end
15
+
16
+ it "defaults players to empty array" do
17
+ expect(cell.players).to eq []
18
+ end
19
+
20
+ it "sets players" do
21
+ cell = Cell.new(location: 1, players: ["Mario"])
22
+ expect(cell.players).to eq ["Mario"]
23
+ end
24
+ end
25
+
26
+ describe "#location" do
27
+ it "returns location" do
28
+ expect(cell.location).to eq 1
29
+ end
30
+ end
31
+
32
+ describe "#players" do
33
+ it "returns an array" do
34
+ expect(cell.players).to be_an Array
35
+ end
36
+ end
37
+
38
+ describe "#enter" do
39
+ let(:mario) { double(:mario) }
40
+
41
+ it "updates player position and adds player to players array" do
42
+ expect(mario).to receive(:position=).with cell.location
43
+ cell.enter(mario, "Board")
44
+ expect(cell.players).to include mario
45
+ end
46
+ end
47
+
48
+ describe "#exit" do
49
+ let(:cell) { Cell.new(location: 1, players: ["mario"]) }
50
+
51
+ it "delete player from players array" do
52
+ cell.exit("mario")
53
+ expect(cell.players).not_to include "mario"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,152 @@
1
+ require "spec_helper"
2
+
3
+ module SnakesAndLadders
4
+ describe Game do
5
+ let(:game) { Game.new(board: "Board") }
6
+
7
+ describe "#initialize" do
8
+ it "raises an error without board" do
9
+ expect { Game.new }.to raise_error(ArgumentError).with_message("missing keyword: board")
10
+ end
11
+
12
+ it "sets a board" do
13
+ expect(game.board).to eq "Board"
14
+ end
15
+
16
+ it "defaults players to empty array" do
17
+ expect(game.players).to eq []
18
+ end
19
+
20
+ it "sets players array" do
21
+ game = Game.new(board: "Board", players: %w(Mario Luigi))
22
+ expect(game.players).to eq %w(Mario Luigi)
23
+ end
24
+
25
+ it "defaults turn to 0" do
26
+ expect(game.turn).to eq 0
27
+ end
28
+
29
+ it "sets turn" do
30
+ game = Game.new(board: "Board", turn: 5)
31
+ expect(game.turn).to eq 5
32
+ end
33
+ end
34
+
35
+ describe "#board" do
36
+ it "returns game board" do
37
+ expect(game.board).to eq "Board"
38
+ end
39
+ end
40
+
41
+ describe "#players" do
42
+ it "returns an array" do
43
+ expect(game.players).to be_an Array
44
+ end
45
+ end
46
+
47
+ describe "#turn" do
48
+ it "return an integer" do
49
+ expect(game.turn).to be_an Integer
50
+ end
51
+ end
52
+
53
+ describe "#play_turn" do
54
+ let(:mario) { double(:player, roll_die: 3, position: 1, destination_after_last_roll: 4, turns: 2, last_roll: 3) }
55
+ let(:luigi) { double(:player, roll_die: 0, position: 0, destination_after_last_roll: 0, turns: 0, last_roll: 0) }
56
+ let(:peach) { double(:player) }
57
+
58
+ let(:players) { [mario, luigi, peach] }
59
+
60
+ let(:board) { double(:board, size: 10, move: true) }
61
+ let(:game) { Game.new(board: board, players: players) }
62
+
63
+ it "defaults winner to nil" do
64
+ expect(game.winner).to be_nil
65
+ end
66
+
67
+ it "is not over" do
68
+ expect(game.over?).to eq false
69
+ end
70
+
71
+ it "cycles player turns" do
72
+ game.play_turn
73
+ expect(game.player).to eq mario
74
+ game.play_turn
75
+ expect(game.player).to eq luigi
76
+ end
77
+
78
+ it "calls player.roll_die" do
79
+ expect(mario).to receive(:roll_die)
80
+ game.play_turn
81
+ end
82
+
83
+ it "increments turn" do
84
+ game.play_turn
85
+ expect(game.turn).to eq 1
86
+ end
87
+
88
+ it "calls board.move" do
89
+ expect(board).to receive(:move).with(mario, 1, 4)
90
+ game.play_turn
91
+ end
92
+ end
93
+
94
+ describe "#simulate" do
95
+ let(:mario) { Player.new(name: "Mario", color: "Red") }
96
+ let(:luigi) { Player.new(name: "Luigi", color: "Green") }
97
+
98
+ let(:game) { Game.new(board: Board.new, players: [mario, luigi]) }
99
+
100
+ before { game.simulate }
101
+
102
+ it "simulates and entire game" do
103
+ expect(game.over?).to eq true
104
+ expect(game.players).to include game.winner
105
+ expect(game.play_turn).to be_nil
106
+ end
107
+ end
108
+
109
+ describe "#over?" do
110
+ # This depends on the position attribute to decide winner.
111
+ # This tests the "board.move returns true" scenario, so we must assume mario already moved, and his position was updated.
112
+ # This explains why position and destination_after_last_roll are equal, which happens when the player moves.
113
+ context "when player lands on last square" do
114
+ let(:board) { double(:board, size: 10, move: true) }
115
+ let(:mario) { double(:mario, roll_die: 1, position: 10, destination_after_last_roll: 10, turns: 1, last_roll: 1) }
116
+ let(:game) { Game.new(board: board, players: [mario]) }
117
+
118
+ it "returns true" do
119
+ game.play_turn
120
+ expect(game.over?).to be true
121
+ end
122
+ end
123
+
124
+ # This depends on destination_after_last_roll to decide winner.
125
+ # This tests the "board.move returns false" scenario. So mario's position does not get updated, and we must decide if his roll
126
+ # takes him over the winning line, since he can't move off the board.
127
+ context "when player roll will cause it to exceed last cell" do
128
+ let(:board) { double(:board, size: 10, move: false) }
129
+ let(:mario) { double(:mario, roll_die: 3, position: 9, destination_after_last_roll: 12, turns: 1, last_roll: 3) }
130
+ let(:game) { Game.new(board: board, players: [mario]) }
131
+
132
+ it "returns true" do
133
+ game.play_turn
134
+ expect(game.over?).to be true
135
+ end
136
+ end
137
+
138
+ context "when player roll is not sufficient to win" do
139
+ let(:board) { double(:board, size: 10, move: true) }
140
+ let(:mario) { double(:mario, roll_die: 1, position: 8, destination_after_last_roll: 9, last_roll: 1) }
141
+ let(:game) { Game.new(board: board, players: [mario]) }
142
+
143
+ before { game.add_player(mario) }
144
+
145
+ it "returns false" do
146
+ game.play_turn
147
+ expect(game.over?).to be false
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,108 @@
1
+ require "spec_helper"
2
+
3
+ module SnakesAndLadders
4
+ describe Grid do
5
+ let(:grid) { Grid.new(size: 10) }
6
+
7
+ context "#initialize" do
8
+ it "raises an error without size" do
9
+ expect { Grid.new }.to raise_error(ArgumentError).with_message("missing keyword: size")
10
+ end
11
+
12
+ it "raises an error if object mappings are greater than size" do
13
+ expect { Grid.new(size: 1, tile_mappings: [{ 1 => 1 }, { 2 => 2 }]) }.to raise_error(ArgumentError)
14
+ end
15
+
16
+ it "sets size" do
17
+ expect(grid.size).to eq 10
18
+ end
19
+
20
+ it "sets tile_mappings" do
21
+ tile_mappings = [{ princess: "peach" }]
22
+ grid = Grid.new(size: 10, tile_mappings: tile_mappings)
23
+ expect(grid.tile_mappings).to eq tile_mappings
24
+ end
25
+
26
+ it "defaults tile_mappings to an empty array" do
27
+ expect(grid.tile_mappings).to eq Array.new
28
+ end
29
+
30
+ it "sets default_tile" do
31
+ grid = Grid.new(size: 10, default_tile: :BowserCastle)
32
+ expect(grid.default_tile).to eq :BowserCastle
33
+ end
34
+
35
+ it "defaults default_tile to Cell" do
36
+ expect(grid.default_tile).to eq :Cell
37
+ end
38
+ end
39
+
40
+ describe "#size" do
41
+ it "returns an integer" do
42
+ expect(grid.size).to be_an Integer
43
+ end
44
+ end
45
+
46
+ describe "#tile_mappings" do
47
+ it "returns an array" do
48
+ expect(grid.tile_mappings).to be_an Array
49
+ end
50
+ end
51
+
52
+ describe "#default_tile" do
53
+ it "returns a symbol" do
54
+ expect(grid.default_tile).to be_an Symbol
55
+ end
56
+ end
57
+
58
+ describe "#build" do
59
+ let :tile_mappings do
60
+ [
61
+ { class: :Portal, location: 1, destination: 3 },
62
+ { class: :Portal, location: 4, destination: 2 },
63
+ ]
64
+ end
65
+
66
+ it "builds a grid from mappings" do
67
+ grid = Grid.new(size: 4, tile_mappings: tile_mappings).build
68
+ expect(grid).to be_a Hash
69
+
70
+ expect(grid[1]).to be_a Portal
71
+ expect(grid[2]).to be_a Cell
72
+ expect(grid[3]).to be_a Cell
73
+ expect(grid[4]).to be_a Portal
74
+
75
+ expect(grid[1].location).to eq 1
76
+ expect(grid[2].location).to eq 2
77
+ expect(grid[3].location).to eq 3
78
+ expect(grid[4].location).to eq 4
79
+
80
+ expect(grid[1].destination).to eq 3
81
+ expect(grid[4].destination).to eq 2
82
+ end
83
+ end
84
+
85
+ describe "#build with custom default_tile" do
86
+ let :tile_mappings do
87
+ [ { class: :Portal, location: 1, destination: 3 } ]
88
+ end
89
+
90
+ let(:castle) { double(:Castle) }
91
+
92
+ it "builds a grid from mappings" do
93
+ skip "not sure how to test this since we have no custom class"
94
+ # grid = Grid.new(size: 3, tile_mappings: tile_mappings, default_tile: :Castle).build
95
+ end
96
+ end
97
+
98
+ describe "CLASSIC_BOARD_MAPPINGS" do
99
+ it "should define classic board const" do
100
+ expect(Grid).to have_constant(:CLASSIC_BOARD_MAPPINGS)
101
+ end
102
+ end
103
+
104
+ describe ".classic" do
105
+ skip "just a reminder to test this class method"
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,123 @@
1
+ require "spec_helper"
2
+
3
+ module SnakesAndLadders
4
+ describe Player do
5
+ let(:player) { Player.new(name: "Mario", color: "Red") }
6
+
7
+ describe "#initialize" do
8
+ it "raises an error without name" do
9
+ expect { Player.new(color: "Red") }.to raise_error(ArgumentError).with_message("missing keyword: name")
10
+ end
11
+
12
+ it "raises an error without color" do
13
+ expect { Player.new(name: "Mario") }.to raise_error(ArgumentError).with_message("missing keyword: color")
14
+ end
15
+
16
+ it "sets player name" do
17
+ expect(player.name).to eq "Mario"
18
+ end
19
+
20
+ it "sets player color" do
21
+ expect(player.color).to eq "Red"
22
+ end
23
+
24
+ it "defaults die_rolls to an empty array" do
25
+ expect(player.die_rolls).to eq []
26
+ end
27
+
28
+ it "sets die_rolls" do
29
+ player = Player.new(name: "Mario", color: "Red", die_rolls: [1,2,3])
30
+ expect(player.die_rolls).to eq [1,2,3]
31
+ end
32
+
33
+ it "defaults position to 0" do
34
+ expect(player.position).to eq 0
35
+ end
36
+
37
+ it "sets position" do
38
+ player = Player.new(name: "Mario", color: "Red", position: 5)
39
+ expect(player.position).to eq 5
40
+ end
41
+ end
42
+
43
+ describe "#name" do
44
+ it "returns player name" do
45
+ expect(player.name).to eq "Mario"
46
+ end
47
+ end
48
+
49
+ describe "#color" do
50
+ it "returns player color" do
51
+ expect(player.color).to eq "Red"
52
+ end
53
+ end
54
+
55
+ describe "#position" do
56
+ it "returns an integer" do
57
+ expect(player.position).to be_an Integer
58
+ end
59
+ end
60
+
61
+ describe "#position=" do
62
+ it "sets position" do
63
+ player.position = 5
64
+ expect(player.position).to eq 5
65
+ end
66
+ end
67
+
68
+ describe "#to_s" do
69
+ it "returns the name attribute" do
70
+ expect(player.to_s).to eq "Mario"
71
+ end
72
+ end
73
+
74
+ describe "#roll_die" do
75
+ it "returns an integer" do
76
+ expect(player.roll_die).to be_an Integer
77
+ end
78
+
79
+ it "is greater than 0" do
80
+ expect(player.roll_die).to be > 0
81
+ end
82
+
83
+ it "is lesser than 7" do
84
+ expect(player.roll_die).to be < 7
85
+ end
86
+
87
+ it "records die roll" do
88
+ roll = player.roll_die
89
+ expect(player.die_rolls).to include roll
90
+ end
91
+ end
92
+
93
+ describe "#die_rolls" do
94
+ it "returns an array" do
95
+ expect(player.die_rolls).to be_an Array
96
+ end
97
+ end
98
+
99
+ describe "#turns" do
100
+ before do
101
+ 3.times { player.roll_die }
102
+ end
103
+
104
+ it "returns the number of turns played" do
105
+ expect(player.turns).to eq 3
106
+ end
107
+ end
108
+
109
+ describe "#last_roll" do
110
+ it "returns the last die roll" do
111
+ roll = player.roll_die
112
+ expect(player.last_roll).to eq roll
113
+ end
114
+ end
115
+
116
+ describe "#destination_after_last_roll" do
117
+ it "returns last roll added to position" do
118
+ roll = player.roll_die
119
+ expect(player.destination_after_last_roll).to eq player.position + roll
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,62 @@
1
+ require "spec_helper"
2
+
3
+ module SnakesAndLadders
4
+ describe Portal do
5
+ let(:portal) { Portal.new(location: 3, destination: 5) }
6
+
7
+ describe "#initialize" do
8
+ it "raises an error without location" do
9
+ expect { Portal.new(destination: 1) }.to raise_error(ArgumentError).with_message("missing keyword: location")
10
+ end
11
+
12
+ it "raises an error without destination" do
13
+ expect { Portal.new(location: 1) }.to raise_error(ArgumentError).with_message("missing keyword: destination")
14
+ end
15
+
16
+ it "sets destination" do
17
+ expect(portal.destination).to eq 5
18
+ end
19
+
20
+ it "calls super and sets location" do
21
+ expect(portal.location).to eq 3
22
+ end
23
+
24
+ it "raises an error when location is equal to destination" do
25
+ expect { Portal.new(location: 1, destination: 1) }.to raise_error(ArgumentError)
26
+ end
27
+ end
28
+
29
+ describe "#location" do
30
+ it "returns location" do
31
+ expect(portal.location).to eq 3
32
+ end
33
+ end
34
+
35
+ describe "#destination" do
36
+ it "returns destination" do
37
+ expect(portal.destination).to eq 5
38
+ end
39
+ end
40
+
41
+ describe "#enter" do
42
+ let(:board) { double(:board) }
43
+
44
+ it "moves player to cell corresponding to its destination" do
45
+ expect(board).to receive(:move).with("Mario", portal.location, portal.destination)
46
+ portal.enter("Mario", board)
47
+ end
48
+ end
49
+
50
+ describe "#type" do
51
+ it "returns snake when destination is less than location" do
52
+ portal = Portal.new(location: 5, destination: 1)
53
+ expect(portal.type).to eq(:snake)
54
+ end
55
+
56
+ it "returns ladder when location is less than destination" do
57
+ portal = Portal.new(location: 1, destination: 5)
58
+ expect(portal.type).to eq(:ladder)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,82 @@
1
+ require "snakes_and_ladders"
2
+ require "support/matchers"
3
+ require "support/utils"
4
+
5
+ # This file was generated by the `rspec --init` command. Conventionally, all
6
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
7
+ # The generated `.rspec` file contains `--require spec_helper` which will cause this
8
+ # file to always be loaded, without a need to explicitly require it in any files.
9
+ #
10
+ # Given that it is always loaded, you are encouraged to keep this file as
11
+ # light-weight as possible. Requiring heavyweight dependencies from this file
12
+ # will add to the boot time of your test suite on EVERY test run, even for an
13
+ # individual file that may not need all of that loaded. Instead, make a
14
+ # separate helper file that requires this one and then use it only in the specs
15
+ # that actually need it.
16
+ #
17
+ # The `.rspec` file also contains a few flags that are not defaults but that
18
+ # users commonly want.
19
+ #
20
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
21
+ RSpec.configure do |config|
22
+ # The settings below are suggested to provide a good initial experience
23
+ # with RSpec, but feel free to customize to your heart's content.
24
+ =begin
25
+ # These two settings work together to allow you to limit a spec run
26
+ # to individual examples or groups you care about by tagging them with
27
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
28
+ # get run.
29
+ config.filter_run :focus
30
+ config.run_all_when_everything_filtered = true
31
+
32
+ # Many RSpec users commonly either run the entire suite or an individual
33
+ # file, and it's useful to allow more verbose output when running an
34
+ # individual spec file.
35
+ if config.files_to_run.one?
36
+ # Use the documentation formatter for detailed output,
37
+ # unless a formatter has already been configured
38
+ # (e.g. via a command-line flag).
39
+ config.default_formatter = 'doc'
40
+ end
41
+
42
+ # Print the 10 slowest examples and example groups at the
43
+ # end of the spec run, to help surface which specs are running
44
+ # particularly slow.
45
+ config.profile_examples = 10
46
+
47
+ # Run specs in random order to surface order dependencies. If you find an
48
+ # order dependency and want to debug it, you can fix the order by providing
49
+ # the seed, which is printed after each run.
50
+ # --seed 1234
51
+ config.order = :random
52
+
53
+ # Seed global randomization in this process using the `--seed` CLI option.
54
+ # Setting this allows you to use `--seed` to deterministically reproduce
55
+ # test failures related to randomization by passing the same `--seed` value
56
+ # as the one that triggered the failure.
57
+ Kernel.srand config.seed
58
+
59
+ # rspec-expectations config goes here. You can use an alternate
60
+ # assertion/expectation library such as wrong or the stdlib/minitest
61
+ # assertions if you prefer.
62
+ config.expect_with :rspec do |expectations|
63
+ # Enable only the newer, non-monkey-patching expect syntax.
64
+ # For more details, see:
65
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
66
+ expectations.syntax = :expect
67
+ end
68
+
69
+ # rspec-mocks config goes here. You can use an alternate test double
70
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
71
+ config.mock_with :rspec do |mocks|
72
+ # Enable only the newer, non-monkey-patching expect syntax.
73
+ # For more details, see:
74
+ # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
75
+ mocks.syntax = :expect
76
+
77
+ # Prevents you from mocking or stubbing a method that does not exist on
78
+ # a real object. This is generally recommended.
79
+ mocks.verify_partial_doubles = true
80
+ end
81
+ =end
82
+ end
@@ -0,0 +1,5 @@
1
+ RSpec::Matchers.define :have_constant do |const|
2
+ match do |owner|
3
+ owner.const_defined?(const)
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: snakes_and_ladders
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Mohamad El-Husseini
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: A Ruby implementation of Snakes and Ladders (Chutes and Ladders) board
70
+ game.
71
+ email:
72
+ - husseini.mel@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".rspec"
79
+ - Gemfile
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - lib/snakes_and_ladders.rb
84
+ - lib/snakes_and_ladders/board.rb
85
+ - lib/snakes_and_ladders/cell.rb
86
+ - lib/snakes_and_ladders/game.rb
87
+ - lib/snakes_and_ladders/grid.rb
88
+ - lib/snakes_and_ladders/player.rb
89
+ - lib/snakes_and_ladders/portal.rb
90
+ - lib/snakes_and_ladders/version.rb
91
+ - snakes_and_ladders.gemspec
92
+ - spec/snakes_and_ladders/board_spec.rb
93
+ - spec/snakes_and_ladders/cell_spec.rb
94
+ - spec/snakes_and_ladders/game_spec.rb
95
+ - spec/snakes_and_ladders/grid_spec.rb
96
+ - spec/snakes_and_ladders/player_spec.rb
97
+ - spec/snakes_and_ladders/portal_spec.rb
98
+ - spec/spec_helper.rb
99
+ - spec/support/matchers.rb
100
+ homepage: https://github.com/abitdodgy/snakes_and_ladders
101
+ licenses:
102
+ - MIT
103
+ metadata: {}
104
+ post_install_message:
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ requirements: []
119
+ rubyforge_project:
120
+ rubygems_version: 2.2.2
121
+ signing_key:
122
+ specification_version: 4
123
+ summary: This is an object oriented Ruby implementation of the classic Snakes and
124
+ Ladders (Chutes and Ladders) board game.
125
+ test_files:
126
+ - spec/snakes_and_ladders/board_spec.rb
127
+ - spec/snakes_and_ladders/cell_spec.rb
128
+ - spec/snakes_and_ladders/game_spec.rb
129
+ - spec/snakes_and_ladders/grid_spec.rb
130
+ - spec/snakes_and_ladders/player_spec.rb
131
+ - spec/snakes_and_ladders/portal_spec.rb
132
+ - spec/spec_helper.rb
133
+ - spec/support/matchers.rb