rb_battleship 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []