gamefic 1.3.2 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|