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