story-teller 1.1.3

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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +623 -0
  3. data/README.md +188 -0
  4. data/Rakefile +58 -0
  5. data/config/database.yml +37 -0
  6. data/exe/inform.rb +6 -0
  7. data/game/config.yml +5 -0
  8. data/game/example.inf +90 -0
  9. data/game/example.rb +105 -0
  10. data/game/forms/example_form.rb +2 -0
  11. data/game/grammar/admin.inf.rb +185 -0
  12. data/game/grammar/builder.inf.rb +310 -0
  13. data/game/grammar/game_grammar.inf.rb +6 -0
  14. data/game/grammar/meta.inf.rb +41 -0
  15. data/game/languages/english.rb +571 -0
  16. data/game/models/example_model.rb +2 -0
  17. data/game/modules/example_module.rb +9 -0
  18. data/game/modules/parser_extensions.rb +264 -0
  19. data/game/rules/example_state.rb +2 -0
  20. data/game/scripts/example_script.rb +2 -0
  21. data/game/topics/example_topic.rb +2 -0
  22. data/game/verbs/game_verbs.rb +35 -0
  23. data/game/verbs/metaverbs.rb +2066 -0
  24. data/lib/story_teller/application.rb +82 -0
  25. data/lib/story_teller/cli.rb +35 -0
  26. data/lib/story_teller/color.rb +144 -0
  27. data/lib/story_teller/config.rb +61 -0
  28. data/lib/story_teller/curses_adapter.rb +30 -0
  29. data/lib/story_teller/database.rb +527 -0
  30. data/lib/story_teller/game/loader.rb +276 -0
  31. data/lib/story_teller/game.rb +22 -0
  32. data/lib/story_teller/inform/models.rb +42 -0
  33. data/lib/story_teller/inform/relational/link.rb +239 -0
  34. data/lib/story_teller/inform/relational/module.rb +203 -0
  35. data/lib/story_teller/inform/relational/object.rb +546 -0
  36. data/lib/story_teller/inform/relational/tag.rb +152 -0
  37. data/lib/story_teller/options.rb +151 -0
  38. data/lib/story_teller/persistence.rb +340 -0
  39. data/lib/story_teller/player_character.rb +99 -0
  40. data/lib/story_teller/privileges.rb +55 -0
  41. data/lib/story_teller/runtime.rb +381 -0
  42. data/lib/story_teller/snapshots.rb +412 -0
  43. data/lib/story_teller/terminal.rb +58 -0
  44. data/lib/story_teller/version.rb +24 -0
  45. data/lib/story_teller_cli.rb +34 -0
  46. metadata +158 -0
@@ -0,0 +1,264 @@
1
+ # game/modules/parser_extensions.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: false
4
+
5
+ # Copyright Nels Nelson 2008-2025 but freely usable (see license)
6
+ #
7
+ # This file is part of the StoryTeller.
8
+ #
9
+ # The StoryTeller is free software: you can redistribute it and/or
10
+ # modify it under the terms of the GNU General Public License as published
11
+ # by the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # The StoryTeller is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with the StoryTeller. If not, see <http://www.gnu.org/licenses/>.
21
+
22
+ # The Inform module
23
+ module Inform
24
+ # The Parser module
25
+ module Parser
26
+ # §28 How nouns are parsed
27
+ #
28
+ # So: a parse_name routine, if provided, is expected to try to match as many
29
+ # words as possible starting from the current position of wn and reading them
30
+ # in one at a time using the NextWord() routine. Thus it must not stop just
31
+ # because the first word makes sense, but must keep reading and find out how
32
+ # many words in a row make sense. It should return:
33
+ #
34
+ # 0 if the text didn't make any sense at all,
35
+ # k if k words in a row of the text seem to refer to the object, or
36
+ # −1 to tell the parser it doesn't want to decide after all.
37
+ #
38
+ # §A5 Entry point ParseNoun routine
39
+ #
40
+ # To do the job of parsing the name property (if parse_name hasn't done it
41
+ # already). This takes one argument, the object in question, and returns a
42
+ # value as if it were a parse_name routine.
43
+ #
44
+ # The ParseNoun entry point routine returns the number of words matched, or 0
45
+ # if there is no match, or −1 to decline to make a decision and give the job
46
+ # back to the parser. Note that if −1 is returned, the word number variable
47
+ # wn must be left set to the first word the parser should look at — probably
48
+ # the same value it had when ParseNoun was called, but not necessarily.
49
+ def ParseNoun(obj)
50
+ i = @wn - 1; m = 0; p = 0
51
+ if obj.in? InformLibrary::Compass
52
+ while (word = NextWord())
53
+ break unless word.matches_exactly?(obj)
54
+ m += 1
55
+ end
56
+ else
57
+ while (word = NextWord())
58
+ word_is_plural = plural?(word)
59
+ # singular_word = singularize(word)
60
+ if word_is_plural && word.matches_last_exactly?(obj)
61
+ m += 1
62
+ elsif Prepositions.include?(word) && word.matches_any_exactly?(obj)
63
+ p += 1
64
+ m += 1
65
+ elsif !Prepositions.include?(word) && word.matches_any_exactly?(obj)
66
+ m += 1
67
+ elsif word.matches?(obj)
68
+ m += 1
69
+ else
70
+ break
71
+ end
72
+ end
73
+ end
74
+ @wn = i + 1
75
+ m <= 0 ? -1 : p + m
76
+ end
77
+
78
+ def ScoreMatchL(context, match_list, match_scores, number_matched, threshold = 0)
79
+ threshold += 1 if @indef_type.anybits?(OTHER_BIT)
80
+ threshold += 1 if @indef_type.anybits?(MY_BIT)
81
+ threshold += 1 if @indef_type.anybits?(THAT_BIT)
82
+ threshold += 1 if @indef_type.anybits?(LIT_BIT)
83
+ threshold += 1 if @indef_type.anybits?(UNLIT_BIT)
84
+ threshold += 1 if @indef_owner
85
+
86
+ if defined?(DEBUG)
87
+ if @parser_trace >= 4
88
+ print " Scoring match list: indef mode " + @indef_mode + ", type " +
89
+ @indef_type + ", satisfying " + threshold + " requirements:\n"
90
+ end
91
+ end
92
+ # DEBUG
93
+
94
+ # if defined?(PREFER_HELD)
95
+ # a_s = SCORE__BESTLOC; l_s = SCORE__NEXTBESTLOC
96
+ # if @action_to_be == :Take || @action_to_be == :Remove
97
+ # a_s = SCORE__NEXTBESTLOC; l_s = SCORE__BESTLOC
98
+ # end
99
+ # # TODO: FIXME
100
+ # # Verify this is needed
101
+ # context = context # silence empty block warning
102
+ # else # not PREFER_HELD
103
+ a_s = SCORE__NEXTBESTLOC; l_s = SCORE__BESTLOC
104
+ if %i[held multiheld multiexcept].include?(context)
105
+ a_s = SCORE__BESTLOC; l_s = SCORE__NEXTBESTLOC
106
+ end
107
+ # end
108
+ # # PREFER_HELD
109
+
110
+ if number_matched > 20
111
+ log.warn "Matched more than 20 objects!"
112
+ # caller.each { |t| log.warn t }
113
+ end
114
+ # TODO: FIXME The @match_list traversal here is buggy and
115
+ # sometimes produces a situation where obj is nil.
116
+ for i in (0...number_matched)
117
+ # match_list.compact.each_with_index do |obj, i|
118
+ obj = match_list[i]; its_owner = parent(obj); its_score=0; met=0
119
+ # TODO: Remove:
120
+ if obj.nil?
121
+ log.warn "ScoreMatchL: obj is nil in iteration: #{i}, number_matched: " \
122
+ "#{number_matched}, match_list: #{match_list.length}"
123
+ next
124
+ end
125
+
126
+ mine = @actor.descendants
127
+ mine += @actor.body.descendants if @actor.respond_to?(:body)
128
+ other = @actors_location.descendants - mine
129
+
130
+ if defined?(DEBUG) && @scope_reason == PARSING_REASON && @parser_trace >= 10
131
+ println ">>In ScoreMatchL..."
132
+ println ">> ...context is #{context}"
133
+ println ">> ...@actor is #{@actor}"
134
+ println ">> ...mine are #{mine}"
135
+ println ">> ...obj is #{obj} (#{obj.identity})"
136
+ println ">> ...obj.in? mine are #{mine.include?(obj)}"
137
+ println ">> ...other are #{other}"
138
+ println ">> ...obj.in? other are #{other.include?(obj)}"
139
+ println ">> ...@indef_type & MY_BIT #{@indef_type & MY_BIT}"
140
+ println ">> ...@indef_type & MY_BIT != 0 #{@indef_type & MY_BIT != 0}"
141
+ end
142
+
143
+ # met += 1 if @indef_type & OTHER_BIT != 0 && obj.notin?($itobj, $himobj, $herobj)
144
+ met += 1 if @indef_type.anybits?(OTHER_BIT) && !language_pronouns.values.map(&:last).include?(obj)
145
+ met += 1 if @indef_type.anybits?(MY_BIT) && (its_owner == @actor || mine.include?(obj))
146
+ met += 1 if @indef_type.anybits?(THAT_BIT) && (its_owner == @actors_location || other.include?(obj))
147
+ met += 1 if @indef_type.anybits?(LIT_BIT) && obj.has?(:light)
148
+ met += 1 if @indef_type.anybits?(UNLIT_BIT) && obj.hasnt?(:light)
149
+ met += 1 if @indef_owner && its_owner == @indef_owner
150
+
151
+ if defined?(DEBUG) && @scope_reason == PARSING_REASON && @parser_trace >= 10
152
+ println ">> ...met is #{met}"
153
+ end
154
+
155
+ if met < threshold
156
+ if defined?(DEBUG)
157
+ if @parser_trace >= 4
158
+ print " " + The(match_list[i]) + " (" + match_list[i] + ") in " +
159
+ the(its_owner) + " is rejected (doesn't match descriptors)\n"
160
+ end
161
+ end
162
+ # DEBUG
163
+ match_list[i] = nil
164
+ else
165
+ its_score = 0
166
+ its_score = SCORE__UNCONCEALED if obj != @player && obj.hasnt?(:concealed)
167
+
168
+ if its_owner == @actor
169
+ its_score += a_s
170
+ elsif its_owner == @actors_location
171
+ its_score += l_s
172
+ elsif its_owner != Inform::English::Compass
173
+ its_score += SCORE__NOTCOMPASS
174
+ end
175
+
176
+ its_score += (SCORE__CHOOSEOBJ * ChooseObjects(obj, 2))
177
+
178
+ its_score += SCORE__NOTSCENERY if obj.hasnt? :scenery
179
+ its_score += SCORE__NOTACTOR if obj != @actor
180
+
181
+ # A small bonus for having the correct GNA,
182
+ # for sorting out ambiguous articles and the like.
183
+
184
+ if @indef_cases & PowersOfTwo_TB[GetGNAOfObject(obj)]
185
+ its_score += SCORE__GNA
186
+ end
187
+
188
+ match_scores[i] ||= 0
189
+ begin
190
+ match_scores[i] += its_score
191
+ rescue StandardError => e
192
+ log.error "Failed to accumulate score", e
193
+ end
194
+ if defined?(DEBUG)
195
+ if @parser_trace >= 4
196
+ print " " + The(match_list[i]) + " (#{match_list[i].object_id}) in " +
197
+ the(its_owner) + " : #{match_scores[i]} points\n"
198
+ end
199
+ end
200
+ # DEBUG
201
+ end
202
+ end
203
+
204
+ # TODO: There has to be a better way
205
+ log.debug " ScoreMatchL match_list: #{match_list.map { |a| a&.name || 'nil' }}"
206
+ while (i = match_list.index(nil))
207
+ log.debug " ScoreMatchL deleting from match_list: #{i}"
208
+ match_list.delete_at(i)
209
+ match_scores.delete_at(i)
210
+ number_matched = match_list.length
211
+ end
212
+
213
+ [match_list, match_scores, number_matched]
214
+ end
215
+
216
+ def time_parse_routine
217
+ return false unless defined?(Chronic)
218
+ begin
219
+ println "Parsing time token..."
220
+ timestamp = Chronic.parse(consume_remaining_text)
221
+ println " result #=> #{timestamp}"
222
+ return timestamp
223
+ rescue StandardError => e
224
+ log.error e
225
+ @etype = e
226
+ end
227
+ false
228
+ end
229
+
230
+ def id_parse_routine
231
+ l = TryNumber(@wn); @wn += 1
232
+ return false unless l.number?
233
+ getobject(l.to_i)
234
+ end
235
+
236
+ def query_parse_routine
237
+ l = consume_remaining_text
238
+ return false if l.empty?
239
+ @consult_words = l
240
+ @search_results = findobject(/#{l}.*/i) if l.length > 2
241
+ @search_results.first
242
+ end
243
+
244
+ def text_parse_routine
245
+ @consult_words = consume_remaining_text
246
+ if @consult_words.empty?
247
+ @consult_words = nil
248
+ return false
249
+ end
250
+ GPR_TEXT
251
+ end
252
+ alias emote_parse_routine text_parse_routine
253
+
254
+ def user_parse_routine
255
+ l = consume_remaining_text
256
+ return false if l.empty?
257
+ @consult_words = l
258
+ @search_results = Account.where(username: l) if l.length > 2
259
+ @search_results.first
260
+ end
261
+ end
262
+ # module Parser
263
+ end
264
+ # module Inform
@@ -0,0 +1,2 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
@@ -0,0 +1,2 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
@@ -0,0 +1,2 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # The Inform module
5
+ module Inform
6
+ # The Verb module
7
+ module Verbs
8
+ def ExampleSub
9
+ "This is just an example."
10
+ end
11
+ def GrinSub
12
+ "You grin."
13
+ end
14
+ def SmileSub
15
+ "You smile."
16
+ end
17
+ def HugSub
18
+ return "You wrap your arms around your torso and squeeze." if noun == @player
19
+ return if ObjectIsUntouchable(noun)
20
+ if noun.life? :Hug
21
+ j = RunLife(noun, :Hug)
22
+ if j.is_a?(String)
23
+ println j
24
+ else
25
+ println "You give " + the(noun) + " a big hug!"
26
+ end
27
+ return false if j
28
+ elsif noun.has? :animate
29
+ return The(noun) + " graciously declines."
30
+ end
31
+ return true if AfterRoutines()
32
+ "You give " + the(noun) + " a big hug!"
33
+ end
34
+ end
35
+ end