story-gen 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|