From 780f85fc9d729735b192db866ad9a19484dc5743 Mon Sep 17 00:00:00 2001 From: a14m Date: Thu, 10 Dec 2020 21:14:19 +0100 Subject: Add the stable seed generator implementation --- CLI/ruby/lib/game_of_life/generators/seed.rb | 37 +++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/CLI/ruby/lib/game_of_life/generators/seed.rb b/CLI/ruby/lib/game_of_life/generators/seed.rb index e64a28f..917c9af 100644 --- a/CLI/ruby/lib/game_of_life/generators/seed.rb +++ b/CLI/ruby/lib/game_of_life/generators/seed.rb @@ -12,7 +12,42 @@ module GameOfLife # @option options [Numeric] "height" (floored) and used as the height of the universe # @return [GameOfLife::Universe] populated with the parsed input 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>] covering the defined size of universe + # @see ::GameOfLife::Generators::Seed.populate + 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 8 cells of binary data (ex. "01101011") + raw = Random.new(seed).bytes(width * height / 2).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>] + # @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(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 -- cgit v1.2.3 From db5499043df24548d2a6a23e45f23daae8de43ee Mon Sep 17 00:00:00 2001 From: a14m Date: Thu, 10 Dec 2020 21:15:53 +0100 Subject: Add the seed generator testing --- CLI/ruby/spec/game_of_life/generators/seed_spec.rb | 88 +++++++++++++++++++++- 1 file changed, 85 insertions(+), 3 deletions(-) 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 -- cgit v1.2.3 From b795db0ff7cd09de33cb3b4133563c7971e2fbc2 Mon Sep 17 00:00:00 2001 From: a14m Date: Thu, 10 Dec 2020 21:35:07 +0100 Subject: Refactor input generator (and update docs) --- CLI/ruby/lib/game_of_life/generators/input.rb | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) 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>] 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>] + # @param input [Array>] # @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 -- cgit v1.2.3 From 8928e27b37624babab9a7cd570a1349d3c850d03 Mon Sep 17 00:00:00 2001 From: a14m Date: Thu, 10 Dec 2020 21:39:09 +0100 Subject: Update yard docs and references --- CLI/ruby/lib/game_of_life/generators/seed.rb | 8 ++++++-- CLI/ruby/lib/game_of_life/version.rb | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CLI/ruby/lib/game_of_life/generators/seed.rb b/CLI/ruby/lib/game_of_life/generators/seed.rb index 917c9af..22be789 100644 --- a/CLI/ruby/lib/game_of_life/generators/seed.rb +++ b/CLI/ruby/lib/game_of_life/generators/seed.rb @@ -11,20 +11,23 @@ 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) 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) + 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 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>] 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 @@ -38,6 +41,7 @@ module GameOfLife # @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) 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 -- cgit v1.2.3 From 114b36f8e9ea06e05bf5ea005599f2c31b0aff1b Mon Sep 17 00:00:00 2001 From: a14m Date: Thu, 10 Dec 2020 22:05:10 +0100 Subject: Update the seed generator test --- CLI/ruby/spec/game_of_life_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 -- cgit v1.2.3 From 26b2e1f4d805d852d455e0c55709a948df83c45d Mon Sep 17 00:00:00 2001 From: a14m Date: Thu, 10 Dec 2020 22:08:18 +0100 Subject: Update the game-of-life binary to check max width/height And raise an error if the requested width/height greater than the maximum provided by console.winsize (to avoid rendering multi-lines) Since the terminal cannot scroll with bigger width it's better to limit the max configurations to the max allowed without scrolling --- CLI/ruby/bin/game-of-life | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) 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 -- cgit v1.2.3 From 83b680f34e65d986d53bb6f58ef7f877651ed06c Mon Sep 17 00:00:00 2001 From: a14m Date: Thu, 10 Dec 2020 22:15:57 +0100 Subject: Update CHANGELOG --- CLI/ruby/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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 -- cgit v1.2.3 From 88ec97f698f5ba68ad2a4be0f45cefa24a3f8934 Mon Sep 17 00:00:00 2001 From: a14m Date: Thu, 10 Dec 2020 22:24:37 +0100 Subject: Remove size optimization --- CLI/ruby/lib/game_of_life/generators/seed.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLI/ruby/lib/game_of_life/generators/seed.rb b/CLI/ruby/lib/game_of_life/generators/seed.rb index 22be789..d991efe 100644 --- a/CLI/ruby/lib/game_of_life/generators/seed.rb +++ b/CLI/ruby/lib/game_of_life/generators/seed.rb @@ -31,8 +31,8 @@ module GameOfLife 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 8 cells of binary data (ex. "01101011") - raw = Random.new(seed).bytes(width * height / 2).unpack1("B*").split("").each_slice(width) + # 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 -- cgit v1.2.3