story-gen 0.0.1
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.
- data/README.md +259 -0
- data/bin/story +82 -0
- data/gem.gemspec +13 -0
- data/lib/any.rb +11 -0
- data/lib/array/case_equal_fixed.rb +9 -0
- data/lib/array/chomp.rb +21 -0
- data/lib/array/separate.rb +10 -0
- data/lib/array/to_h.rb +9 -0
- data/lib/code.rb +123 -0
- data/lib/enumerable/empty.rb +10 -0
- data/lib/enumerable/lazy.rb +10 -0
- data/lib/enumerable/new.rb +30 -0
- data/lib/enumerable/sample.rb +8 -0
- data/lib/fact.rb +333 -0
- data/lib/hash/put.rb +14 -0
- data/lib/object/map_.rb +9 -0
- data/lib/object/to_rb.rb +9 -0
- data/lib/parse.rb +463 -0
- data/lib/story.rb +89 -0
- data/lib/story/compile.rb +926 -0
- data/lib/string/lchomp.rb +14 -0
- data/lib/string/ru_downcase.rb +21 -0
- data/lib/strscan/substr.rb +16 -0
- data/lib/unique_names.rb +269 -0
- data/sample/fight_club.sdl +58 -0
- data/sample/university.sdl +53 -0
- data/sample//320/261/320/276/320/271/321/206/320/276/320/262/321/201/320/272/320/270/320/271_/320/272/320/273/321/203/320/261.sdl +58 -0
- data/sample//321/203/320/275/320/270/320/262/320/265/321/200/321/201/320/270/321/202/320/265/321/202.sdl +54 -0
- metadata +75 -0
data/README.md
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
Story Generator
|
2
|
+
===============
|
3
|
+
|
4
|
+
Generate stories from descriptions written in Story Description Language (see below)!
|
5
|
+
|
6
|
+
Install
|
7
|
+
-------
|
8
|
+
|
9
|
+
Story Generator is a Ruby Gem, so simply install it with `gem install story-gen`. Of course you also need [Ruby](https://www.ruby-lang.org) >= 1.9.3!
|
10
|
+
|
11
|
+
Usage
|
12
|
+
-----
|
13
|
+
|
14
|
+
story file.sdl
|
15
|
+
|
16
|
+
Read story description from "file.sdl", generate a story and write it to stdout.
|
17
|
+
|
18
|
+
story -c -n MyStory -o file.rb file.sdl
|
19
|
+
|
20
|
+
Read story description from "file.sdl" and write a class MyStory to "file.rb". The class MyStory subclasses Story (see docs) and can be used like this: `MyStory.new.write()`.
|
21
|
+
|
22
|
+
story --help
|
23
|
+
|
24
|
+
More help on `story`.
|
25
|
+
|
26
|
+
Story Description Language (SDL)
|
27
|
+
--------------------------------
|
28
|
+
|
29
|
+
A simple story:
|
30
|
+
|
31
|
+
"Hello, world!"
|
32
|
+
|
33
|
+
It just prints "Hello, world!". No escape sequences for you, thus, for example, to start a new chapter you need to write something like this:
|
34
|
+
|
35
|
+
"
|
36
|
+
Chapter I
|
37
|
+
---------
|
38
|
+
"
|
39
|
+
|
40
|
+
You may use single quotes as well:
|
41
|
+
|
42
|
+
'Hello, world!'
|
43
|
+
|
44
|
+
Comments:
|
45
|
+
|
46
|
+
"Hello, world!" (note: a comment, "literal" style)
|
47
|
+
/* a comment, C style */
|
48
|
+
|
49
|
+
A story is based on Facts. To state a Fact just write it:
|
50
|
+
|
51
|
+
"John" loves "Liza"
|
52
|
+
|
53
|
+
In the Fact expression you may use english and russian words (except keywords, see below), characters from "#№@$%^/-", integer numbers (e.g., `18`) and arbitrary double- or single-quoted strings (`"..."` or `'...'`). Trailing commas are ignored (`xxx yyy zzz,` is the same as `xxx yyy zzz`). The words are case-insensitive (so `loves` and `Loves` mean the same), quoted strings are case-sensitive (so `"John"`≠`"JOHN"`).
|
54
|
+
|
55
|
+
Let's state some Facts:
|
56
|
+
|
57
|
+
"John" is a boy;
|
58
|
+
"Sam" is a boy;
|
59
|
+
"Liza" is a girl;
|
60
|
+
"Sabrina" is a girl;
|
61
|
+
"John" loves "Liza";
|
62
|
+
|
63
|
+
Statements in the story are separated with a semicolon (";"). The semicolon is optional if its absence does not cause an ambiguity, so the following is valid too:
|
64
|
+
|
65
|
+
"John" is a boy;
|
66
|
+
"Sam" is a boy;
|
67
|
+
"Liza" is a girl;
|
68
|
+
"Sabrina" is a girl;
|
69
|
+
"John" loves "Liza"
|
70
|
+
|
71
|
+
What can Facts be used for? You may form conditions with them:
|
72
|
+
|
73
|
+
If X is a boy then "Let's meet " X;
|
74
|
+
|
75
|
+
Here `X` is a variable. Variable names consist of underscores ("_") and capital letters only (e.g., `LONG_VARIABLE_NAME`). You may only capture quoted strings or numbers as variables, so the following condition is invalid:
|
76
|
+
|
77
|
+
If "John" A "Liza" then ... /* ERROR! */
|
78
|
+
|
79
|
+
You may use the captured variables in the statement after `then` keyword.
|
80
|
+
|
81
|
+
To print the captured variable you just write it along with some quoted string:
|
82
|
+
|
83
|
+
"Let's meet " X;
|
84
|
+
|
85
|
+
But wait... We have two boys! What does `if` choose as `X` then? The answer is: random. If there are several combinations of variables which fit the condition then a random combination is chosen.
|
86
|
+
|
87
|
+
You may form complex Fact expressions using `and`, `or` and `not` keywords and parentheses:
|
88
|
+
|
89
|
+
If X is a boy and Y is a girl then X" meets "Y"!"
|
90
|
+
|
91
|
+
If (X is a boy) and (Y is a girl) then X" meets "Y"!"
|
92
|
+
|
93
|
+
If X is a boy and Y is a girl and not X loves Y then
|
94
|
+
X" meets "Y"!"
|
95
|
+
|
96
|
+
The `not` keyword may also be written inside the Fact:
|
97
|
+
|
98
|
+
If X is a boy and Y is a girl and X not loves Y then
|
99
|
+
X" meets "Y"!"
|
100
|
+
|
101
|
+
Not all combinations of `and`, `or` and `not` are available, though. Use common sense to find out which one are. For example, this is an error:
|
102
|
+
|
103
|
+
If X is not a boy then "How can I determine "X"?" /* ERROR! */
|
104
|
+
|
105
|
+
You may also compare variables in the condition:
|
106
|
+
|
107
|
+
If X is a boy and Y is a boy and X != Y then
|
108
|
+
X" and "Y" are two different boys!"
|
109
|
+
|
110
|
+
There are limitations on the comparison: the comparison must be after the `and` keyword and all variables must be mentioned in the left part of `and`.
|
111
|
+
|
112
|
+
You may use `=`, `!=`, `<>`, `<=`, `<`, `>` and `>=` as comparison operators. Take types of the comparands into account, though!
|
113
|
+
|
114
|
+
You may use asterisk ("*") instead of the variable to avoid capturing:
|
115
|
+
|
116
|
+
If X is a boy and X not loves * then
|
117
|
+
X" is a lonely boy."
|
118
|
+
|
119
|
+
You may combine several `if`-s with `or` keyword:
|
120
|
+
|
121
|
+
If X is a boy then
|
122
|
+
"We have found a boy "X"!"
|
123
|
+
or if Y is a girl then
|
124
|
+
"We have found a girl "Y"!"
|
125
|
+
|
126
|
+
Or combine `if`-s with some statement:
|
127
|
+
|
128
|
+
If X is a boy then
|
129
|
+
"We know a boy "X"!"
|
130
|
+
or if Y is a girl then
|
131
|
+
"We know a girl "Y"!"
|
132
|
+
or
|
133
|
+
"We do not know anyone."
|
134
|
+
|
135
|
+
This is like a classical `if ... else if ... else ...` but if multiple conditions are true then random one is chosen (instead of the first one, like in the classical `if-else`). The last `or` is the same as `else` in the classical `if-else` - it is chosen if all conditions are false.
|
136
|
+
|
137
|
+
You may use captured variables to state a Fact:
|
138
|
+
|
139
|
+
If X is a boy and Y is a girl then
|
140
|
+
X loves Y
|
141
|
+
|
142
|
+
You may set the Fact false:
|
143
|
+
|
144
|
+
If X loves Y then
|
145
|
+
X not loves Y
|
146
|
+
|
147
|
+
Set multiple Facts false:
|
148
|
+
|
149
|
+
If X is a boy then
|
150
|
+
X not loves *
|
151
|
+
|
152
|
+
To combine several statements into one use a colon (":") with the final dot ("."):
|
153
|
+
|
154
|
+
If X is a boy then:
|
155
|
+
"Let's meet " X "!";
|
156
|
+
X " is a boy!".
|
157
|
+
|
158
|
+
or parentheses:
|
159
|
+
|
160
|
+
If X is a boy then (
|
161
|
+
"Let's meet " X "!";
|
162
|
+
X " is a boy!";
|
163
|
+
)
|
164
|
+
|
165
|
+
There are other statements you can use:
|
166
|
+
|
167
|
+
- "While":
|
168
|
+
|
169
|
+
<pre><code>
|
170
|
+
While <fact expression> [,] <statement>
|
171
|
+
</code></pre>
|
172
|
+
|
173
|
+
Here <fact expression> is the same as in `if` statement except that you may use `not` in top level:
|
174
|
+
|
175
|
+
<pre><code>
|
176
|
+
While X not loves *:
|
177
|
+
If X is a boy Y is a girl then X loves Y.
|
178
|
+
</code></pre>
|
179
|
+
|
180
|
+
The variables from <fact expression> are not available in <statement>
|
181
|
+
|
182
|
+
- "Repeat n times":
|
183
|
+
|
184
|
+
<pre><code>
|
185
|
+
10 times "Hello!" (note: print "Hello!" 10 times)
|
186
|
+
10...20 times "Hello!" (note: random number between 10 and 20 is
|
187
|
+
chosen)
|
188
|
+
X times "Hello!" (note: the value of the captured variable X
|
189
|
+
is used)
|
190
|
+
X...Y times "Hello!" (note: the value between two captured variables
|
191
|
+
is used)
|
192
|
+
</code></pre>
|
193
|
+
|
194
|
+
- "For all":
|
195
|
+
|
196
|
+
<pre><code>
|
197
|
+
For all <fact expression> [,] <statement>
|
198
|
+
</code></pre>
|
199
|
+
|
200
|
+
<statement> is executed for all combinations of variables in <fact expression>. The <fact expression> is like in `if` statement.
|
201
|
+
|
202
|
+
- Ruby code:
|
203
|
+
|
204
|
+
<pre><code>
|
205
|
+
```puts(x); puts(y)```
|
206
|
+
</code></pre>
|
207
|
+
|
208
|
+
Inside the code you can access the captured variables by their lowercase names:
|
209
|
+
|
210
|
+
<pre><code>
|
211
|
+
If X loves Y then
|
212
|
+
```puts(x); puts(y)```
|
213
|
+
</code></pre>
|
214
|
+
|
215
|
+
- "Either ... or ...":
|
216
|
+
|
217
|
+
<pre><code>
|
218
|
+
either <statement> [,]
|
219
|
+
or <statement> [,]
|
220
|
+
or <statement>
|
221
|
+
...
|
222
|
+
</code></pre>
|
223
|
+
|
224
|
+
A random <statement> is chosen and executed.
|
225
|
+
|
226
|
+
### Notes ###
|
227
|
+
|
228
|
+
`(note: ...)` comment may have nested parentheses:
|
229
|
+
|
230
|
+
(note: this is a comment (with nested parentheses)!)
|
231
|
+
|
232
|
+
Top-level statements may also be delimited with dot ("."):
|
233
|
+
|
234
|
+
"John" is a boy.
|
235
|
+
"Sam" is a boy.
|
236
|
+
"Liza" is a girl.
|
237
|
+
"Sabrina" is a girl.
|
238
|
+
"John" loves "Liza".
|
239
|
+
|
240
|
+
"If" statement may include a comma before `then` and `or`:
|
241
|
+
|
242
|
+
If X is a boy, then "we know "X,
|
243
|
+
or if Y is a girl, then "we know "Y
|
244
|
+
|
245
|
+
There is another form of the statements combination:
|
246
|
+
|
247
|
+
if X is a boy then:
|
248
|
+
- "We know "X;
|
249
|
+
- X" is a good boy";
|
250
|
+
- X" is glad to meet you".
|
251
|
+
|
252
|
+
Keywords `if`, `either`, `or`, `for` (in `for all`) and `while` may start with a capital letter: `If`, `Either` etc.
|
253
|
+
|
254
|
+
You may also use russian keywords! ;)
|
255
|
+
|
256
|
+
Examples
|
257
|
+
--------
|
258
|
+
|
259
|
+
See them in "sample" directory!
|
data/bin/story
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'story/compile'
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
$output_file = nil
|
6
|
+
$input_file = nil
|
7
|
+
$story_class_name = nil
|
8
|
+
$process = lambda do |story_class_code, output|
|
9
|
+
# execute the story
|
10
|
+
story_class = eval story_class_code
|
11
|
+
story = story_class.new
|
12
|
+
begin
|
13
|
+
story.write(output)
|
14
|
+
rescue Story::Error => e
|
15
|
+
abort "error: #{e.pos.file}:#{e.pos.line+1}:#{e.pos.column+1}: #{e.message}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
OptionParser.new do |opts|
|
19
|
+
opts.banner = "Usage: #{File.basename(__FILE__)} [options] [file]"
|
20
|
+
opts.separator ""
|
21
|
+
opts.separator "If `file' is omitted then the story is read from stdin."
|
22
|
+
opts.separator ""
|
23
|
+
opts.separator "Options:"
|
24
|
+
opts.on "-c", "--compile", "Compile only, do not execute the story" do
|
25
|
+
$process = lambda do |story_class_code, output|
|
26
|
+
output.write story_class_code
|
27
|
+
end
|
28
|
+
end
|
29
|
+
opts.on "-o", "--output FILE", "Write to FILE instead of stdout" do |file|
|
30
|
+
$output_file = file
|
31
|
+
end
|
32
|
+
opts.on "-n", "--class NAME", "Produce a class named NAME inheriting Story",
|
33
|
+
"instead of an anonymous class" do |name|
|
34
|
+
$story_class_name = name
|
35
|
+
end
|
36
|
+
opts.on "-r", "--show-relations", "Show relations mentioned in the story" do
|
37
|
+
$process = lambda do |story_class_code, output|
|
38
|
+
story_class = eval story_class_code
|
39
|
+
story = story_class.new
|
40
|
+
story.relations.each { |relation| output.puts relation }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
opts.on "-h", "--help", "Show this message and exit" do
|
44
|
+
puts opts
|
45
|
+
exit
|
46
|
+
end
|
47
|
+
end.parse!
|
48
|
+
if not ARGV.empty? then $input_file = ARGV.shift; end
|
49
|
+
abort "unknown argument: #{ARGV.first}" unless ARGV.empty?
|
50
|
+
|
51
|
+
begin
|
52
|
+
input_text =
|
53
|
+
if $input_file
|
54
|
+
then File.read($input_file)
|
55
|
+
else STDIN.read
|
56
|
+
end
|
57
|
+
story_class_code =
|
58
|
+
begin
|
59
|
+
Story.compile(input_text, $input_file || "-")
|
60
|
+
rescue Parse::Error => e
|
61
|
+
abort "error: #{e.pos.file}:#{e.pos.line+1}:#{e.pos.column+1}: #{e.message}"
|
62
|
+
end
|
63
|
+
if $story_class_name then
|
64
|
+
story_class_code = <<-CODE
|
65
|
+
#{$story_class_name} = begin
|
66
|
+
#{story_class_code}
|
67
|
+
end
|
68
|
+
CODE
|
69
|
+
end
|
70
|
+
output =
|
71
|
+
if $output_file
|
72
|
+
then File.open($output_file, "w")
|
73
|
+
else STDOUT
|
74
|
+
end
|
75
|
+
begin
|
76
|
+
$process.(story_class_code, output)
|
77
|
+
ensure
|
78
|
+
output.close()
|
79
|
+
end
|
80
|
+
rescue IOError => e
|
81
|
+
abort "error: #{e.message}"
|
82
|
+
end
|
data/gem.gemspec
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.name = 'story-gen'
|
4
|
+
s.version = '0.0.1'
|
5
|
+
s.date = '2016-01-10'
|
6
|
+
s.summary = "Story generator"
|
7
|
+
s.description = "Generate stories from descriptions based on Facts!"
|
8
|
+
s.authors = ["Various Furriness"]
|
9
|
+
s.email = 'various.furriness@gmail.com'
|
10
|
+
s.files = Dir["lib/**/*.rb"] + ["README.md", "gem.gemspec"] + Dir["sample/*"]
|
11
|
+
s.executables = Dir["bin/*"].map { |f| File.basename(f) }
|
12
|
+
s.license = 'MIT'
|
13
|
+
end
|
data/lib/any.rb
ADDED
data/lib/array/chomp.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
class Array
|
3
|
+
|
4
|
+
# if +item+ == {Array#last} then {Array#pop}s it; otherwise do nothing
|
5
|
+
#
|
6
|
+
# @return [self]
|
7
|
+
#
|
8
|
+
def chomp!(item)
|
9
|
+
self.pop if item == self.last
|
10
|
+
return self
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [Array] copy of this {Array} processed as per {#chomp!}.
|
14
|
+
def chomp(item)
|
15
|
+
self.dup.chomp!(item)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
# p [1, 2, 3].chomp(3)
|
21
|
+
# p [1, 2, 3].chomp(2)
|
data/lib/array/to_h.rb
ADDED
data/lib/code.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
|
2
|
+
class Code
|
3
|
+
|
4
|
+
# @api private
|
5
|
+
# @note used by {Code}, {::code}, {::non_code} only.
|
6
|
+
#
|
7
|
+
# @param [Array<String, NonCodePart>] parts
|
8
|
+
# @param [Object, nil] metadata
|
9
|
+
#
|
10
|
+
def initialize(parts, metadata = nil)
|
11
|
+
@parts = parts
|
12
|
+
@metadata = metadata
|
13
|
+
end
|
14
|
+
|
15
|
+
# @overload + str
|
16
|
+
# @param [String] str
|
17
|
+
# @return [Code]
|
18
|
+
# @overload + code
|
19
|
+
# @param [Code] code
|
20
|
+
# @return [Code]
|
21
|
+
def + arg
|
22
|
+
case arg
|
23
|
+
when String then self + Code.new([arg])
|
24
|
+
when Code then Code.new(self.parts + arg.parts)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# @overload << str
|
29
|
+
# Appends +str+ to self.
|
30
|
+
# @param [String] str
|
31
|
+
# @return [self]
|
32
|
+
# @overload << code
|
33
|
+
# Appends +str+ to self.
|
34
|
+
# @param [Code] code
|
35
|
+
# @return [self]
|
36
|
+
def << arg
|
37
|
+
case arg
|
38
|
+
when String then self << Code.new([arg])
|
39
|
+
when Code then @parts.concat(arg.parts); self
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @overload metadata(obj)
|
44
|
+
# @param [Object] obj
|
45
|
+
# @return [Code] a {Code} with +obj+ attached to it. The +obj+ can later
|
46
|
+
# be retrieved with {#metadata}().
|
47
|
+
# @overload metadata
|
48
|
+
# @return [Object, nil] an {Object} attached to this {Code} with
|
49
|
+
# {#metadata}(obj) or nil if no {Object} is attached.
|
50
|
+
def metadata(*args)
|
51
|
+
if args.empty?
|
52
|
+
then @metadata
|
53
|
+
else Code.new(@parts, args.first)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @yieldparam [Object] part
|
58
|
+
# @yieldreturn [String]
|
59
|
+
# @return [Code] a {Code} with {#non_code_parts} mapped by the passed block.
|
60
|
+
def map_non_code_parts(&f)
|
61
|
+
Code.new(
|
62
|
+
@parts.map do |part|
|
63
|
+
case part
|
64
|
+
when String then part
|
65
|
+
when NonCodePart then f.(part.data)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Enumerable<Object>] non-code parts of this {Code} introduced
|
72
|
+
# with {::non_code}. See also {#map_non_code_parts}.
|
73
|
+
def non_code_parts
|
74
|
+
@parts.select { |part| part.is_a? NonCodePart }.map(&:data)
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [String]
|
78
|
+
def to_s
|
79
|
+
raise "non-code part: #{x.inspect}" if @parts.find { |part| part.is_a? NonCodePart }
|
80
|
+
@parts.join
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [String]
|
84
|
+
def inspect
|
85
|
+
Inspectable.new(@parts, @metadata).inspect
|
86
|
+
end
|
87
|
+
|
88
|
+
# @api private
|
89
|
+
# @note used by {Code}, {::non_code} only.
|
90
|
+
NonCodePart = Struct.new :data
|
91
|
+
|
92
|
+
protected
|
93
|
+
|
94
|
+
# @!visibility private
|
95
|
+
attr_reader :parts
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
# @!visibility private
|
100
|
+
Inspectable = Struct.new :parts, :metadata
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
# @overload code
|
105
|
+
# @return [Code] an empty {Code}.
|
106
|
+
# @overload code(str)
|
107
|
+
# @param [String] str
|
108
|
+
# @return [Code] +str+ converted to {Code}.
|
109
|
+
def code(str = nil)
|
110
|
+
if str
|
111
|
+
then Code.new([str])
|
112
|
+
else Code.new([])
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# @param [Object] data
|
117
|
+
# @return [Code] a {Code} consisting of the single non-code part +data+.
|
118
|
+
# See also {Code#non_code_parts}.
|
119
|
+
def non_code(data)
|
120
|
+
Code.new([Code::NonCodePart.new(data)])
|
121
|
+
end
|
122
|
+
|
123
|
+
alias __non_code__ non_code
|