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 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 &lt;fact expression&gt; [,] &lt;statement&gt;
171
+ </code></pre>
172
+
173
+ Here &lt;fact expression&gt; 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 &lt;fact expression&gt; are not available in &lt;statement&gt;
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 &lt;fact expression&gt; [,] &lt;statement&gt;
198
+ </code></pre>
199
+
200
+ &lt;statement&gt; is executed for all combinations of variables in &lt;fact expression&gt;. The &lt;fact expression&gt; 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 &lt;statement&gt; [,]
219
+ or &lt;statement&gt; [,]
220
+ or &lt;statement&gt;
221
+ ...
222
+ </code></pre>
223
+
224
+ A random &lt;statement&gt; 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
@@ -0,0 +1,11 @@
1
+
2
+ # To disable YARD warnings:
3
+ # @!parse
4
+ # class Object
5
+ # def === other; end
6
+ # end
7
+
8
+ # @return [Object] an {Object} which is {Object#===} to anything.
9
+ def any
10
+ Object
11
+ end
@@ -0,0 +1,9 @@
1
+
2
+ class Array
3
+
4
+ def === other
5
+ self.size == other.size and
6
+ self.zip(other).all? { |e1, e2| e1 === e2 }
7
+ end
8
+
9
+ end
@@ -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)
@@ -0,0 +1,10 @@
1
+
2
+ class Array
3
+
4
+ # @param [Object, nil] separator
5
+ # @return [Array] [self_1, separator, self_2, separator, ..., self_n].
6
+ def separate(separator)
7
+ (self.map { |item| [item, separator] }.reduce(:concat) or [1])[0...-1]
8
+ end
9
+
10
+ end
data/lib/array/to_h.rb ADDED
@@ -0,0 +1,9 @@
1
+
2
+ class Array
3
+
4
+ # @return [Hash]
5
+ def to_h
6
+ self.reduce({}) { |h, e| h[e.first] = e.last; h }
7
+ end
8
+
9
+ end
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