summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authora14m <[email protected]>2020-12-10 21:31:16 +0000
committera14m <[email protected]>2020-12-10 21:31:16 +0000
commitfe3c74d73728de8c732af6982aad2d19e3f61373 (patch)
tree35f07cdcf823d03316e803d79bd25d8a7272a378
parentb4e9a712549dba1326aa680157574e946e1faa85 (diff)
parent88ec97f698f5ba68ad2a4be0f45cefa24a3f8934 (diff)
Merge branch 'feature/ruby/seed' into 'master'
Feature/ruby/seed See merge request a14m/game-of-life!6
-rw-r--r--CLI/ruby/CHANGELOG.md1
-rwxr-xr-xCLI/ruby/bin/game-of-life21
-rw-r--r--CLI/ruby/lib/game_of_life/generators/input.rb23
-rw-r--r--CLI/ruby/lib/game_of_life/generators/seed.rb41
-rw-r--r--CLI/ruby/lib/game_of_life/version.rb1
-rw-r--r--CLI/ruby/spec/game_of_life/generators/seed_spec.rb88
-rw-r--r--CLI/ruby/spec/game_of_life_spec.rb5
7 files changed, 165 insertions, 15 deletions
diff --git a/CLI/ruby/CHANGELOG.md b/CLI/ruby/CHANGELOG.md
index f436b8f..fd1663c 100644
--- a/CLI/ruby/CHANGELOG.md
+++ b/CLI/ruby/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## Unreleased
+- Support `--seed` option for populating the universe with pre-state
## 0.1.2
- Identical to v0.1.1, but fixes the gitlab release workflow
diff --git a/CLI/ruby/bin/game-of-life b/CLI/ruby/bin/game-of-life
index 0230271..f49682e 100755
--- a/CLI/ruby/bin/game-of-life
+++ b/CLI/ruby/bin/game-of-life
@@ -20,10 +20,10 @@ class CLI < Thor
class_option "input", aliases: "-i", type: :string, banner: :VALUE,
desc: "Specify the path/URL for the file to use as an initial state. (used instead of seed)"
- class_option "width", aliases: "-w", type: :numeric, banner: :WIDTH,
+ class_option "width", type: :numeric, banner: :WIDTH,
desc: "Specify the width of generated universe. (default to terminal width)"
- class_option "height", aliases: "-h", type: :numeric, banner: :HEIGHT,
+ class_option "height", type: :numeric, banner: :HEIGHT,
desc: "Specify the hight of generated universe. (default to terminal height)"
class_option "dead-cell", type: :string, banner: :CHAR, default: "\s",
@@ -34,18 +34,31 @@ class CLI < Thor
class_option "delay", aliases: "-d", type: :numeric, banner: "Milli-Seconds", default: 50,
desc: "Specify the introduced delay between each generation"
+ # rubocop:disable Metrics/AbcSize
+ # rubocop:disable Metrics/MethodLength
def start
+ max_height = IO.console.winsize[0] - 2
+ max_width = IO.console.winsize[1]
+ if options["width"]&.> max_width
+ fail GameOfLife::Error, "Invalid --width value, must not exceed current terminal width: #{max_width}"
+ end
+ if options["height"]&.> max_height
+ fail GameOfLife::Error, "Invalid --height value, must not exceed current terminal height: #{max_height}"
+ end
+
options_with_console_defaults = {
# Defaults the hight to less then the height of the terminal to allow banner info
# and an extra emtpy line to avoid triggering terminal scroll while flushing
- "height" => IO.console.winsize[0] - 3,
- "width" => IO.console.winsize[1],
+ "height" => max_height,
+ "width" => max_width,
"seed" => rand(100_000),
}.merge(options)
universe = GameOfLife.generate(options_with_console_defaults)
GameOfLife.run(universe)
end
+ # rubocop:enable Metrics/AbcSize
+ # rubocop:enable Metrics/MethodLength
class << self
# Allow exit with status 1 on failure
diff --git a/CLI/ruby/lib/game_of_life/generators/input.rb b/CLI/ruby/lib/game_of_life/generators/input.rb
index 79626d4..bb43e3d 100644
--- a/CLI/ruby/lib/game_of_life/generators/input.rb
+++ b/CLI/ruby/lib/game_of_life/generators/input.rb
@@ -14,15 +14,27 @@ module GameOfLife
# @option options [Numeric] "width" (floored) and used as the width of the universe
# @option options [Numeric] "height" (floored) and used as the height of the universe
# @return [GameOfLife::Universe] populated with the parsed input
+ # @see ::GameOfLife::Generators::Input.generate_input_data
# @see ::GameOfLife::Generators::Input.populate
def new(options)
+ width = options["width"].to_i
+ height = options["height"].to_i
+ uri = options["input"]
+ input = generate_input_data(uri: uri)
+ populate(input: input, width: width, height: height)
+ end
+
+ # Generate the seeding data for populating the {GameOfLife::Universe} from input URI
+ # @param uri [String] inputh path to local file or URL to open
+ # @return [Array<Array<True|False>>] covering the defined size of universe
+ # @see ::GameOfLife::Generators::Input.populate
+ private def generate_input_data(uri:)
# By design, it's intentional that we use URI.open to to allow seamless file and url access
- raw = URI.open(options["input"]).read.split("\n") # rubocop:disable Security/Open
+ raw = URI.open(uri).read.split("\n") # rubocop:disable Security/Open
# Parse the file and convert it to a boolean 2D array
# where any alpha numeric character is considered a living cell (true)
raw.map! { |row| row.chars.map { |char| char.match?(/[[:alnum:]]/) } }
- populate(parsed_input: raw, width: options["width"].to_i, height: options["height"].to_i)
rescue ::OpenURI::HTTPError, ::SocketError
raise GameOfLife::Error, "URL isn't avaialable, please check it again and make sure the URL is correct"
rescue ::Errno::ENOENT
@@ -31,16 +43,17 @@ module GameOfLife
end
# Populate the {GameOfLife::Universe} with reference to the parsed input 2D array (True/False)
- # @param parsed_input [Array<Array<True|False>>]
+ # @param input [Array<Array<True|False>>]
# @param width [Integer] width of the generated cyclic universe
# @param height [Integer] height of the generated cyclic universe
# @return [GameOfLife::Universe] populated with the parsed input
- private def populate(parsed_input:, width:, height:)
+ # @see ::GameOfLife::Generators::Input.generate_input_data
+ private def populate(input:, width:, height:)
# Generate a universe based on the parsed raw file and fill it with the cells
universe = GameOfLife::Universe.new(height: height, width: width)
(0...height).each do |i|
(0...width).each do |j|
- cell = { x: j, y: i, alive: parsed_input.fetch(i, nil)&.fetch(j, nil) }
+ cell = { x: j, y: i, alive: input.fetch(i, nil)&.fetch(j, nil) }
universe[i][j] = ::GameOfLife::Cell.new(cell)
end
end
diff --git a/CLI/ruby/lib/game_of_life/generators/seed.rb b/CLI/ruby/lib/game_of_life/generators/seed.rb
index e64a28f..d991efe 100644
--- a/CLI/ruby/lib/game_of_life/generators/seed.rb
+++ b/CLI/ruby/lib/game_of_life/generators/seed.rb
@@ -11,8 +11,47 @@ module GameOfLife
# @option options [Numeric] "width" (floored) and used as the width of the universe
# @option options [Numeric] "height" (floored) and used as the height of the universe
# @return [GameOfLife::Universe] populated with the parsed input
+ # @see ::GameOfLife::Generators::Seed.generate_seed_data
+ # @see ::GameOfLife::Generators::Seed.populate
def new(options)
- fail ::NotImplementedError, "Seed option isn't implemented yet, please use --input option"
+ width = options["width"].to_i
+ height = options["height"].to_i
+ seed = options["seed"].to_i
+ input = generate_seed_data(seed: seed, width: width, height: height)
+ populate(input: input, width: width, height: height)
+ end
+
+ # Generate the seeding data for populating the {GameOfLife::Universe}
+ # @param seed [Integer] used to generate {Array<"0"|"1">} using Std ruby Random
+ # @param width [Integer] width of the generated cyclic universe
+ # @param height [Integer] height of the generated cyclic universe
+ # @return [Array<Array<True|False>>] covering the defined size of universe
+ # @see ::GameOfLife::Generators::Seed.populate
+ # @see https://ruby-doc.org/core-2.4.0/Random.html
+ private def generate_seed_data(seed:, width:, height:)
+ # Create a pseudo(stable) random generator from the seed
+ # Generate a sequence of bytes to cover the Game of Life Universe plane
+ # each byte can be unpacked into binary data string (ex. "01101011")
+ raw = Random.new(seed).bytes(width * height).unpack1("B*").split("").each_slice(width)
+ raw.map { |row| row.map { |char| char.eql?("1") } }
+ end
+
+ # Populate the {GameOfLife::Universe} with reference to the parsed input 2D array (True/False)
+ # @param input [Array<Array<True|False>>]
+ # @param width [Integer] width of the generated cyclic universe
+ # @param height [Integer] height of the generated cyclic universe
+ # @return [GameOfLife::Universe] populated with the parsed input
+ # @see ::GameOfLife::Generators::Seed.generate_seed_data
+ private def populate(input:, width:, height:)
+ # Generate a universe based on the parsed raw file and fill it with the cells
+ universe = GameOfLife::Universe.new(height: height, width: width)
+ (0...height).each do |i|
+ (0...width).each do |j|
+ cell = { x: j, y: i, alive: input.fetch(i, nil)&.fetch(j, nil) }
+ universe[i][j] = ::GameOfLife::Cell.new(cell)
+ end
+ end
+ universe
end
end
end
diff --git a/CLI/ruby/lib/game_of_life/version.rb b/CLI/ruby/lib/game_of_life/version.rb
index 5fd800a..1c9f528 100644
--- a/CLI/ruby/lib/game_of_life/version.rb
+++ b/CLI/ruby/lib/game_of_life/version.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
module GameOfLife
+ # Game of life version number
VERSION = "0.1.2"
end
diff --git a/CLI/ruby/spec/game_of_life/generators/seed_spec.rb b/CLI/ruby/spec/game_of_life/generators/seed_spec.rb
index 17037e6..b9f9ed2 100644
--- a/CLI/ruby/spec/game_of_life/generators/seed_spec.rb
+++ b/CLI/ruby/spec/game_of_life/generators/seed_spec.rb
@@ -2,9 +2,91 @@
RSpec.describe GameOfLife::Generators::Seed do
describe ".new" do
- it "raises NotImplementedError" do
- options = { "seed" => 1337, "width" => 1, "height" => 1 }
- expect { described_class.new(options) }.to raise_error(NotImplementedError)
+ it "returns a GameOfLife::Universe" do
+ options = { "seed" => 1, "width" => 1, "height" => 1 }
+ expect(described_class.new(options)).to be_a GameOfLife::Universe
+ end
+
+ context "when seed => 1" do
+ it "returns the same universe for width => 10, height => 1" do
+ options = { "seed" => 1, "width" => 10, "height" => 1 }
+ stub_const("GameOfLife::Cell::LIVE_CELL", "X")
+ stub_const("GameOfLife::Cell::DEAD_CELL", ".")
+
+ universe = "..X..X.XXX"
+ expect(described_class.new(options).to_s).to eq universe
+ end
+
+ it "returns the same universe for width => 20, height => 1" do
+ options = { "seed" => 1, "width" => 20, "height" => 1 }
+ stub_const("GameOfLife::Cell::LIVE_CELL", "X")
+ stub_const("GameOfLife::Cell::DEAD_CELL", ".")
+
+ universe = "..X..X.XXXXX.X..XX.."
+ expect(described_class.new(options).to_s).to eq universe
+ end
+
+ it "returns the same universe for width => 20, height => 2" do
+ options = { "seed" => 1, "width" => 20, "height" => 2 }
+ stub_const("GameOfLife::Cell::LIVE_CELL", "X")
+ stub_const("GameOfLife::Cell::DEAD_CELL", ".")
+
+ universe = "..X..X.XXXXX.X..XX..\n" \
+ "...X.XX.X.X.XXX.X.XX"
+ expect(described_class.new(options).to_s).to eq universe
+ end
+
+ it "returns the same universe for width => 20, height => 3" do
+ options = { "seed" => 1, "width" => 20, "height" => 3 }
+ stub_const("GameOfLife::Cell::LIVE_CELL", "X")
+ stub_const("GameOfLife::Cell::DEAD_CELL", ".")
+
+ universe = "..X..X.XXXXX.X..XX..\n" \
+ "...X.XX.X.X.XXX.X.XX\n" \
+ "X........X...XXXXXXX"
+ expect(described_class.new(options).to_s).to eq universe
+ end
+ end
+
+ context "when seed => 2" do
+ it "returns the same universe for width => 10, height => 1" do
+ options = { "seed" => 2, "width" => 10, "height" => 1 }
+ stub_const("GameOfLife::Cell::LIVE_CELL", "X")
+ stub_const("GameOfLife::Cell::DEAD_CELL", ".")
+
+ universe = "X.X.X....X"
+ expect(described_class.new(options).to_s).to eq universe
+ end
+
+ it "returns the same universe for width => 20, height => 1" do
+ options = { "seed" => 2, "width" => 20, "height" => 1 }
+ stub_const("GameOfLife::Cell::LIVE_CELL", "X")
+ stub_const("GameOfLife::Cell::DEAD_CELL", ".")
+
+ universe = "X.X.X....X.XXX..X..X"
+ expect(described_class.new(options).to_s).to eq universe
+ end
+
+ it "returns the same universe for width => 20, height => 2" do
+ options = { "seed" => 2, "width" => 20, "height" => 2 }
+ stub_const("GameOfLife::Cell::LIVE_CELL", "X")
+ stub_const("GameOfLife::Cell::DEAD_CELL", ".")
+
+ universe = "X.X.X....X.XXX..X..X\n" \
+ "XX.X.XX.XXXX....XXXX"
+ expect(described_class.new(options).to_s).to eq universe
+ end
+
+ it "returns the same universe for width => 20, height => 3" do
+ options = { "seed" => 2, "width" => 20, "height" => 3 }
+ stub_const("GameOfLife::Cell::LIVE_CELL", "X")
+ stub_const("GameOfLife::Cell::DEAD_CELL", ".")
+
+ universe = "X.X.X....X.XXX..X..X\n" \
+ "XX.X.XX.XXXX....XXXX\n" \
+ "X...X.X..XX....X..X."
+ expect(described_class.new(options).to_s).to eq universe
+ end
end
end
end
diff --git a/CLI/ruby/spec/game_of_life_spec.rb b/CLI/ruby/spec/game_of_life_spec.rb
index b4c1710..6564327 100644
--- a/CLI/ruby/spec/game_of_life_spec.rb
+++ b/CLI/ruby/spec/game_of_life_spec.rb
@@ -48,11 +48,12 @@ RSpec.describe GameOfLife do
described_class.generate(options)
end
- it "calls Generators::Seed.new (and raises error)" do
+ it "calls Generators::Seed.new" do
allow(described_class::Cell).to receive(:const_set)
allow(described_class).to receive(:const_set)
- expect { described_class.generate(options) }.to raise_error(NotImplementedError)
+ expect(described_class::Generators::Seed).to receive(:new).with(options)
+ described_class.generate(options)
end
end
end