nlife 0.0.1 → 0.1.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/TODO.md +4 -3
- data/bin/nlife +1 -1
- data/lib/nlife.rb +4 -1
- data/lib/nlife/game.rb +31 -19
- data/lib/nlife/helper.rb +27 -14
- data/lib/nlife/ui.rb +9 -11
- data/lib/nlife/version.rb +1 -1
- data/nlife.gemspec +11 -9
- data/spec/nlife/game_spec.rb +93 -0
- data/spec/nlife/helper_spec.rb +51 -0
- data/spec/nlife/ui_spec.rb +0 -0
- data/spec/nlife/version_spec.rb +3 -0
- data/spec/spec_helper.rb +11 -0
- metadata +36 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9c6e3e08c39d85b040029e683f60d34729949db3
|
4
|
+
data.tar.gz: bd91c74cf8e82a5eee043f09fd978e88f4ac091f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9dc92b0822b1364a375792da53deb4b51a9351aa7b945d1b4f1f96d30973ce62afb1b29ec8d6508134a49030431b5971ff2adbcd671534695d8426e3eb18f1b3
|
7
|
+
data.tar.gz: aaa56266f27f2cd2cfe44df5961fb8925291653c711fde9ea5a95cc417537bd561df3695446e77187eab0b07aba44e0c8bd679e6c542193ed2cdcc7f610dc8f7
|
data/CHANGELOG.md
ADDED
data/TODO.md
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
TODO
|
2
2
|
====
|
3
3
|
|
4
|
-
### Tests
|
5
|
-
- add rspec tests
|
6
|
-
|
7
4
|
### Game features
|
8
5
|
- resizing grid
|
9
6
|
- load/save file
|
10
7
|
- set cell state
|
11
8
|
- yank/paste/clear region
|
9
|
+
- set speed
|
10
|
+
- status line showing:
|
11
|
+
- iteration
|
12
|
+
- speed
|
12
13
|
|
13
14
|
### Render features
|
14
15
|
- make render a separate class
|
data/bin/nlife
CHANGED
data/lib/nlife.rb
CHANGED
data/lib/nlife/game.rb
CHANGED
@@ -1,30 +1,31 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'narray'
|
2
|
+
require 'numru/fftw3'
|
3
3
|
include NumRu
|
4
4
|
|
5
|
-
require_relative
|
5
|
+
require_relative 'helper.rb'
|
6
6
|
|
7
7
|
module NLife
|
8
|
+
# encapsulates game logic
|
8
9
|
class Game
|
9
10
|
attr_reader :state
|
10
|
-
|
11
|
+
attr_reader :density
|
11
12
|
|
12
13
|
def initialize(rows, cols, surround = nil, rule = nil)
|
13
|
-
@state = NArray.
|
14
|
-
@density = NArray.
|
14
|
+
@state = NArray.float(cols, rows)
|
15
|
+
@density = NArray.float(cols, rows)
|
15
16
|
surround ||= default_surround(rows, cols)
|
16
|
-
@
|
17
|
+
@fft_surround = FFTW3.fft(surround.to_f, -1) / surround.size
|
17
18
|
@rule = rule || default_rule
|
18
19
|
end
|
19
20
|
|
20
21
|
def default_surround(rows, cols)
|
21
|
-
|
22
|
-
[row.abs, col.abs].max == 1
|
22
|
+
Helper.surround_from_block(rows, cols) do |row, col|
|
23
|
+
[row.abs, col.abs].max == 1 ? 1 : 0
|
23
24
|
end
|
24
25
|
end
|
25
26
|
|
26
27
|
def default_rule
|
27
|
-
|
28
|
+
Helper.rule_from_golly('B3/S23')
|
28
29
|
end
|
29
30
|
|
30
31
|
def seed
|
@@ -35,14 +36,16 @@ module NLife
|
|
35
36
|
end
|
36
37
|
end
|
37
38
|
|
38
|
-
def step
|
39
|
-
|
40
|
-
|
39
|
+
def step(count = 1)
|
40
|
+
count.times do
|
41
|
+
calc_density
|
42
|
+
calc_next_state
|
43
|
+
end
|
41
44
|
end
|
42
45
|
|
43
46
|
def calc_density
|
44
47
|
t_state = FFTW3.fft(@state, -1)
|
45
|
-
t_density = t_state * @
|
48
|
+
t_density = t_state * @fft_surround
|
46
49
|
@density = FFTW3.fft(t_density, 1).real.round
|
47
50
|
end
|
48
51
|
|
@@ -56,11 +59,20 @@ module NLife
|
|
56
59
|
|
57
60
|
# use for debug only
|
58
61
|
def print
|
59
|
-
puts
|
62
|
+
puts '#' * (@state.shape[0] + 2)
|
60
63
|
@state.shape[1].times do |i|
|
61
|
-
|
64
|
+
print_row(i)
|
62
65
|
end
|
63
|
-
puts
|
66
|
+
puts '#' * (@state.shape[0] + 2)
|
67
|
+
end
|
68
|
+
|
69
|
+
# use for debug only
|
70
|
+
def print_row(row)
|
71
|
+
puts '#' + @state[true, row].each.map { |e| e > 0 ? '*' : ' ' }.join + '#'
|
72
|
+
end
|
73
|
+
|
74
|
+
def surround
|
75
|
+
FFTW3.fft(@fft_surround, 1).real
|
64
76
|
end
|
65
77
|
|
66
78
|
def rows
|
@@ -70,5 +82,5 @@ module NLife
|
|
70
82
|
def cols
|
71
83
|
@state.shape[0]
|
72
84
|
end
|
73
|
-
end
|
74
|
-
end
|
85
|
+
end # class Game
|
86
|
+
end # module NLife
|
data/lib/nlife/helper.rb
CHANGED
@@ -1,34 +1,47 @@
|
|
1
|
-
require
|
1
|
+
require 'narray'
|
2
2
|
|
3
3
|
module NLife
|
4
|
+
# miscellaneous functions for state/surround gereration
|
4
5
|
module Helper
|
5
|
-
|
6
|
-
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# surround is calculated for rectangle centered at (0, 0)
|
9
|
+
def surround_from_block(rows, cols)
|
10
|
+
result = NArray.float(cols, rows)
|
7
11
|
result.shape[1].times do |row|
|
8
12
|
result.shape[0].times do |col|
|
9
13
|
result[col, row] = yield((row + rows / 2) % rows - rows / 2,
|
10
|
-
(col + cols / 2) % cols - cols / 2)
|
14
|
+
(col + cols / 2) % cols - cols / 2)
|
11
15
|
end
|
12
16
|
end
|
13
17
|
result
|
14
18
|
end
|
15
19
|
|
16
|
-
# constructs rules from two arrays,
|
17
|
-
def
|
20
|
+
# constructs rules from two arrays: birth, survival densities
|
21
|
+
def rule_from_arrays(birth, survival)
|
22
|
+
unless birth.size == survival.size
|
23
|
+
fail ArgumentError 'params sizes not match'
|
24
|
+
end
|
25
|
+
max_density = birth.size
|
18
26
|
proc do |state, density|
|
19
|
-
|
27
|
+
fail ArgumentError 'density too high' if density > max_density
|
28
|
+
result = (state == 0 ? birth : survival)[density.round]
|
29
|
+
result ? 1.0 : 0.0
|
20
30
|
end
|
21
31
|
end
|
22
32
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
birth = Array.new(9, false)
|
33
|
+
def golly_to_array(string)
|
34
|
+
match = %r{^B([0-8]*)\/S([0-8]*)$}.match(string)
|
35
|
+
raise ArgumentError 'wrong input format' unless match
|
36
|
+
birth = (0..8).map { |i| match[1].include? i.to_s }
|
28
37
|
survival = Array.new(9, false)
|
29
|
-
match[1].split(//).map(&:to_i).each { |i| birth[i] = true }
|
30
38
|
match[2].split(//).map(&:to_i).each { |i| survival[i] = true }
|
31
|
-
return
|
39
|
+
return birth, survival
|
40
|
+
end
|
41
|
+
|
42
|
+
# constructs rules from string, describing birth/survival densities
|
43
|
+
def rule_from_golly(string)
|
44
|
+
rule_from_arrays(*golly_to_array(string))
|
32
45
|
end
|
33
46
|
end
|
34
47
|
end
|
data/lib/nlife/ui.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
require 'curses'
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative 'game.rb'
|
4
4
|
|
5
5
|
module NLife
|
6
6
|
class UI
|
7
|
-
RENDER_DEAD =
|
7
|
+
RENDER_DEAD = ' '
|
8
8
|
RENDER_LIVE = "\u2588"
|
9
9
|
|
10
10
|
def initialize
|
@@ -20,7 +20,7 @@ module NLife
|
|
20
20
|
@window_lines = Curses.lines
|
21
21
|
@window_cols = Curses.cols
|
22
22
|
@window = Curses::Window.new(@window_lines, @window_cols, 0, 0)
|
23
|
-
@window.box(
|
23
|
+
@window.box('|', '-')
|
24
24
|
@window.timeout = 0
|
25
25
|
end
|
26
26
|
|
@@ -45,22 +45,20 @@ module NLife
|
|
45
45
|
|
46
46
|
def dispatch_key(key)
|
47
47
|
case key
|
48
|
-
when
|
49
|
-
when
|
50
|
-
when
|
48
|
+
when 'p' then @pause = !@pause
|
49
|
+
when 's' then @life.seed
|
50
|
+
when 'q' then return false
|
51
51
|
end
|
52
|
-
|
52
|
+
true
|
53
53
|
end
|
54
54
|
|
55
55
|
def step
|
56
|
-
unless @pause
|
57
|
-
@life.step
|
58
|
-
end
|
56
|
+
@life.step unless @pause
|
59
57
|
end
|
60
58
|
|
61
59
|
def render
|
62
60
|
@life.rows.times do |i|
|
63
|
-
string =
|
61
|
+
string = ''
|
64
62
|
@life.cols.times do |j|
|
65
63
|
string += @life.state[j, i] > 0 ? RENDER_LIVE : RENDER_DEAD
|
66
64
|
end
|
data/lib/nlife/version.rb
CHANGED
data/nlife.gemspec
CHANGED
@@ -4,17 +4,19 @@ Gem::Specification.new do |s|
|
|
4
4
|
s.name = 'nlife'
|
5
5
|
s.version = NLife::VERSION
|
6
6
|
s.licenses = ['LGPL-3.0']
|
7
|
-
s.authors = [
|
7
|
+
s.authors = ['Oleg Zubchenko']
|
8
8
|
s.email = 'RedGreenBlueDiamond@gmail.com'
|
9
|
-
s.homepage = '
|
10
|
-
s.summary =
|
11
|
-
s.description =
|
12
|
-
s.files = Dir[
|
13
|
-
|
9
|
+
s.homepage = 'https://github.com/rgbd/ruby-nlife'
|
10
|
+
s.summary = 'Generalized Game of Life'
|
11
|
+
s.description = 'Game of Life with customizable rules on ncurses viewer'
|
12
|
+
s.files = Dir['**/*.rb'] + Dir['bin/*'] + Dir['*.md'] +
|
13
|
+
Dir['*.gemspec']
|
14
14
|
s.executables << 'nlife'
|
15
15
|
|
16
16
|
s.add_runtime_dependency 'curses', '~> 1.0', '>= 1.0.1'
|
17
|
-
s.add_runtime_dependency 'narray', '~>0.6.1'
|
18
|
-
s.add_runtime_dependency 'numru-misc', '~>0.1.2'
|
19
|
-
s.add_runtime_dependency 'ruby-fftw3', '~>0.4.2'
|
17
|
+
s.add_runtime_dependency 'narray', '~> 0.6.1', '>= 0.6.1.1'
|
18
|
+
s.add_runtime_dependency 'numru-misc', '~> 0.1.2'
|
19
|
+
s.add_runtime_dependency 'ruby-fftw3', '~> 0.4.2'
|
20
|
+
|
21
|
+
s.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0'
|
20
22
|
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'narray'
|
2
|
+
|
3
|
+
describe NLife::Game do
|
4
|
+
before do
|
5
|
+
@rows = 8
|
6
|
+
@cols = 8
|
7
|
+
@game = described_class.new(@rows, @cols)
|
8
|
+
end
|
9
|
+
|
10
|
+
context 'classic defauts' do
|
11
|
+
before do
|
12
|
+
# classic surround
|
13
|
+
@surround = NArray.int(@rows, @cols)
|
14
|
+
@surround[(-1..1).to_a, (-1..1).to_a] = 1
|
15
|
+
@surround[0, 0] = 0
|
16
|
+
@rule_length = 8
|
17
|
+
# ruler------012345678
|
18
|
+
@birth = '...X.....'.split(//).map { |e| e == 'X' }
|
19
|
+
@survival = '..XX.....'.split(//).map { |e| e == 'X' }
|
20
|
+
@rule = proc do |s, d|
|
21
|
+
result = s == 0 ? @birth[d] : @survival[d]
|
22
|
+
result ? 1 : 0
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should have classic default surround' do
|
27
|
+
expect(@game.surround.round).to eq(@surround)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should have classic default rules' do
|
31
|
+
game_rule = @game.instance_variable_get(:@rule)
|
32
|
+
[0, 1].each do |s|
|
33
|
+
0.upto(8) do |d|
|
34
|
+
expect(@rule.call(s, d)).to eq(game_rule.call(s, d))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end # context 'classic defauts'
|
39
|
+
|
40
|
+
context 'stepping' do
|
41
|
+
before do
|
42
|
+
# glider
|
43
|
+
@state = NArray.to_na([
|
44
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
45
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
46
|
+
[0, 0, 0, 1, 0, 0, 0, 0],
|
47
|
+
[0, 0, 0, 0, 1, 0, 0, 0],
|
48
|
+
[0, 0, 1, 1, 1, 0, 0, 0],
|
49
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
50
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
51
|
+
[0, 0, 0, 0, 0, 0, 0, 0]
|
52
|
+
]).to_f
|
53
|
+
@density = NArray.to_na([
|
54
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
55
|
+
[0, 0, 1, 1, 1, 0, 0, 0],
|
56
|
+
[0, 0, 1, 1, 2, 1, 0, 0],
|
57
|
+
[0, 1, 3, 5, 3, 2, 0, 0],
|
58
|
+
[0, 1, 1, 3, 2, 2, 0, 0],
|
59
|
+
[0, 1, 2, 3, 2, 1, 0, 0],
|
60
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
61
|
+
[0, 0, 0, 0, 0, 0, 0, 0]
|
62
|
+
]).to_f
|
63
|
+
@next_state = NArray.to_na([
|
64
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
65
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
66
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
67
|
+
[0, 0, 1, 0, 1, 0, 0, 0],
|
68
|
+
[0, 0, 0, 1, 1, 0, 0, 0],
|
69
|
+
[0, 0, 0, 1, 0, 0, 0, 0],
|
70
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
71
|
+
[0, 0, 0, 0, 0, 0, 0, 0]
|
72
|
+
]).to_f
|
73
|
+
@game.instance_variable_set(:@state, @state)
|
74
|
+
@game.instance_variable_set(:@density, NArray.float(@rows, @cols))
|
75
|
+
end # before
|
76
|
+
|
77
|
+
it '.calc_density' do
|
78
|
+
@game.send(:calc_density)
|
79
|
+
expect(@game.instance_variable_get(:@density)).to eq(@density)
|
80
|
+
end
|
81
|
+
|
82
|
+
it '.calc_nest_state' do
|
83
|
+
@game.instance_variable_set(:@density, @density)
|
84
|
+
@game.send(:calc_next_state)
|
85
|
+
expect(@game.instance_variable_get(:@state)).to eq(@next_state)
|
86
|
+
end
|
87
|
+
|
88
|
+
it '.step' do
|
89
|
+
@game.step
|
90
|
+
expect(@game.instance_variable_get(:@state)).to eq(@next_state)
|
91
|
+
end
|
92
|
+
end # context 'stepping'
|
93
|
+
end # describe NLife::Game
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'narray'
|
2
|
+
|
3
|
+
describe NLife::Helper do
|
4
|
+
before do
|
5
|
+
@rows = 8
|
6
|
+
@cols = 8
|
7
|
+
end
|
8
|
+
|
9
|
+
context 'surround generation' do
|
10
|
+
before do
|
11
|
+
# classic surround
|
12
|
+
@surround = NArray.int(@rows, @cols)
|
13
|
+
@surround[(-1..1).to_a, (-1..1).to_a] = 1
|
14
|
+
@surround[0, 0] = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
it '.surround_from_block' do
|
18
|
+
shape = @surround.shape.reverse
|
19
|
+
got_surround = described_class.surround_from_block(*shape) do |row, col|
|
20
|
+
[row, col].map(&:abs).max == 1 ? 1 : 0
|
21
|
+
end
|
22
|
+
expect(got_surround).to eq(@surround)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'rule generation' do
|
27
|
+
before do
|
28
|
+
@rule_length = 8
|
29
|
+
# ruler------012345678
|
30
|
+
@birth = '...X..X..'.split(//).map { |e| e == 'X' }
|
31
|
+
@survival = '..XX.....'.split(//).map { |e| e == 'X' }
|
32
|
+
@golly = 'B36/S23'
|
33
|
+
end
|
34
|
+
|
35
|
+
it '.golly_to_array' do
|
36
|
+
expect(described_class.golly_to_array(@golly)).to eq([@birth, @survival])
|
37
|
+
end
|
38
|
+
|
39
|
+
it '.rule_from_arrays' do
|
40
|
+
rule = described_class.rule_from_arrays(@birth, @survival)
|
41
|
+
0.upto(@rule_length) do |i|
|
42
|
+
expect(rule.call(0, i) > 0).to eq(@birth[i])
|
43
|
+
expect(rule.call(1, i) > 0).to eq(@survival[i])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it '.rule_from_golly' do
|
48
|
+
expect(described_class).to respond_to(:rule_from_golly)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
File without changes
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative '../lib/nlife.rb'
|
2
|
+
|
3
|
+
RSpec.configure do |config|
|
4
|
+
config.expect_with :rspec do |expectations|
|
5
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
6
|
+
end
|
7
|
+
|
8
|
+
config.mock_with :rspec do |mocks|
|
9
|
+
mocks.verify_partial_doubles = true
|
10
|
+
end
|
11
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nlife
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Oleg Zubchenko
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-05-
|
11
|
+
date: 2015-05-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: curses
|
@@ -37,6 +37,9 @@ dependencies:
|
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: 0.6.1
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 0.6.1.1
|
40
43
|
type: :runtime
|
41
44
|
prerelease: false
|
42
45
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -44,6 +47,9 @@ dependencies:
|
|
44
47
|
- - "~>"
|
45
48
|
- !ruby/object:Gem::Version
|
46
49
|
version: 0.6.1
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 0.6.1.1
|
47
53
|
- !ruby/object:Gem::Dependency
|
48
54
|
name: numru-misc
|
49
55
|
requirement: !ruby/object:Gem::Requirement
|
@@ -72,13 +78,34 @@ dependencies:
|
|
72
78
|
- - "~>"
|
73
79
|
- !ruby/object:Gem::Version
|
74
80
|
version: 0.4.2
|
75
|
-
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: rspec
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '3.2'
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 3.2.0
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '3.2'
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: 3.2.0
|
101
|
+
description: Game of Life with customizable rules on ncurses viewer
|
76
102
|
email: RedGreenBlueDiamond@gmail.com
|
77
103
|
executables:
|
78
104
|
- nlife
|
79
105
|
extensions: []
|
80
106
|
extra_rdoc_files: []
|
81
107
|
files:
|
108
|
+
- CHANGELOG.md
|
82
109
|
- README.md
|
83
110
|
- TODO.md
|
84
111
|
- bin/nlife
|
@@ -88,7 +115,12 @@ files:
|
|
88
115
|
- lib/nlife/ui.rb
|
89
116
|
- lib/nlife/version.rb
|
90
117
|
- nlife.gemspec
|
91
|
-
|
118
|
+
- spec/nlife/game_spec.rb
|
119
|
+
- spec/nlife/helper_spec.rb
|
120
|
+
- spec/nlife/ui_spec.rb
|
121
|
+
- spec/nlife/version_spec.rb
|
122
|
+
- spec/spec_helper.rb
|
123
|
+
homepage: https://github.com/rgbd/ruby-nlife
|
92
124
|
licenses:
|
93
125
|
- LGPL-3.0
|
94
126
|
metadata: {}
|