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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c35f010532ce719b33633e584eed9bfa532928d7
4
- data.tar.gz: ef440164cce6a34a1098d634116e3a1092e7fa6c
3
+ metadata.gz: c183cbc357d79a7ad5537c2dbd12b798e383d209
4
+ data.tar.gz: 91055b2773aa8d6ee095a8da1be6209c67e8d4bd
5
5
  SHA512:
6
- metadata.gz: f6a8aa67a07346933d103cb74ddaa809e9df3b086a45ae6b29374ddfdc64e1c5ff6c5f61185d3e6da52612e28f196b61be5167837deed5eeed634716ce90eec5
7
- data.tar.gz: a526186ab3953e8a2848e74c8d4616a4cb946d7927c8e674dd306f9a8aa827f975d9d92fe98628720dc32f4283aae4ca4dfa791a80acd5c6b41e703c6575d780
6
+ metadata.gz: aaea9c46d01e5f0683c9a96148c2695be6094133369acf9dd5f8eb7c09f78f6071cdf36059463f9d354af7ed3662505ab4c09c6c098f70b2b2b1c10d16fe13f5
7
+ data.tar.gz: dea0c09003fb3a1c06bbfadc9a19522287b72bca2fc3a3cf309e8af31119f1c98f42cc5ae121d853b7396f135e84840a6e33b40cfe7127e4a920272180f840e5
data/lib/gamefic.rb CHANGED
@@ -14,6 +14,7 @@ require "gamefic/rule"
14
14
  require "gamefic/director"
15
15
  require "gamefic/plot"
16
16
  require "gamefic/engine"
17
+ require "gamefic/user"
17
18
  require "gamefic/direction"
18
19
 
19
20
  require 'gamefic/version'
@@ -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 doesn't need to parse the command first.
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.stream.send message
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.stream.send message.strip if !user.nil?
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
@@ -1,108 +1,7 @@
1
1
  module Gamefic
2
2
 
3
- class Engine
4
- def initialize(plot)
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
@@ -1,254 +1,20 @@
1
- require 'gamefic/engine'
2
- require 'rexml/document'
3
- require 'gamefic/ansi'
4
- require 'gamefic/html'
5
- require 'json'
6
-
7
- begin
8
- require 'io/console'
9
- rescue LoadError
10
- puts "This version of Ruby does not support io/console. Text may not wrap correctly."
11
- if RUBY_VERSION.split('.')[0].to_i < 2
12
- puts "It is recommended that you upgrade to Ruby 2.0.0 or higher."
13
- end
14
- end
15
-
16
- module Gamefic
17
-
18
- module Tty
19
- class Engine < Gamefic::Engine
20
- def post_initialize
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(/&apos;/, "'").gsub(/&quot;/, '"').gsub(/&lt;/, '<').gsub(/&gt;/, '>')
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