gamefic 1.3.2 → 1.4.0
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.
- checksums.yaml +4 -4
- data/lib/gamefic.rb +1 -0
- data/lib/gamefic/character.rb +19 -13
- data/lib/gamefic/engine.rb +2 -103
- data/lib/gamefic/engine/base.rb +55 -0
- data/lib/gamefic/engine/tty.rb +20 -254
- data/lib/gamefic/html.rb +20 -19
- data/lib/gamefic/html_to_ansi.rb +185 -0
- data/lib/gamefic/plot.rb +21 -29
- data/lib/gamefic/plot/scene_mount.rb +103 -36
- data/lib/gamefic/scene.rb +3 -2
- data/lib/gamefic/scene/active.rb +1 -6
- data/lib/gamefic/scene/base.rb +34 -8
- data/lib/gamefic/scene/conclusion.rb +0 -3
- data/lib/gamefic/scene/custom.rb +28 -7
- data/lib/gamefic/scene/multiple_choice.rb +31 -38
- data/lib/gamefic/scene/multiple_scene.rb +15 -0
- data/lib/gamefic/scene/pause.rb +4 -15
- data/lib/gamefic/scene/yes_or_no.rb +8 -13
- data/lib/gamefic/scene_data.rb +9 -0
- data/lib/gamefic/scene_data/base.rb +12 -0
- data/lib/gamefic/scene_data/multiple_choice.rb +19 -0
- data/lib/gamefic/scene_data/multiple_scene.rb +25 -0
- data/lib/gamefic/scene_data/yes_or_no.rb +18 -0
- data/lib/gamefic/shell.rb +77 -78
- data/lib/gamefic/tester.rb +1 -1
- data/lib/gamefic/tty.rb +8 -0
- data/lib/gamefic/user.rb +8 -0
- data/lib/gamefic/user/base.rb +21 -0
- data/lib/gamefic/user/buffer.rb +25 -0
- data/lib/gamefic/user/tty.rb +55 -0
- data/lib/gamefic/version.rb +1 -1
- metadata +15 -6
- data/lib/gamefic/engine/cgi.rb +0 -221
- data/lib/gamefic/scene/multiple_choice/input.rb +0 -11
- data/lib/gamefic/scene/passive.rb +0 -17
- data/lib/gamefic/scene/question.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c183cbc357d79a7ad5537c2dbd12b798e383d209
|
4
|
+
data.tar.gz: 91055b2773aa8d6ee095a8da1be6209c67e8d4bd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aaea9c46d01e5f0683c9a96148c2695be6094133369acf9dd5f8eb7c09f78f6071cdf36059463f9d354af7ed3662505ab4c09c6c098f70b2b2b1c10d16fe13f5
|
7
|
+
data.tar.gz: dea0c09003fb3a1c06bbfadc9a19522287b72bca2fc3a3cf309e8af31119f1c98f42cc5ae121d853b7396f135e84840a6e33b40cfe7127e4a920272180f840e5
|
data/lib/gamefic.rb
CHANGED
data/lib/gamefic/character.rb
CHANGED
@@ -28,25 +28,30 @@ module Gamefic
|
|
28
28
|
# Disconnect the current User.
|
29
29
|
#
|
30
30
|
def disconnect
|
31
|
-
# TODO: We might need some cleanup here. Like, move the character out of the game, or set a timeout to allow dropped users to reconnect... figure it out.
|
32
31
|
@user = nil
|
33
32
|
end
|
34
33
|
|
35
34
|
# Perform a command.
|
36
35
|
# The command can be specified as a String or a set of tokens. Either form
|
37
36
|
# should yield the same result, but using tokens can yield better
|
38
|
-
# performance since it
|
37
|
+
# performance since it bypasses the parser.
|
39
38
|
#
|
40
39
|
# The command will be executed immediately regardless of game state.
|
41
40
|
#
|
41
|
+
# If the from_user argument is true, the command is assumed to have come
|
42
|
+
# directly from user input. The character's last_order and last_object
|
43
|
+
# will be updated with the result.
|
44
|
+
#
|
42
45
|
# @example Send a command as a string
|
43
46
|
# character.perform "take the key"
|
44
47
|
#
|
45
48
|
# @example Send a command as a set of tokens
|
46
49
|
# character.perform :take, @key
|
47
50
|
#
|
48
|
-
def perform(*command)
|
49
|
-
Director.dispatch(self, *command)
|
51
|
+
def perform(*command, from_user: false)
|
52
|
+
o = Director.dispatch(self, *command)
|
53
|
+
last_order = o if from_user
|
54
|
+
o
|
50
55
|
end
|
51
56
|
|
52
57
|
# Quietly perform a command.
|
@@ -79,7 +84,7 @@ module Gamefic
|
|
79
84
|
# compatibility with Opal.
|
80
85
|
message = message.gsub(/[ \t\r]*\n[ \t\r]*\n[ \t\r]*/, '</p><p>')
|
81
86
|
message = message.gsub(/[ \t]*\n[ \t]*/, ' ')
|
82
|
-
user.
|
87
|
+
user.send message
|
83
88
|
end
|
84
89
|
end
|
85
90
|
end
|
@@ -89,15 +94,16 @@ module Gamefic
|
|
89
94
|
#
|
90
95
|
# @param message [String]
|
91
96
|
def stream(message)
|
92
|
-
user.
|
93
|
-
end
|
94
|
-
|
95
|
-
def destroy
|
96
|
-
if @user != nil
|
97
|
-
@user.quit
|
98
|
-
end
|
99
|
-
super
|
97
|
+
user.send message.strip unless user.nil?
|
100
98
|
end
|
99
|
+
|
100
|
+
# TODO This might not be necessary. The User#quit method was a noop anyway.
|
101
|
+
#def destroy
|
102
|
+
# if @user != nil
|
103
|
+
# @user.quit
|
104
|
+
# end
|
105
|
+
# super
|
106
|
+
#end
|
101
107
|
|
102
108
|
# Proceed to the next Action in the current stack.
|
103
109
|
# This method is typically used in Action blocks to cascade through
|
data/lib/gamefic/engine.rb
CHANGED
@@ -1,108 +1,7 @@
|
|
1
1
|
module Gamefic
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
@plot = plot
|
6
|
-
post_initialize
|
7
|
-
end
|
8
|
-
def post_initialize
|
9
|
-
@user = User.new @plot
|
10
|
-
end
|
11
|
-
def run
|
12
|
-
@plot.introduce @user.character
|
13
|
-
print @user.state.output
|
14
|
-
while !@plot.concluded?(@user.character)
|
15
|
-
turn
|
16
|
-
end
|
17
|
-
print @user.state.output
|
18
|
-
end
|
19
|
-
def turn
|
20
|
-
@plot.ready
|
21
|
-
print @user.state.output
|
22
|
-
if !@user.character[:test_queue].nil? and @user.character[:test_queue].length > 0
|
23
|
-
test_command = @user.character[:test_queue].shift
|
24
|
-
@user.character.tell "[TESTING] #{@plot.scenes[@user.character.scene].prompt} #{test_command}"
|
25
|
-
@user.character.queue.push test_command
|
26
|
-
else
|
27
|
-
@user.stream.select @plot.scenes[@user.character.scene].prompt
|
28
|
-
@user.state.input
|
29
|
-
end
|
30
|
-
@plot.update
|
31
|
-
print @user.state.output
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
class User
|
36
|
-
attr_reader :state, :character, :story
|
37
|
-
def initialize(plot)
|
38
|
-
@plot = plot
|
39
|
-
@character = Character.new @plot, :name => 'yourself', :synonyms => 'self myself you me', :proper_named => true
|
40
|
-
@character.connect self
|
41
|
-
post_initialize
|
42
|
-
end
|
43
|
-
def post_initialize
|
44
|
-
@stream = UserStream.new
|
45
|
-
@state = UserState.new self
|
46
|
-
end
|
47
|
-
def stream
|
48
|
-
@stream ||= UserStream.new
|
49
|
-
end
|
50
|
-
def state
|
51
|
-
@state ||= UserState.new(self)
|
52
|
-
end
|
53
|
-
def state=(state_class)
|
54
|
-
@state = state_class.new self
|
55
|
-
end
|
56
|
-
def refresh
|
57
|
-
# Nothing to do
|
58
|
-
end
|
59
|
-
def quit
|
60
|
-
#exit
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
class UserStream
|
65
|
-
def initialize
|
66
|
-
@queue = Array.new
|
67
|
-
@buffer = ''
|
68
|
-
end
|
69
|
-
def flush
|
70
|
-
tmp = @buffer.clone
|
71
|
-
#@buffer.clear
|
72
|
-
@buffer = ''
|
73
|
-
tmp
|
74
|
-
end
|
75
|
-
def send(data)
|
76
|
-
# Quick and dirty HTML sanitization
|
77
|
-
#data.gsub!(/<[a-z]+[^>]*>/i, "")
|
78
|
-
#data.gsub!(/<\/[^>]*>/, "")
|
79
|
-
@buffer += data
|
80
|
-
end
|
81
|
-
def select(prompt)
|
82
|
-
print "#{prompt} "
|
83
|
-
STDOUT.flush
|
84
|
-
line = STDIN.gets
|
85
|
-
@queue.push line.strip
|
86
|
-
end
|
87
|
-
def recv
|
88
|
-
@queue.shift
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
class UserState
|
93
|
-
attr_reader :user
|
94
|
-
def initialize(user)
|
95
|
-
@user = user
|
96
|
-
end
|
97
|
-
def input
|
98
|
-
line = @user.stream.recv
|
99
|
-
if line != nil
|
100
|
-
@user.character.queue.push line
|
101
|
-
end
|
102
|
-
end
|
103
|
-
def output
|
104
|
-
@user.stream.flush
|
105
|
-
end
|
3
|
+
module Engine
|
4
|
+
autoload :Base, 'gamefic/engine/base'
|
106
5
|
end
|
107
6
|
|
108
7
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Gamefic
|
2
|
+
|
3
|
+
# Basic functionality for running a single-player game from a console.
|
4
|
+
#
|
5
|
+
class Engine::Base
|
6
|
+
attr_writer :user_class
|
7
|
+
attr_reader :plot
|
8
|
+
|
9
|
+
def initialize(plot)
|
10
|
+
@plot = plot
|
11
|
+
post_initialize
|
12
|
+
end
|
13
|
+
|
14
|
+
def post_initialize
|
15
|
+
# Override in subclasses
|
16
|
+
end
|
17
|
+
|
18
|
+
def user_class
|
19
|
+
@user_class ||= Gamefic::User::Base
|
20
|
+
end
|
21
|
+
|
22
|
+
def connect
|
23
|
+
@character = @plot.make Character, name: 'yourself', synonyms: 'self myself you me', proper_named: true
|
24
|
+
@user = user_class.new
|
25
|
+
@character.connect @user
|
26
|
+
@character
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
connect
|
31
|
+
@plot.introduce @character
|
32
|
+
print @user.flush
|
33
|
+
turn until @plot.concluded?(@character)
|
34
|
+
print @user.flush
|
35
|
+
end
|
36
|
+
|
37
|
+
def turn
|
38
|
+
@plot.ready
|
39
|
+
print @user.flush
|
40
|
+
if @character.queue.empty?
|
41
|
+
receive
|
42
|
+
end
|
43
|
+
@plot.update
|
44
|
+
print @user.flush
|
45
|
+
end
|
46
|
+
|
47
|
+
def receive
|
48
|
+
print @plot.scenes[@character.scene].prompt_for(@character) + ' '
|
49
|
+
input = STDIN.gets
|
50
|
+
@character.queue.push input unless input.nil?
|
51
|
+
puts ''
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
data/lib/gamefic/engine/tty.rb
CHANGED
@@ -1,254 +1,20 @@
|
|
1
|
-
require 'gamefic/
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
@user = Tty::User.new @plot
|
22
|
-
end
|
23
|
-
end
|
24
|
-
class User < Gamefic::User
|
25
|
-
def post_initialize
|
26
|
-
@stream = Tty::UserStream.new
|
27
|
-
@state = UserState.new self
|
28
|
-
end
|
29
|
-
def ansi=(val)
|
30
|
-
@stream.ansi = val
|
31
|
-
end
|
32
|
-
def ansi
|
33
|
-
@stream.ansi
|
34
|
-
end
|
35
|
-
def save filename, snapshot
|
36
|
-
data = snapshot.merge(:metadata => @character.plot.metadata)
|
37
|
-
json = JSON.generate data
|
38
|
-
if json.nil?
|
39
|
-
@character.tell "Nothing to save."
|
40
|
-
end
|
41
|
-
if filename.nil?
|
42
|
-
stream.select "Enter the filename to save:"
|
43
|
-
filename = stream.queue.pop
|
44
|
-
end
|
45
|
-
if filename != ''
|
46
|
-
File.open(filename, 'w') do |f|
|
47
|
-
f.write json
|
48
|
-
end
|
49
|
-
@character.tell "Game saved."
|
50
|
-
end
|
51
|
-
end
|
52
|
-
def restore filename
|
53
|
-
if filename.nil?
|
54
|
-
stream.select "Enter the filename to restore:"
|
55
|
-
filename = stream.queue.pop
|
56
|
-
end
|
57
|
-
if filename != ''
|
58
|
-
if File.exists?(filename)
|
59
|
-
data = JSON.parse File.read(filename), symbolize_names: true
|
60
|
-
if (data[:metadata] != @character.plot.metadata)
|
61
|
-
@character.tell "The save file is not compatible with this version of the game."
|
62
|
-
else
|
63
|
-
return data
|
64
|
-
end
|
65
|
-
else
|
66
|
-
@character.tell "File \"#{filename}\" not found."
|
67
|
-
end
|
68
|
-
end
|
69
|
-
nil
|
70
|
-
end
|
71
|
-
end
|
72
|
-
class UserStream < Gamefic::UserStream
|
73
|
-
include Ansi
|
74
|
-
include Ansi::Code
|
75
|
-
def initialize
|
76
|
-
super
|
77
|
-
end
|
78
|
-
def size
|
79
|
-
begin
|
80
|
-
return STDOUT.winsize.reverse
|
81
|
-
rescue
|
82
|
-
return [nil,nil]
|
83
|
-
end
|
84
|
-
end
|
85
|
-
def flush
|
86
|
-
data = @buffer.clone
|
87
|
-
@buffer.clear
|
88
|
-
return if data.strip == ''
|
89
|
-
output = ''
|
90
|
-
begin
|
91
|
-
doc = Html.parse("<body>#{data.strip}</body>")
|
92
|
-
format_recursively doc
|
93
|
-
texts = REXML::XPath.match(doc, './/text()')
|
94
|
-
output = texts.join('').gsub(/'/, "'").gsub(/"/, '"').gsub(/</, '<').gsub(/>/, '>')
|
95
|
-
output += Ansi.graphics_mode(Attribute::NORMAL)
|
96
|
-
output = Html::decode(output)
|
97
|
-
rescue REXML::ParseException => e
|
98
|
-
output = Html.encode(data) + "\n\n"
|
99
|
-
end
|
100
|
-
output.gsub!(/(\n\n)+/, "\n\n")
|
101
|
-
width = size[0]
|
102
|
-
if width.nil?
|
103
|
-
output
|
104
|
-
else
|
105
|
-
"#{terminalize(output, width - 1)}"
|
106
|
-
end
|
107
|
-
end
|
108
|
-
def select(prompt)
|
109
|
-
super
|
110
|
-
print "\n"
|
111
|
-
end
|
112
|
-
def has_code?(fmt, code)
|
113
|
-
if fmt.kind_of?(Array)
|
114
|
-
return fmt.flatten.include?(code)
|
115
|
-
end
|
116
|
-
return fmt == code
|
117
|
-
end
|
118
|
-
|
119
|
-
private
|
120
|
-
|
121
|
-
def format_recursively(top, stack = nil)
|
122
|
-
ol_index = 1
|
123
|
-
stack ||= [Attribute::NORMAL]
|
124
|
-
top.elements.each { |element|
|
125
|
-
formats = [Attribute::NORMAL]
|
126
|
-
classes = element.attribute('class').to_s.split(' ')
|
127
|
-
if classes.include?('hint')
|
128
|
-
formats.push Foreground::YELLOW
|
129
|
-
end
|
130
|
-
case element.name
|
131
|
-
when 'strong', 'b'
|
132
|
-
formats.push Attribute::BOLD
|
133
|
-
when 'em', 'i', 'u'
|
134
|
-
formats.push Attribute::UNDERSCORE
|
135
|
-
when 'a'
|
136
|
-
if element.attributes['rel'].to_s == 'gamefic'
|
137
|
-
element.attributes['href'] = element.attributes['href'][5..-1]
|
138
|
-
formats.push [Attribute::UNDERSCORE, Extra::COMMAND]
|
139
|
-
else
|
140
|
-
formats.push [Attribute::UNDERSCORE, Extra::HREF]
|
141
|
-
end
|
142
|
-
when 'li'
|
143
|
-
if top.name == 'ol'
|
144
|
-
element.text = "#{ol_index}. #{element.text}"
|
145
|
-
ol_index += 1
|
146
|
-
else
|
147
|
-
element.text = "* #{element.text}"
|
148
|
-
end
|
149
|
-
formats.push [Extra::LINE]
|
150
|
-
when 'img'
|
151
|
-
formats.push [Extra::IGNORED]
|
152
|
-
when 'body', 'p', 'ol', 'ul'
|
153
|
-
formats.push Extra::BLOCK
|
154
|
-
when 'pre'
|
155
|
-
formats.push [Extra::BLOCK, Extra::PRE]
|
156
|
-
when 'nav'
|
157
|
-
formats.push Extra::BLOCK
|
158
|
-
when 'h1', 'h2', 'h3', 'h4', 'h5'
|
159
|
-
formats.push [Attribute::BOLD, Extra::BLOCK, Extra::UPPERCASE]
|
160
|
-
when 'kbd'
|
161
|
-
formats.push [Foreground::GREEN]
|
162
|
-
end
|
163
|
-
if has_code?(stack, Extra::IGNORED)
|
164
|
-
element.parent.delete_element(element)
|
165
|
-
end
|
166
|
-
stack.push formats
|
167
|
-
if has_code?(stack, Extra::UPPERCASE)
|
168
|
-
element.texts.each { |text|
|
169
|
-
text.value.upcase!
|
170
|
-
}
|
171
|
-
end
|
172
|
-
element.texts.each { |text|
|
173
|
-
text.value = "#{Ansi.graphics_mode(*stack)}#{text.value}"
|
174
|
-
}
|
175
|
-
if has_code?(stack.last, Extra::IMAGE)
|
176
|
-
element.text = "#{element.attribute('alt') ? element.attribute('alt') : '[Image]'}"
|
177
|
-
end
|
178
|
-
format_recursively element, stack
|
179
|
-
if has_code?(stack.last, Extra::COMMAND)
|
180
|
-
if element.attribute('data-command').to_s != ''
|
181
|
-
tmp = stack.pop
|
182
|
-
element.add_text "#{Ansi.graphics_mode(*stack)}"
|
183
|
-
element.add_text " [#{element.attribute('data-command')}]"
|
184
|
-
stack.push tmp
|
185
|
-
element.add_text "#{Ansi.graphics_mode(*stack)}"
|
186
|
-
end
|
187
|
-
end
|
188
|
-
if has_code?(stack.last, Extra::BLOCK) and !has_code?(stack.last, Extra::PRE)
|
189
|
-
element.texts.first.value.lstrip! unless element.texts.first.nil?
|
190
|
-
element.texts.last.value.rstrip! unless element.texts.last.nil?
|
191
|
-
element.texts.each { |t|
|
192
|
-
t.value = t.value.gsub(/ +/, ' ').strip
|
193
|
-
}
|
194
|
-
end
|
195
|
-
if has_code?(stack.last, Extra::BLOCK)
|
196
|
-
element.add_text("\n\n")
|
197
|
-
elsif has_code?(stack.last, Extra::LINE)
|
198
|
-
if !has_code?(stack[-2], Extra::BLOCK) || element != top.elements.to_a.last
|
199
|
-
element.add_text("\n")
|
200
|
-
end
|
201
|
-
end
|
202
|
-
if has_code?(stack.last, Extra::HREF)
|
203
|
-
if element.attribute('href').to_s != "#"
|
204
|
-
tmp = stack.pop
|
205
|
-
element.add_text "#{Ansi.graphics_mode(*stack)}"
|
206
|
-
element.add_text(" [#{element.attribute('href')}]")
|
207
|
-
stack.push tmp
|
208
|
-
element.add_text "#{Ansi.graphics_mode(*stack)}"
|
209
|
-
end
|
210
|
-
end
|
211
|
-
if has_code?(stack.last, Extra::IMAGE)
|
212
|
-
element.add_text(" [#{element.attribute('src')}]")
|
213
|
-
if !has_code?(stack, Extra::BLOCK)
|
214
|
-
element.add_text("\n\n")
|
215
|
-
end
|
216
|
-
end
|
217
|
-
stack.pop
|
218
|
-
}
|
219
|
-
end
|
220
|
-
def terminalize string, max_length
|
221
|
-
i = 0
|
222
|
-
output = ''
|
223
|
-
line_length = 0
|
224
|
-
while i < string.length
|
225
|
-
line_length += 1
|
226
|
-
char = string[i,1]
|
227
|
-
if char == "\e"
|
228
|
-
# Right now, graphics modes are the only supported ANSI sequences.
|
229
|
-
end_of_seq = string.index("m", i)
|
230
|
-
output += string[i..end_of_seq]
|
231
|
-
i = end_of_seq + 1
|
232
|
-
elsif char == " "
|
233
|
-
next_space = string.index(/[\s]/, i + 1)
|
234
|
-
if !next_space.nil? and line_length + (next_space - i) > max_length
|
235
|
-
output += "\n"
|
236
|
-
line_length = 0
|
237
|
-
else
|
238
|
-
output += char
|
239
|
-
end
|
240
|
-
i += 1
|
241
|
-
else
|
242
|
-
if char == "\n"
|
243
|
-
line_length = 0
|
244
|
-
end
|
245
|
-
output += char
|
246
|
-
i += 1
|
247
|
-
end
|
248
|
-
end
|
249
|
-
output #output.strip
|
250
|
-
end
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
end
|
1
|
+
require 'gamefic/user/tty'
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
|
5
|
+
# Extend Engine::Base to connect with User::Tty, which provides ANSI
|
6
|
+
# formatting for HTML.
|
7
|
+
#
|
8
|
+
class Engine::Tty < Engine::Base
|
9
|
+
def post_initialize
|
10
|
+
self.user_class = Gamefic::User::Tty
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.start plot
|
14
|
+
engine = self.new(plot)
|
15
|
+
engine.connect
|
16
|
+
engine.run
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|