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 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