rb_battleship 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 931cc1b1f8aa09c59792fea9b6b1b0f2a5ac44a5
4
+ data.tar.gz: ef85817e4cfe22c5e8b5c4d6e815c2b94f0c0b4e
5
+ SHA512:
6
+ metadata.gz: bb0a90101f5609c42a4a8395e5525931ac8aaa0c3aa7782541cd8dc3b8b53fdb0b536742dcfb0b6dbf022fcf7d057f594932f354273925201ba8031793666315
7
+ data.tar.gz: d23cad03fc419bddd789214d16cae28c2beb925c73696ef19a328796201c23692065449c0a2c50b9a5627db9272588e28c8d71d24d6cacc5e357aa3c8d528451
@@ -0,0 +1,3 @@
1
+ Gemfile.lock
2
+ .ruby-version
3
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
3
+ gemspec
@@ -0,0 +1,62 @@
1
+ # :ship: :boom: Battleship :boom: :ship:
2
+ The objective of this task is to build a battleship game for two players in Ruby in a terminal.
3
+
4
+ ## Design
5
+
6
+ ### SRP
7
+ The code design follows the single responsibility principle by using a dedicated class for any specific task.
8
+
9
+ ## Installation
10
+ Install the gem from your shell:
11
+ ```shell
12
+ gem install battleship
13
+ ```
14
+
15
+ ## Gameplay
16
+ The gem includes a single binary you can use to start a new game:
17
+ ```shell
18
+ battleship
19
+ ******* 1° round
20
+ ******* Player 1 turn
21
+ place small (3x1) ship on grid by specifying coordinates and cardinal point separated by spaces (ie 1 3 SE):
22
+ >
23
+ ```
24
+
25
+ ## Tests
26
+ The gem is covered, where possible, by fast, isolated unit testing:gem is covered, where possible, by fast, isolated unit testing. Execute them all by:
27
+ ```shell
28
+ bundle exec rake
29
+ ```
30
+
31
+ ## Kata objectives
32
+ * :warning: You should not require any gems aside from ones to help you write any tests (rspec, minitest...). :warning:
33
+ * Your code must be tested as much as possible.
34
+ * Each step must be 100% done before moving to the next. We won't take into account any part done in advance.
35
+ * You should explain how to run your game (and the tests) in a README.md file in the root of your project.
36
+
37
+ ## 1st step - Board setup
38
+ The game is played on 5x5 grids (one per player). Your first task is to set the game up:
39
+ * Players have 2 ships each to place on their grid. A small ship (3x1 side) and large one (4x1 size)
40
+ * Players are asked in turn to place their ships on their board (ie: interactively)
41
+ * A ship can't be placed out of bounds nor on the same space as another ship.
42
+
43
+ ## 2nd step - Gameplay setup
44
+ Once all the ships are placed the game begins! Now you have to set the gameplay mechanisms up:
45
+ * Players take turns to shoot at the opponent grid one after the other by selecting coordinates.
46
+ * Each shot receives a Hit, Miss or Sink response.
47
+ * The winner is the player who sinks all of their opponent ships first
48
+
49
+ ## 3rd step - Gameplay improvements
50
+ At this point the game works and we'd like some improvements of some of its mechanisms:
51
+ * Starting player is determined at random.
52
+ * If a shot attempt is made out of bounds the game offers a retry.
53
+ * When the game is finished it gives the option to play again.
54
+
55
+ ## 4th step - Bonus
56
+ This part is here to spice things up! :boom: We'd like to have these features added to the game:
57
+ * Ships can now be placed diagonally.
58
+ * If a player misses a shot 3 times in a row the game gives a hint of a valid shot.
59
+ * The game now works in a "Best of 3" match mode:
60
+ > If player 1 wins the first game, the match is not over yet because player 2 can still win the 2 other games and win the match.
61
+ > If player 1 wins first 2 consecutive games, then the other player can possibly win 1 game maximum, which is not sufficient to beat the 2 games won by player 1.
62
+ > If each player won 1 of the first 2 games then the third game is played. Whoever wins the third game is declared winner of the match.
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:spec) do |t|
5
+ t.libs << "spec"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["spec/**/*_spec.rb"]
8
+ end
9
+
10
+ task :default => :spec
@@ -0,0 +1,22 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "battleship/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "rb_battleship"
8
+ s.version = Battleship::VERSION
9
+ s.authors = ["costajob"]
10
+ s.email = ["costajob@gmail.com"]
11
+ s.license = "MIT"
12
+ s.summary = "Implementation of the Battleship code kata"
13
+ s.homepage = "https://github.com/costajob/battleship.git"
14
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
15
+ s.bindir = "bin"
16
+ s.executables << "battleship"
17
+ s.require_paths = ["lib"]
18
+ s.required_ruby_version = ">= 2.3.1"
19
+ s.add_development_dependency "bundler", "~> 1.16"
20
+ s.add_development_dependency "rake", "~> 12.3"
21
+ s.add_development_dependency "minitest", "~> 5.1"
22
+ end
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path("../../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require "battleship"
7
+
8
+ Battleship::GamePlay.call
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "battleship"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,7 @@
1
+ require "battleship/gameplay"
2
+
3
+ module Battleship
4
+ extend self
5
+
6
+ CARDINALS = %w[N E S W NE NW SE SW]
7
+ end
@@ -0,0 +1,38 @@
1
+ module Battleship
2
+ class Coord
3
+ class FactoryError < StandardError; end
4
+
5
+ attr_reader :x, :y
6
+
7
+ def self.orig
8
+ @orig ||= new(0, 0)
9
+ end
10
+
11
+ def self.factory(data)
12
+ new(*data.split(" "))
13
+ rescue ArgumentError
14
+ fail FactoryError.new("coordinate must be specified as 'x y' (ie 1 2)")
15
+ end
16
+
17
+ def initialize(x, y)
18
+ @x = x.to_i.abs
19
+ @y = y.to_i.abs
20
+ end
21
+
22
+ def to_s
23
+ "#{x} #{y}"
24
+ end
25
+
26
+ def >=(other)
27
+ x >= other.x && y >= other.y
28
+ end
29
+
30
+ def <=(other)
31
+ x <= other.x && y <= other.y
32
+ end
33
+
34
+ def ==(other)
35
+ x == other.x && y == other.y
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,81 @@
1
+ require "battleship/player"
2
+
3
+ module Battleship
4
+ class GamePlay
5
+ STARS = "*" * 7
6
+ START_MSG = -> (nbr) { "#{STARS} #{nbr}° round" }
7
+ TURN_MSG = -> (name) { "#{STARS} #{name} turn" }
8
+ ROUND_MSG = -> (name) { "#{STARS} #{name} wins the round" }
9
+ GAMEOVER_MSG = -> (name) { "#{STARS} #{name} wins the game" }
10
+ ANSWERS = %w[Y N y n]
11
+ MATCHES = 3
12
+
13
+ def self.call
14
+ new.call
15
+ end
16
+
17
+ attr_reader :matches, :output
18
+
19
+ def initialize(player_class=Player, output=STDOUT)
20
+ @player_class = player_class
21
+ @players = nil
22
+ @winner = nil
23
+ @matches = []
24
+ @output = output
25
+ end
26
+
27
+ def players
28
+ return @players if @players
29
+ p1, p2 = [1, 2].map { |i| @player_class.factory(name: "Player #{i}", output: @output) }
30
+ p1.enemy = p2
31
+ p2.enemy = p1
32
+ @players = [p1, p2]
33
+ end
34
+
35
+ def call
36
+ output.puts(GAMEOVER_MSG.call(play))
37
+ end
38
+
39
+ private def play
40
+ loop do
41
+ output.puts(START_MSG.call(matches.size+1))
42
+ setup
43
+ round
44
+ return winner.name if winner
45
+ end
46
+ end
47
+
48
+ private def winner
49
+ @winner ||= players.detect do |player|
50
+ matches.count(player.name) > 1
51
+ end
52
+ end
53
+
54
+ private def setup
55
+ players.shuffle.each do |player|
56
+ output.puts(TURN_MSG.call(player.name))
57
+ player.setup
58
+ end
59
+ end
60
+
61
+ private def fight
62
+ loop do
63
+ players.each do |player|
64
+ output.puts(TURN_MSG.call(player.name))
65
+ player.shot
66
+ return player.name if player.enemy.gameover?
67
+ end
68
+ end
69
+ end
70
+
71
+ private def round
72
+ matches << fight
73
+ output.puts(ROUND_MSG.call(matches[-1]))
74
+ reset!
75
+ end
76
+
77
+ private def reset!
78
+ @players = nil
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,82 @@
1
+ require "forwardable"
2
+ require "battleship/coord"
3
+
4
+ module Battleship
5
+ class Grid
6
+ extend Forwardable
7
+
8
+ SIZE = 4
9
+ PLACE_ERR = "ship placed ouside of grid and/or overlapping an exisitng one"
10
+ COORD_ERR = -> (grid) { "coordinate is ouside of grid (#{grid.bot_left}) - (#{grid.top_right})" }
11
+ HIT = "Hit"
12
+ MISS = "Miss"
13
+ SINK = "Sink"
14
+
15
+ class PlacingError < StandardError; end
16
+
17
+ def self.factory(coord_class=Coord)
18
+ new(coord_class.orig, coord_class.new(SIZE, SIZE))
19
+ end
20
+
21
+ def_delegators :@ships, :include?, :empty?, :size
22
+
23
+ attr_reader :bot_left, :top_right, :ships
24
+
25
+ def initialize(bot_left, top_right)
26
+ @bot_left = bot_left
27
+ @top_right = top_right
28
+ @ships = []
29
+ end
30
+
31
+ def to_s
32
+ "bot_left=#{bot_left} top_right=#{top_right} ships=#{ships.size}"
33
+ end
34
+
35
+ def <<(ship)
36
+ fail PlacingError.new(PLACE_ERR) unless valid?(ship)
37
+ ships << ship
38
+ end
39
+
40
+ def shot(coord)
41
+ fail PlacingError.new(COORD_ERR.call(self)) unless included?(coord)
42
+ data = damages(coord)
43
+ purge!
44
+ return SINK if data.any? { |d| d < 0 }
45
+ return HIT if data.any? { |d| d > 0 }
46
+ MISS
47
+ end
48
+
49
+ def footprint
50
+ ships.map(&:footprint).flatten
51
+ end
52
+
53
+ private def damages(coord)
54
+ ships.reduce([]) do |acc, ship|
55
+ damage = ship.strike(coord)
56
+ acc << damage
57
+ end
58
+ end
59
+
60
+ private def purge!
61
+ ships.reject!(&:empty?)
62
+ end
63
+
64
+ private def valid?(ship)
65
+ within?(ship) && !overlap?(ship)
66
+ end
67
+
68
+ private def within?(ship)
69
+ ship.footprint.all? { |coord| included?(coord) }
70
+ end
71
+
72
+ private def included?(coord)
73
+ coord >= bot_left && coord <= top_right
74
+ end
75
+
76
+ private def overlap?(ship)
77
+ footprint.any? do |coord|
78
+ ship.footprint.any? { |c| c == coord }
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,80 @@
1
+ require "forwardable"
2
+ require "battleship/grid"
3
+ require "battleship/position"
4
+ require "battleship/ship"
5
+
6
+ module Battleship
7
+ class Player
8
+ extend Forwardable
9
+
10
+ PLACE_MSG = -> (ship) { "place #{ship.name} ship on grid by specifying coordinates and cardinal point separated by spaces (ie 1 3 SE):" }
11
+ SHOT_MSG = "enter coordinates for shooting your enemy grid (ie 2 3):"
12
+ TIP_MSG = -> (shot, coord) { "#{shot}, you should try with the (#{coord}) coordinates next time" }
13
+ TIP_THRESHOLD = 3
14
+
15
+ class MissesError < StandardError; end
16
+
17
+ def_delegator :@grid, :empty?, :gameover?
18
+
19
+ def self.factory(name:, grid_class: Grid, output: STDOUT)
20
+ new(name: name, grid: grid_class.factory, output: output)
21
+ end
22
+
23
+ attr_reader :name, :grid, :ships
24
+ attr_accessor :enemy
25
+
26
+ def initialize(name:, grid:, output: STDOUT)
27
+ @name = name
28
+ @grid = grid
29
+ @misses = 0
30
+ @output = output
31
+ @pos = nil
32
+ @coord = nil
33
+ end
34
+
35
+ def setup(ships=[Ship.small, Ship.large])
36
+ Array(ships).each { |ship| place(ship) }
37
+ end
38
+
39
+ def shot(coord_class=Coord)
40
+ @output.puts(SHOT_MSG)
41
+ @coord ||= STDIN.gets
42
+ shot = enemy.grid.shot(coord_class.factory(@coord.chomp))
43
+ @output.puts(report(shot))
44
+ @coord = nil
45
+ rescue grid.class::PlacingError, coord_class::FactoryError => e
46
+ @output.puts(e.message)
47
+ @coord = nil
48
+ retry
49
+ end
50
+
51
+ private def report(shot)
52
+ return shot unless tip?(shot)
53
+ coord = enemy.grid.footprint.sample
54
+ TIP_MSG.call(shot, coord)
55
+ end
56
+
57
+ private def tip?(shot)
58
+ return reset! unless shot == grid.class::MISS
59
+ @misses += 1
60
+ @misses >= TIP_THRESHOLD
61
+ end
62
+
63
+ private def reset!
64
+ @misses = 0
65
+ false
66
+ end
67
+
68
+ private def place(ship, pos_class=Position)
69
+ @output.puts(PLACE_MSG.call(ship))
70
+ @pos ||= STDIN.gets
71
+ ship.place(pos_class.factory(@pos.chomp))
72
+ grid << ship
73
+ @pos = nil
74
+ rescue grid.class::PlacingError, pos_class::FactoryError => e
75
+ @output.puts(e.message)
76
+ @pos = nil
77
+ retry
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,31 @@
1
+ require "battleship/coord"
2
+
3
+ module Battleship
4
+ class Position
5
+ class CardinalError < StandardError; end
6
+ class FactoryError < StandardError; end
7
+
8
+ def self.factory(data)
9
+ new(*data.split(" "))
10
+ rescue ArgumentError
11
+ fail FactoryError.new("position must be specified as 'x y cardinal' (ie 1 2 N)")
12
+ end
13
+
14
+ attr_reader :coord
15
+ attr_accessor :cardinal
16
+
17
+ def initialize(x, y, cardinal, coord_class=Coord)
18
+ @coord = coord_class.new(x, y)
19
+ @cardinal = check(cardinal)
20
+ end
21
+
22
+ def to_s
23
+ "#{coord} #{cardinal}"
24
+ end
25
+
26
+ private def check(cardinal)
27
+ return cardinal if CARDINALS.include?(cardinal)
28
+ fail CardinalError.new("#{cardinal} is not a valid cardinal point")
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,63 @@
1
+ require "forwardable"
2
+
3
+ module Battleship
4
+ class Ship
5
+ extend Forwardable
6
+
7
+ SMALL_LEN = 3
8
+ LARGE_LEN = 4
9
+ WIDTH = 1
10
+
11
+ def_delegators :@footprint, :empty?
12
+
13
+ def self.small
14
+ new(SMALL_LEN, WIDTH, "small (#{SMALL_LEN}x#{WIDTH})")
15
+ end
16
+
17
+ def self.large
18
+ new(LARGE_LEN, WIDTH, "large (#{LARGE_LEN}x#{WIDTH})")
19
+ end
20
+
21
+ attr_accessor :name, :len, :wid, :footprint
22
+
23
+ def initialize(len, wid, name=nil)
24
+ @len = len.to_i
25
+ @wid = wid.to_i
26
+ @name = name.to_s
27
+ @footprint = []
28
+ end
29
+
30
+ def to_s
31
+ "len=#{len} wid=#{wid} footprint=(#{footprint.map(&:to_s).join(', ')})"
32
+ end
33
+
34
+ def place(position)
35
+ coord = position.coord
36
+ @footprint = case position.cardinal
37
+ when 'N'
38
+ len.times.map { |i| coord.class.new(coord.x, coord.y + i) }
39
+ when 'E'
40
+ len.times.map { |i| coord.class.new(coord.x + i, coord.y) }
41
+ when 'S'
42
+ len.times.map { |i| coord.class.new(coord.x, coord.y - i) }
43
+ when 'W'
44
+ len.times.map { |i| coord.class.new(coord.x - i, coord.y) }
45
+ when 'NE'
46
+ len.times.map { |i| coord.class.new(coord.x + i, coord.y + i) }
47
+ when 'NW'
48
+ len.times.map { |i| coord.class.new(coord.x - i, coord.y + i) }
49
+ when 'SE'
50
+ len.times.map { |i| coord.class.new(coord.x + i, coord.y - i) }
51
+ when 'SW'
52
+ len.times.map { |i| coord.class.new(coord.x - i, coord.y - i) }
53
+ end
54
+ end
55
+
56
+ def strike(coord)
57
+ hit = @footprint.delete(coord)
58
+ return -1 if empty?
59
+ return 1 if hit
60
+ 0
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ module Battleship
2
+ VERSION = "1.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rb_battleship
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - costajob
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-10-12 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.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '12.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '12.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.1'
55
+ description:
56
+ email:
57
+ - costajob@gmail.com
58
+ executables:
59
+ - battleship
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".gitignore"
64
+ - Gemfile
65
+ - README.md
66
+ - Rakefile
67
+ - battleship.gemspec
68
+ - bin/battleship
69
+ - bin/console
70
+ - bin/setup
71
+ - lib/battleship.rb
72
+ - lib/battleship/coord.rb
73
+ - lib/battleship/gameplay.rb
74
+ - lib/battleship/grid.rb
75
+ - lib/battleship/player.rb
76
+ - lib/battleship/position.rb
77
+ - lib/battleship/ship.rb
78
+ - lib/battleship/version.rb
79
+ homepage: https://github.com/costajob/battleship.git
80
+ licenses:
81
+ - MIT
82
+ metadata: {}
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: 2.3.1
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubyforge_project:
99
+ rubygems_version: 2.5.1
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Implementation of the Battleship code kata
103
+ test_files: []