eunomia_gen 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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +9 -0
- data/.zed/settings.json +11 -0
- data/.zed/tasks.json +43 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +157 -0
- data/Rakefile +22 -0
- data/exe/eunomia_gen +4 -0
- data/lib/eunomia/element.rb +30 -0
- data/lib/eunomia/function/capitalize.rb +18 -0
- data/lib/eunomia/function/downcase.rb +18 -0
- data/lib/eunomia/function/pluralize.rb +68 -0
- data/lib/eunomia/function/quote.rb +20 -0
- data/lib/eunomia/function/titleize.rb +31 -0
- data/lib/eunomia/function/upcase.rb +18 -0
- data/lib/eunomia/functions.rb +35 -0
- data/lib/eunomia/generator.rb +61 -0
- data/lib/eunomia/hash_helpers.rb +50 -0
- data/lib/eunomia/item.rb +51 -0
- data/lib/eunomia/request.rb +49 -0
- data/lib/eunomia/result.rb +90 -0
- data/lib/eunomia/segment/common.rb +23 -0
- data/lib/eunomia/segment/constant.rb +27 -0
- data/lib/eunomia/segment/dice.rb +65 -0
- data/lib/eunomia/segment/number.rb +23 -0
- data/lib/eunomia/segment/reference.rb +48 -0
- data/lib/eunomia/segment/text.rb +23 -0
- data/lib/eunomia/segment.rb +31 -0
- data/lib/eunomia/selector.rb +59 -0
- data/lib/eunomia/separator.rb +19 -0
- data/lib/eunomia/store.rb +37 -0
- data/lib/eunomia/version.rb +5 -0
- data/lib/eunomia.rb +54 -0
- data/sig/eunomia_gen.rbs +4 -0
- metadata +85 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eunomia
|
|
4
|
+
# Generator consists of a list of Items. When generating a result,
|
|
5
|
+
# an item is selected at random based on the item's weight.
|
|
6
|
+
class Generator
|
|
7
|
+
include Eunomia::HashHelpers
|
|
8
|
+
|
|
9
|
+
attr_reader :key,
|
|
10
|
+
:alts,
|
|
11
|
+
:meta,
|
|
12
|
+
:functions,
|
|
13
|
+
:tags,
|
|
14
|
+
:items,
|
|
15
|
+
:selector,
|
|
16
|
+
:sep
|
|
17
|
+
|
|
18
|
+
def initialize(hsh)
|
|
19
|
+
@key = field_or_raise(hsh, :key)
|
|
20
|
+
@functions = list_field(hsh, :functions)
|
|
21
|
+
@alts = hash_field(hsh, :alts)
|
|
22
|
+
@meta = meta_field(hsh)
|
|
23
|
+
@tags = tags_field(hsh)
|
|
24
|
+
@selector = Eunomia::Selector.new(field_or_nil(hsh, :rng))
|
|
25
|
+
@items = items_from(hsh)
|
|
26
|
+
raise "Generators must have items" if @items.empty?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def items_from(hsh)
|
|
30
|
+
list_field(hsh, :items).map do |item|
|
|
31
|
+
item = { segments: item } if item.is_a?(String)
|
|
32
|
+
Eunomia::Item.new(@key, item, @tags)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def alt_for(key, segment)
|
|
37
|
+
return segment unless key
|
|
38
|
+
return segment unless alts[key]
|
|
39
|
+
|
|
40
|
+
alts[key][segment] || segment
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Select items that have all the given tag values
|
|
44
|
+
def filter(tags)
|
|
45
|
+
return items if tags.empty?
|
|
46
|
+
|
|
47
|
+
items.select { |item| (tags - item.tags).empty? }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def generate(request)
|
|
51
|
+
items = filter(request.tags)
|
|
52
|
+
item = selector.select(items)
|
|
53
|
+
raise "No items found for #{key}" unless item
|
|
54
|
+
|
|
55
|
+
result = item.generate(request)
|
|
56
|
+
result.apply(alts, functions, locale: request.alt_key)
|
|
57
|
+
result.merge_meta(meta)
|
|
58
|
+
result
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eunomia
|
|
4
|
+
module HashHelpers
|
|
5
|
+
def field_or_nil(hsh, key)
|
|
6
|
+
hsh[key.to_sym] || hsh[key.to_s]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def field_or_raise(hsh, key)
|
|
10
|
+
val = field_or_nil(hsh, key)
|
|
11
|
+
raise "Missing key: #{key}" unless val
|
|
12
|
+
|
|
13
|
+
val
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def int_field(hsh, key, default = 0)
|
|
17
|
+
n = field_or_nil(hsh, key)
|
|
18
|
+
n.nil? ? default : n.to_i
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def list_field(hsh, key)
|
|
22
|
+
field = field_or_nil(hsh, key)
|
|
23
|
+
return [] unless field
|
|
24
|
+
|
|
25
|
+
field.is_a?(String) ? field.split(/\s+/) : field
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def tags_field(hsh)
|
|
29
|
+
tags = list_field(hsh, :tags)
|
|
30
|
+
return Set.new unless tags
|
|
31
|
+
|
|
32
|
+
Set.new(tags)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def alts_field(hsh)
|
|
36
|
+
hash_field(hsh, :alts)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def meta_field(hsh)
|
|
40
|
+
hash_field(hsh, :meta)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def hash_field(hsh, key)
|
|
44
|
+
field = field_or_nil(hsh, key)
|
|
45
|
+
return {} unless field
|
|
46
|
+
|
|
47
|
+
field.is_a?(Hash) ? field : {}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/eunomia/item.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eunomia
|
|
4
|
+
class Item
|
|
5
|
+
include Eunomia::HashHelpers
|
|
6
|
+
|
|
7
|
+
attr_reader :key,
|
|
8
|
+
:weight,
|
|
9
|
+
:value,
|
|
10
|
+
:tags,
|
|
11
|
+
:alts,
|
|
12
|
+
:meta,
|
|
13
|
+
:functions,
|
|
14
|
+
:segments
|
|
15
|
+
|
|
16
|
+
def initialize(key, hsh, add_tags = nil)
|
|
17
|
+
@key = key
|
|
18
|
+
@weight = int_field(hsh, :weight).clamp(1, 1000)
|
|
19
|
+
@value = int_field(hsh, :value).clamp(0, 1_000_000)
|
|
20
|
+
@tags = tags_field(hsh)
|
|
21
|
+
@tags += add_tags if add_tags
|
|
22
|
+
@alts = alts_field(hsh)
|
|
23
|
+
@meta = meta_field(hsh)
|
|
24
|
+
@functions = list_field(hsh, :functions)
|
|
25
|
+
@segments = scan(field_or_raise(hsh, :segments)).flatten
|
|
26
|
+
raise "Items must have segments" if @segments.empty?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def scan(obj)
|
|
30
|
+
return [] if obj.nil?
|
|
31
|
+
return Eunomia::Segment.build(obj.to_s) unless obj.is_a?(Array)
|
|
32
|
+
|
|
33
|
+
obj.map { |e| Eunomia::Segment.build(e) }.flatten
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def alt_for(key, segment)
|
|
37
|
+
return segment unless key
|
|
38
|
+
return segment unless alts[key]
|
|
39
|
+
|
|
40
|
+
alts[key][segment] || segment
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def generate(request)
|
|
44
|
+
result = Eunomia::Result.new(key, value: value)
|
|
45
|
+
segments.each { |seg| result.append(seg.generate(request)) }
|
|
46
|
+
result.apply(alts, functions, locale: request.alt_key)
|
|
47
|
+
result.merge_meta(meta)
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eunomia
|
|
4
|
+
class Request
|
|
5
|
+
attr_reader :key, :alt_key, :alts, :meta, :tags, :functions, :constants, :depth
|
|
6
|
+
|
|
7
|
+
def initialize(key, alts: {}, alt_key: nil, meta: {}, tags: [], functions: [], constants: {}, unique: false)
|
|
8
|
+
@key = key
|
|
9
|
+
@alt_key = alt_key
|
|
10
|
+
@alts = alts || {}
|
|
11
|
+
@meta = meta || {}
|
|
12
|
+
@tags = Set.new(tags || [])
|
|
13
|
+
@constants = constants || {}
|
|
14
|
+
@functions = functions || []
|
|
15
|
+
@depth = 0
|
|
16
|
+
@unique = unique ? Set.new : nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def generate_unique?
|
|
20
|
+
!@unique.nil?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def alt_key?
|
|
24
|
+
!alt_key.nil?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def alt_for(segment)
|
|
28
|
+
alts[segment] || segment
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def increase_depth
|
|
32
|
+
@depth += 1
|
|
33
|
+
raise "Depth exceeded" if @depth > 100
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def generate
|
|
37
|
+
@depth = 0
|
|
38
|
+
gen = Eunomia.lookup(key)
|
|
39
|
+
|
|
40
|
+
100.times do
|
|
41
|
+
result = gen.generate(self)
|
|
42
|
+
result.apply(alts, functions)
|
|
43
|
+
return result if !@unique || @unique.add?(result.to_s)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
raise "Unable to find a unique result"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eunomia
|
|
4
|
+
class Result
|
|
5
|
+
attr_reader :key, :display, :elements, :multiplier, :base_value, :meta
|
|
6
|
+
|
|
7
|
+
def initialize(key, value: 0, multiplier: 1)
|
|
8
|
+
@key = key
|
|
9
|
+
@base_value = value
|
|
10
|
+
@multiplier = multiplier
|
|
11
|
+
@elements = []
|
|
12
|
+
@meta = Hash.new {|h,k| h[k] = Set.new}
|
|
13
|
+
@display = ""
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def append(obj)
|
|
17
|
+
if obj.is_a?(Element)
|
|
18
|
+
append_element(obj)
|
|
19
|
+
elsif obj.is_a?(Result)
|
|
20
|
+
append_result(obj)
|
|
21
|
+
elsif obj.is_a?(Separator)
|
|
22
|
+
append_separator(obj)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def append_element(element)
|
|
27
|
+
@elements << element
|
|
28
|
+
@display += element.to_s
|
|
29
|
+
@multiplier *= element.multiplier unless element.multiplier.nil?
|
|
30
|
+
@base_value += element.value unless element.value.nil?
|
|
31
|
+
merge_meta(element.meta)
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def append_separator(separator)
|
|
36
|
+
@display += separator.to_s
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def append_result(result)
|
|
41
|
+
element = Element.new(result.to_s, value: result.value, multiplier: result.multiplier, children: result.elements)
|
|
42
|
+
@elements << element
|
|
43
|
+
@display += element.to_s
|
|
44
|
+
@base_value += element.value
|
|
45
|
+
merge_meta(result.meta)
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def merge_meta(m)
|
|
50
|
+
return unless m
|
|
51
|
+
|
|
52
|
+
m.each do |k, v|
|
|
53
|
+
v = [v] unless v.is_a?(Enumerable)
|
|
54
|
+
@meta[k] = Set.new(@meta[k] + v).to_a
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def apply_translations(alts, locale: nil)
|
|
59
|
+
arr = to_s.split(/\s+/)
|
|
60
|
+
arr = arr.map do |segment|
|
|
61
|
+
hsh = alts[segment]
|
|
62
|
+
hsh = hsh.is_a?(Hash) ? hsh[locale] || hsh['*'] || segment : hsh || segment
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def apply_functions(arr, functions)
|
|
67
|
+
Eunomia.apply(arr, functions)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def apply(alts, functions, locale: nil)
|
|
71
|
+
arr = apply_translations(alts, locale: locale)
|
|
72
|
+
arr = apply_functions(arr, functions)
|
|
73
|
+
|
|
74
|
+
@display = arr.join(' ')
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def value
|
|
79
|
+
base_value * multiplier
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def to_s
|
|
83
|
+
@display
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def to_h
|
|
87
|
+
{ display:, key:, value:, multiplier:, meta:, elements: elements&.map(&:to_h) }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eunomia
|
|
4
|
+
module Segment
|
|
5
|
+
# Common implementations of the value, multiplier, calc and generate methods
|
|
6
|
+
module Common
|
|
7
|
+
def value
|
|
8
|
+
0
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def multiplier
|
|
12
|
+
1
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def generate(_request)
|
|
16
|
+
calc # update values for dynamic segments
|
|
17
|
+
Eunomia::Element.new(text, value: value, multiplier: multiplier)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def calc; end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eunomia
|
|
4
|
+
module Segment
|
|
5
|
+
class Constant
|
|
6
|
+
include Common
|
|
7
|
+
|
|
8
|
+
SEPARATOR_MATCHER = /\p{Space}|\p{Punct}/
|
|
9
|
+
|
|
10
|
+
attr_reader :text
|
|
11
|
+
|
|
12
|
+
def initialize(text)
|
|
13
|
+
@text = text
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def generate(_request)
|
|
17
|
+
calc # update values for dynamic segments
|
|
18
|
+
Eunomia::Separator.new(text)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.build(scanner)
|
|
22
|
+
str = scanner.scan(SEPARATOR_MATCHER)
|
|
23
|
+
new(str) if str
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eunomia
|
|
4
|
+
module Segment
|
|
5
|
+
# Dice generates a random number using dice notation (e.g. `[3d6]`)
|
|
6
|
+
# with common operators (e.g. `[3d6+2]` or `[4d10x2]`)
|
|
7
|
+
# All values are integers so using the division operator
|
|
8
|
+
# will be rounded down.
|
|
9
|
+
class Dice
|
|
10
|
+
include Common
|
|
11
|
+
|
|
12
|
+
DICE_MATCHER = %r{\[(\d+)?d(\d+)(?:([+x/-])(\d+))?\]}
|
|
13
|
+
ADD = "+"
|
|
14
|
+
MULTIPLY = "x"
|
|
15
|
+
SUBTRACT = "-"
|
|
16
|
+
DIVIDE = "/"
|
|
17
|
+
OPS = [ADD, MULTIPLY, SUBTRACT, DIVIDE].freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :count, :range, :op, :constant
|
|
20
|
+
|
|
21
|
+
def initialize(count, range, op = ADD, constant = 0)
|
|
22
|
+
@count = count.to_i.clamp(1, 100)
|
|
23
|
+
@range = range.to_i.clamp(1, 1_000_000)
|
|
24
|
+
@op = OPS.include?(op) ? op : ADD
|
|
25
|
+
@constant = constant.to_i.clamp(0, 1_000_000)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def roll
|
|
29
|
+
@multiplier = nil
|
|
30
|
+
sum = 0
|
|
31
|
+
count.times { sum += (rand(range) + 1) }
|
|
32
|
+
sum
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def calc
|
|
36
|
+
case op
|
|
37
|
+
when MULTIPLY
|
|
38
|
+
roll * constant
|
|
39
|
+
when DIVIDE
|
|
40
|
+
roll / constant
|
|
41
|
+
when SUBTRACT
|
|
42
|
+
roll - constant
|
|
43
|
+
when ADD
|
|
44
|
+
roll + constant
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def text
|
|
49
|
+
@multiplier ||= calc
|
|
50
|
+
@multiplier.to_s
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def multiplier
|
|
54
|
+
@multiplier ||= calc
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.build(scanner)
|
|
58
|
+
str = scanner.scan(DICE_MATCHER)
|
|
59
|
+
return unless str
|
|
60
|
+
|
|
61
|
+
new(scanner[1], scanner[2], scanner[3], scanner[4])
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eunomia
|
|
4
|
+
module Segment
|
|
5
|
+
class Number
|
|
6
|
+
include Common
|
|
7
|
+
|
|
8
|
+
NUMBER_MATCHER = /(\d+)/
|
|
9
|
+
|
|
10
|
+
attr_reader :text, :multipler
|
|
11
|
+
|
|
12
|
+
def initialize(number)
|
|
13
|
+
@multiplier = number.to_i
|
|
14
|
+
@text = number.to_s
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.build(scanner)
|
|
18
|
+
str = scanner.scan(NUMBER_MATCHER)
|
|
19
|
+
new(str) if str
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eunomia
|
|
4
|
+
module Segment
|
|
5
|
+
# Reference represents a link to a Generator defined with square brackets.
|
|
6
|
+
# A request may define a constant that is used in place of a generator to
|
|
7
|
+
# return a specific value. This can be identified with a label so that the
|
|
8
|
+
# substitution is only used in specific context. For example `[test]` would
|
|
9
|
+
# first look for a constant named `test` and if not found, get the generator
|
|
10
|
+
# from the store. `[test:label]` would only use the constant if the request
|
|
11
|
+
# has a constant key if `test:label` and, otherwise, use the generator
|
|
12
|
+
# called `test`.
|
|
13
|
+
class Reference
|
|
14
|
+
include Common
|
|
15
|
+
|
|
16
|
+
REFERENCE_MATCHER = /^\[([A-Za-z][A-Za-z0-9_-]+)(?::([A-Za-z0-9_-]+))?\]/
|
|
17
|
+
|
|
18
|
+
attr_reader :key, :label
|
|
19
|
+
|
|
20
|
+
def initialize(key, label = nil)
|
|
21
|
+
@key = key
|
|
22
|
+
@label = label
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def label?
|
|
26
|
+
!label.nil?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def generate(request)
|
|
30
|
+
return Eunomia::Element.new(request.constants[lookup]) if request.constants.key?(lookup)
|
|
31
|
+
|
|
32
|
+
request.increase_depth
|
|
33
|
+
Eunomia.generate(key, request)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def lookup
|
|
37
|
+
@lookup ||= label.nil? ? key : "#{key}:#{label}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.build(scanner)
|
|
41
|
+
str = scanner.scan(REFERENCE_MATCHER)
|
|
42
|
+
return unless str
|
|
43
|
+
|
|
44
|
+
new(scanner[1], scanner[2])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eunomia
|
|
4
|
+
module Segment
|
|
5
|
+
# Text respresents a string of constant, non-numeric text.
|
|
6
|
+
class Text
|
|
7
|
+
include Common
|
|
8
|
+
|
|
9
|
+
TEXT_MATCHER = /\p{Alpha}[\p{Alnum}-]*/
|
|
10
|
+
|
|
11
|
+
attr_reader :text
|
|
12
|
+
|
|
13
|
+
def initialize(text)
|
|
14
|
+
@text = text
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.build(scanner)
|
|
18
|
+
str = scanner.scan(TEXT_MATCHER)
|
|
19
|
+
new(str) if str
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
|
|
5
|
+
require_relative "segment/common"
|
|
6
|
+
require_relative "segment/dice"
|
|
7
|
+
require_relative "segment/constant"
|
|
8
|
+
require_relative "segment/text"
|
|
9
|
+
require_relative "segment/number"
|
|
10
|
+
require_relative "segment/reference"
|
|
11
|
+
|
|
12
|
+
module Eunomia
|
|
13
|
+
# Segment represents a single token in an `Item`.
|
|
14
|
+
module Segment
|
|
15
|
+
def self.build(str)
|
|
16
|
+
ss = StringScanner.new(str)
|
|
17
|
+
arr = []
|
|
18
|
+
until ss.eos?
|
|
19
|
+
seg = Eunomia::Segment::Dice.build(ss) ||
|
|
20
|
+
Eunomia::Segment::Reference.build(ss) ||
|
|
21
|
+
Eunomia::Segment::Number.build(ss) ||
|
|
22
|
+
Eunomia::Segment::Text.build(ss) ||
|
|
23
|
+
Eunomia::Segment::Constant.build(ss)
|
|
24
|
+
raise "Unable to parse #{ss.rest} (#{ss.rest_size})" unless seg
|
|
25
|
+
|
|
26
|
+
arr << seg
|
|
27
|
+
end
|
|
28
|
+
arr
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eunomia
|
|
4
|
+
# Selector defines a strategy for selecting an item from a list of items.
|
|
5
|
+
# The default behavior is to randomly generate a number between 0 and the
|
|
6
|
+
# sum of the weights of the items and select the first match.
|
|
7
|
+
#
|
|
8
|
+
# Alternately, a dice notation string can be provided to generate a random
|
|
9
|
+
# number based on the dice notation. The dice notation is shifted down based
|
|
10
|
+
# on the number of dice so that, for example, 2d6 will generate a number
|
|
11
|
+
# between 0 and 10.
|
|
12
|
+
#
|
|
13
|
+
# If the dice notation is less than the total weight of the items the
|
|
14
|
+
# items greater than the maxumum value are ignored. If the dice notation is
|
|
15
|
+
# greater than the total weight of the items the number is regenerated.
|
|
16
|
+
class Selector
|
|
17
|
+
DICE_MATCHER = /^(\d+)?d(\d+)?/
|
|
18
|
+
|
|
19
|
+
attr_reader :count, :range
|
|
20
|
+
|
|
21
|
+
def initialize(dice = nil)
|
|
22
|
+
@count = nil
|
|
23
|
+
@range = nil
|
|
24
|
+
return unless dice
|
|
25
|
+
|
|
26
|
+
m = DICE_MATCHER.match(dice)
|
|
27
|
+
return unless m
|
|
28
|
+
|
|
29
|
+
@count = m[1].to_i.clamp(1, 100)
|
|
30
|
+
@range = m[2].to_i.clamp(1, 10_000)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def select(items)
|
|
34
|
+
n = random(items)
|
|
35
|
+
items.each do |item|
|
|
36
|
+
n -= item.weight
|
|
37
|
+
return item if n.negative?
|
|
38
|
+
end
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def random(items)
|
|
43
|
+
max_weight = items.map(&:weight).sum
|
|
44
|
+
if count.nil? || count >= max_weight
|
|
45
|
+
rand(max_weight)
|
|
46
|
+
else
|
|
47
|
+
n = roll
|
|
48
|
+
n /= 2 while n >= max_weight
|
|
49
|
+
n
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def roll
|
|
54
|
+
sum = 0
|
|
55
|
+
count.times { sum += rand(range) }
|
|
56
|
+
sum
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eunomia
|
|
4
|
+
# Store holds all the generators. Generators are stored by key.
|
|
5
|
+
class Store
|
|
6
|
+
def initialize
|
|
7
|
+
@generators = {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def lookup(key)
|
|
11
|
+
@generators[key] or raise Error, "Generator #{key} not found"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def keys
|
|
15
|
+
@generators.keys
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def read(path)
|
|
19
|
+
text = File.read(path)
|
|
20
|
+
hsh_or_array = JSON.parse(text)
|
|
21
|
+
add(hsh_or_array)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def add(hsh_or_array)
|
|
25
|
+
return unless hsh_or_array
|
|
26
|
+
|
|
27
|
+
if hsh_or_array.is_a?(Array)
|
|
28
|
+
hsh_or_array.each do |item|
|
|
29
|
+
add(item)
|
|
30
|
+
end
|
|
31
|
+
else
|
|
32
|
+
gen = Eunomia::Generator.new(hsh_or_array)
|
|
33
|
+
@generators[gen.key] = gen
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|