1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
|
# frozen_string_literal: true
require "io/console"
require "game_of_life/version"
require "game_of_life/plane"
require "game_of_life/universe"
require "game_of_life/cell"
# Game of Life module entry point
module GameOfLife
# Base Error class
class Error < StandardError; end
class << self
# Validate and parse the CLI options and set defaults for the dynamic options.
# @param options [Hash] CLI options to generate initial conditions
# @option options [String | nil] "input" path to a local file or URL to open
# @option options [Numeric | nil] "seed" number to use if no "input" is provided
# @option options [Numeric] "width" of the universe
# @option options [Numeric] "height" of the universe
# @option options [Numeric] "delay" introduced after each cycle
# @option options [String] "live-cell" representation
# @option options [String] "dead-cell" representation
# @smell AbcSize, MethodLenght, Cyclomatic and Perceived Complexity needed for parsing and validating options
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
# return [Hash] of the parsed options with dynamic defaults based on window size and kernel random
def parsed_options(options)
max_height = IO.console.winsize[0] - 2
max_width = IO.console.winsize[1]
if options["delay"].to_i.negative?
fail GameOfLife::Error, "Invalid --delay value. must be positive value (sadly this is how time works -so far-)"
end
if (options["width"]&.< 1) || (options["width"]&.> max_width)
fail GameOfLife::Error, "Invalid --width value. " \
"must be between 1 and #{max_width} (current terminal width)"
end
if (options["height"]&.< 1) || (options["height"]&.> max_height)
fail GameOfLife::Error, "Invalid --height value. " \
"must be between 1 and #{max_height} (current terminal height)"
end
fail GameOfLife::Error, "Invalid --live-cell value. must be a single character" if options["live-cell"].length > 1
fail GameOfLife::Error, "Invalid --dead-cell value. must be a single character" if options["dead-cell"].length > 1
if options["dead-cell"] == options["live-cell"]
fail GameOfLife::Error, "Invalid --dead-cell value. must be a different character than --live-cell"
end
{
# 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" => max_height,
"width" => max_width,
"seed" => rand(100_000),
}.merge(options)
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
# Generate a universe based on the options (from an input file/URI) or using a random/passed seed
# This method also sets the Classes/Modules constants for the run defining the options for
# Cell (Dead/Live) state representations, and the delay introduced after rendering each universe/generation
# and the banner for the info/options
#
# @param options [Hash] CLI options to generate initial conditions
# @option options [String | nil] "input" path to a local file or URL to open
# @option options [Numeric] "seed" number to use if no "input" is provided (defaults to random)
# @option options [Numeric] "width" of the universe
# @option options [Numeric] "height" of the universe
# @option options [Numeric] "delay" introduced after each cycle
# @option options [String] "live-cell" representation
# @option options [String] "dead-cell" representation
# @return [GameOfLife::Universe] populated with the parsed input/seed
def generate(options)
# Define the constants for Cell representation
GameOfLife::Cell.const_set(:LIVE_CELL, options["live-cell"])
GameOfLife::Cell.const_set(:DEAD_CELL, options["dead-cell"])
# Define the constant used for delay
GameOfLife.const_set(:DELAY, options["delay"].to_f)
banner = options["input"] ? "Input: #{options['input']}" : "Seed: #{options['seed']}"
# Define the constant used for banner info
GameOfLife.const_set(:BANNER, banner)
return GameOfLife::Generators::Input.new(options) if options["input"]
GameOfLife::Generators::Seed.new(options)
end
# The infinite running loop for rendering the current state of a universe
# and then evolving it and repeating.
def run(universe)
Kernel.loop do
GameOfLife.render(universe)
universe.evolve!
end
end
# render method to do the terminal screen/scrollback buffer cleaning
# and rendering of the current universe/banner info
# based on the delay/cell configured by the user
def render(universe)
# Use Alternate Screen (\e[?1049h) and move cursor to top left (\e[H)
# Ref: https://github.com/ruby/ruby/blob/ruby_2_7/lib/irb/easter-egg.rb#L112-L131
# Ref: https://stackoverflow.com/questions/11023929/using-the-alternate-screen-in-a-bash-script
# Ref: https://superuser.com/questions/122911/what-commands-can-i-use-to-reset-and-clear-my-terminal
print "\e[?1049h\e[H"
print universe.to_s
print BANNER
sleep(DELAY / 1000.0)
end
end
# Generators module for generating the universe initial state
module Generators
autoload :Input, "game_of_life/generators/input.rb"
autoload :Seed, "game_of_life/generators/seed.rb"
end
end
|