gruesome 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in grue.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ <pre>
2
+ ▄▄▄▄▄ ▄▄▄▄▄
3
+ ▀████▄ ▄████▀
4
+ ▀████▄ ▄████▀
5
+ ▄▄▄▄▄▄ ██████▄ ▄▄▄ ▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄▄ ▄████▀ ▄▄▄ ▄▄▄ ▄▄▄▄▄▄
6
+ ▄████████▄ ██▓▀████▄ ██▓ ███ ▄███████ ▄███████▄ ▄████▀██▄ ████▄████ ▄███████
7
+ ██▓ ███ ██▓ ▀███ ██▓ ███ ██▓ ██▓ ███ ███▀ ███ █████████ ██▓
8
+ ██▓ ██▓ ███ ██▓ ███ ██▓ ██▓ ██▓ ███ ███▀█▀███ ██▓
9
+ ██▓ █████ ████████ ███ ███ ████████ ▀██████▄ ██▓ ███ ██▓ ███ ████████
10
+ ██▓ ▀▀███ ▀██▓▀▀▀█▄▄ ███ ███ ███▀▀▀▀▀ ▀▀▀▀███ ██▓ ███ ██▓ ███ ███▀▀▀▀▀
11
+ ██▓ ███ ██▓ ███ ██▓ ███ ██▓ ██▓ ███ ██▓ ███ ███ ███ ██▓
12
+ ▀████████▀ ██▓ ███ ▀███████▀ ▀███████ ▀███████▀ ▀███████▀ ██▓ ███ ▀███████
13
+ ▀▀▀▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀▀▀ ▀▀▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀▀▀▀
14
+ </pre>
15
+
16
+ # Gruesome
17
+
18
+ The Ruby Z-Code Emulator and IF Manager (Currently in progress). It current can
19
+ play version 3 games such as the ZORK trilogy.
20
+
21
+ ## Currently implemented
22
+
23
+ * Majority of version 3 Z-Machine (Enough to play most games)
24
+
25
+ ## Next
26
+
27
+ * Versions 4+ (Goal: Run Photopia)
28
+
29
+ ## Overall Goals
30
+
31
+ There are several goals:
32
+
33
+ * Emulate the Z-Machine accurately for all versions (text-only first)
34
+ * Provide detailed information to IF designers and programmers
35
+ * Make it easy to pull down and play new and old IF and text adventure games
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ # rake spec
4
+ RSpec::Core::RakeTask.new(:spec) do |spec|
5
+ spec.pattern = 'spec/*/*_spec.rb'
6
+ end
7
+
8
+ # rake doc
9
+ RSpec::Core::RakeTask.new(:doc) do |spec|
10
+ spec.pattern = 'spec/*/*_spec.rb'
11
+ spec.rspec_opts = ['--format documentation']
12
+ end
13
+
14
+ require 'bundler'
15
+ Bundler::GemHelper.install_tasks
data/bin/gruesome ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.dirname(__FILE__) + '/../lib'
4
+
5
+ require 'gruesome'
6
+ require 'gruesome/cli'
7
+
8
+ Gruesome::CLI.run
9
+
data/gruesome.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "gruesome/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "gruesome"
7
+ s.version = Grue::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Dave Wilkinson"]
10
+ s.email = ["wilkie05@gmail.com"]
11
+ s.homepage = "http://github.com/wilkie/gruesome"
12
+ s.summary = %q{An Interactive Fiction client that can play/read interactive stories}
13
+ s.description = %q{Reads and executes various interactive fiction technologies and helps easily download new stories from other sources on the Internet.}
14
+
15
+ s.rubyforge_project = "gruesome"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_development_dependency "rspec"
23
+
24
+ s.add_dependency "bit-struct"
25
+ end
@@ -0,0 +1,70 @@
1
+ require 'optparse'
2
+
3
+ require_relative '../gruesome'
4
+ require_relative 'machine'
5
+ require_relative 'logo'
6
+
7
+ module Gruesome
8
+ class CLI
9
+ BANNER = <<-USAGE
10
+ Usage:
11
+ gruesome play STORY_FILE
12
+
13
+ Description:
14
+ The 'play' command will start a session of the story given as STORY_FILE
15
+
16
+ Example:
17
+ gruesome play zork1.z3
18
+
19
+ USAGE
20
+
21
+ class << self
22
+ def parse_options
23
+ @opts = OptionParser.new do |opts|
24
+ opts.banner = BANNER.gsub(/^\t{2}/, '')
25
+
26
+ opts.separator ''
27
+ opts.separator 'Options:'
28
+
29
+ opts.on('-h', '--help', 'Display this help') do
30
+ puts opts
31
+ exit
32
+ end
33
+ end
34
+
35
+ @opts.parse!
36
+ end
37
+
38
+ def CLI.run
39
+ begin
40
+ parse_options
41
+ rescue OptionParser::InvalidOption => e
42
+ warn e
43
+ exit -1
44
+ end
45
+
46
+ def fail
47
+ puts @opts
48
+ exit -1
49
+ end
50
+
51
+ if ARGV.empty?
52
+ fail
53
+ end
54
+
55
+ case ARGV.first
56
+ when 'play'
57
+ fail unless ARGV[1]
58
+
59
+ Gruesome::Logo.print
60
+
61
+ puts
62
+ puts "--------------------------------------------------------------------------------"
63
+ puts
64
+
65
+ Gruesome::Machine.new(ARGV[1]).execute
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+ #
3
+ # DO NOT REMOVE ABOVE COMMENT! IT IS MAGIC!
4
+
5
+ module Gruesome
6
+ module Logo
7
+ # I'm just making sure this works regardless of the unicode format this
8
+ # source file is saved as...
9
+ LOGO = <<ENDLOGO
10
+ ▄▄▄▄▄ ▄▄▄▄▄
11
+ ▀████▄ ▄████▀
12
+ ▀████▄ ▄████▀
13
+ ▄▄▄▄▄▄ ██████▄ ▄▄▄ ▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄▄▄ ▄████▀ ▄▄▄ ▄▄▄ ▄▄▄▄▄▄
14
+ ▄████████▄ ██▓▀████▄ ██▓ ███ ▄███████ ▄████████▄ ▄████▀██▄ ████▄████ ▄███████
15
+ ██▓ ███ ██▓ ▀███ ██▓ ███ ██▓ ██▓ ███ ███▀ ███ █████████ ██▓
16
+ ██▓ ██▓ ███ ██▓ ███ ██▓ ██▓ ██▓ ███ ███▀█▀███ ██▓
17
+ ██▓ █████ ████████ ███ ███ ████████ ▀███████▄ ██▓ ███ ██▓ ███ ████████
18
+ ██▓ ▀▀███ ▀██▓▀▀▀█▄▄ ███ ███ ███▀▀▀▀▀ ▀▀▀▀▀███ ██▓ ███ ██▓ ███ ███▀▀▀▀▀
19
+ ██▓ ███ ██▓ ███ ██▓ ███ ██▓ ██▓ ███ ██▓ ███ ███ ███ ██▓
20
+ ▀████████▀ ██▓ ███ ▀███████▀ ▀███████ ▀████████▀ ▀███████▀ ██▓ ███ ▀███████
21
+ ▀▀▀▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀▀▀ ▀▀▀▀▀▀ ▀▀▀▀▀▀ ▀▀▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀▀▀▀
22
+ ENDLOGO
23
+
24
+ def Logo.print
25
+ puts LOGO
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'z/machine'
2
+
3
+ module Gruesome
4
+ class Machine
5
+ def initialize(story_file)
6
+ # Later, detect the type
7
+ #
8
+ # For now, assume Z-Machine
9
+
10
+ @machine = Z::Machine.new(story_file)
11
+ end
12
+
13
+ def execute
14
+ @machine.execute
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Grue
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,30 @@
1
+ # The Z-Machine, in order to conserve precious space, often encoded in various
2
+ # strings a codeword that would inject the word given at a particular place in
3
+ # the abbreviation table.
4
+
5
+ require_relative 'memory'
6
+ require_relative 'header'
7
+ require_relative 'zscii'
8
+
9
+ module Gruesome
10
+ module Z
11
+ class AbbreviationTable
12
+ def initialize(memory)
13
+ @memory = memory
14
+ @header = Header.new(@memory.contents)
15
+ end
16
+
17
+ def lookup(alphabet, index, translation_alphabet)
18
+ abbrev_index = (32 * (alphabet)) + index
19
+ addr = @header.abbrev_tbl_addr + abbrev_index*2
20
+
21
+ # this will yield a word address, which we multiply by 2
22
+ # to get into a byte address
23
+ str_addr = @memory.force_readw(addr)
24
+ str_addr *= 2
25
+
26
+ ZSCII.translate(translation_alphabet, @header.version, @memory.force_readzstr(str_addr)[1], nil)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,292 @@
1
+ # The Z-Machine has a RISC-like instruction set with 1 byte opcodes with
2
+ # the exception of several extension opcodes of 2 bytes.
3
+ #
4
+ # The operands are variable length, however, mostly for size constraint reasons.
5
+ #
6
+ # The format:
7
+ #
8
+ # OPCODE :: 1 or 2 bytes
9
+ # OPCODE_TYPE (opt) :: 1 to 2 bytes, divided into 2-bit fields
10
+ # OPERANDS :: 0 to 8 given, each 1 or 2 bytes, or an unlimited length string
11
+
12
+ require_relative 'opcode'
13
+ require_relative 'operand_type'
14
+ require_relative 'opcode_class'
15
+ require_relative 'instruction'
16
+ require_relative 'zscii'
17
+
18
+ module Gruesome
19
+ module Z
20
+
21
+ # This is the instruction decoder
22
+ class Decoder
23
+ def initialize(memory)
24
+ @memory = memory
25
+ @instruction_cache = {}
26
+ @header = Header.new(@memory.contents)
27
+ @abbreviation_table = AbbreviationTable.new(@memory)
28
+
29
+ # For versions 1 and 2, there is a permanent alphabet
30
+ @alphabet = 0
31
+ end
32
+
33
+ def fetch
34
+ pc = @memory.program_counter
35
+ orig_pc = pc
36
+
37
+ # Determine type and form of the operand
38
+ # along with the number of operands
39
+
40
+ # read first byte to get opcode
41
+ opcode = @memory.force_readb(pc)
42
+ pc = pc + 1
43
+
44
+ # opcode form is top 2 bits
45
+ opcode_form = (opcode >> 6) & 3
46
+ operand_count = 0
47
+ operand_types = Array.new(8) { OperandType::OMITTED }
48
+ operand_values = []
49
+
50
+ # SHORT
51
+ if opcode_form == 2
52
+ # operand count is determined by bits 4 and 5
53
+ if ((opcode >> 4) & 3) == 3
54
+ operand_count = 0
55
+ opcode_class = OpcodeClass::OP0
56
+ else
57
+ operand_count = 1
58
+ opcode_class = OpcodeClass::OP1
59
+ end
60
+
61
+ operand_types[0] = (opcode >> 4) & 3
62
+
63
+ # opcode is given as bottom 4 bits
64
+ opcode = opcode & 0b1111
65
+
66
+ # VARIABLE
67
+ elsif opcode_form == 3
68
+ if (opcode & 0b100000) == 0
69
+ # when bit 5 is clear, there are two operands
70
+ operand_count = 2
71
+ opcode_class = OpcodeClass::OP2
72
+ else
73
+ # otherwise, there are VAR number
74
+ operand_count = 8
75
+ opcode_class = OpcodeClass::VAR
76
+ end
77
+
78
+ # opcode is given as bottom 5 bits
79
+ opcode = opcode & 0b11111
80
+
81
+ # EXTENDED
82
+ elsif opcode == 190 # extended form
83
+ opcode_class = OpcodeClass::EXT
84
+
85
+ # VAR number
86
+ operand_count = 8
87
+
88
+ # opcode is given as the next byte
89
+ opcode = @memory.force_readb(pc)
90
+ pc = pc + 1
91
+
92
+ # LONG
93
+ else
94
+
95
+ # there are always 2 operands
96
+ operand_count = 2
97
+ opcode_class = OpcodeClass::OP2
98
+
99
+ # bit 6 of opcode is type of operand 1
100
+ type = opcode & 0b1000000
101
+ if type == 0 # 0 means small constant
102
+ type = OperandType::SMALL
103
+ else # 1 means variable
104
+ type = OperandType::VARIABLE
105
+ end
106
+ operand_types[0] = type
107
+
108
+ # bit 5 of opcode is type of operand 2
109
+ type = opcode & 0b100000
110
+ if type == 0 # 0 means small constant
111
+ type = OperandType::SMALL
112
+ else # 1 means variable
113
+ type = OperandType::VARIABLE
114
+ end
115
+ operand_types[1] = type
116
+
117
+ # opcode is given as bottom 5 bits
118
+ opcode = opcode & 0b11111
119
+ end
120
+
121
+ # We need the opcode and opcode_class to be combined
122
+ opcode = (opcode << 3) | opcode_class
123
+
124
+ # convert some moved opcodes
125
+ if (@header.version <= 4)
126
+ if opcode == Opcode::CALL_1N
127
+ opcode = Opcode::NOT
128
+ end
129
+ end
130
+
131
+ # handle VAR operands
132
+ if opcode_form == 3 or opcode_class == OpcodeClass::VAR or opcode_class == OpcodeClass::EXT
133
+ # each type for the operands is given by reading
134
+ # the next 1 or 2 bytes.
135
+ #
136
+ # This byte contains 4 type descriptions where
137
+ # the most significant 2 bits are the 0th type
138
+ # and the least 2 are the 3rd type
139
+ #
140
+ # If a type is deemed omitted, every subsequent
141
+ # type must also be omitted
142
+
143
+ byte = @memory.force_readb(pc)
144
+ pc = pc + 1
145
+
146
+ operand_types[0] = (byte >> 6) & 3
147
+ operand_types[1] = (byte >> 4) & 3
148
+ operand_types[2] = (byte >> 2) & 3
149
+ operand_types[3] = byte & 3
150
+
151
+ # Get the number of operands
152
+ idx = -1
153
+ first_omitted = -1
154
+ operand_count = operand_types.inject(0) do |result, element|
155
+ idx = idx + 1
156
+ if element == OperandType::OMITTED
157
+ first_omitted = idx
158
+ result
159
+ elsif first_omitted == -1
160
+ result + 1
161
+ else
162
+ # Error, OMITTED was found, but another type
163
+ # was defined as not omitted
164
+ # We will ignore
165
+ result
166
+ end
167
+ end
168
+
169
+ if opcode == Opcode::CALL_VS2 or opcode == Opcode::CALL_VN2
170
+ # Certain opcodes can have up to 8 operands!
171
+ # These are given by a second byte
172
+ byte = @memory.force_readb(pc)
173
+ pc = pc + 1
174
+
175
+ operand_types[4] = (byte >> 6) & 3
176
+ operand_types[5] = (byte >> 4) & 3
177
+ operand_types[6] = (byte >> 2) & 3
178
+ operand_types[7] = byte & 3
179
+
180
+ # update operand_count once more
181
+ operand_count = operand_types.inject(operand_count) do |result, element|
182
+ idx = idx + 1
183
+ if element == OperandType::OMITTED
184
+ first_omitted = idx
185
+ result
186
+ elsif first_omitted == -1
187
+ result + 1
188
+ else
189
+ # Error, OMITTED was found, but another type
190
+ # was defined as not omitted
191
+ # We will ignore
192
+ result
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ # Retrieve the operand values
199
+ operand_types = operand_types.slice(0, operand_count)
200
+ operand_types.each do |i|
201
+ if i == OperandType::SMALL or i == OperandType::VARIABLE
202
+ operand_values << @memory.force_readb(pc)
203
+ pc = pc + 1
204
+ elsif i == OperandType::LARGE
205
+ operand_values << @memory.force_readw(pc)
206
+ pc = pc + 2
207
+ end
208
+ end
209
+
210
+ # If the opcode stores, we need to pull the next byte to get the
211
+ # destination of the result
212
+ destination = nil
213
+ if Opcode.is_store?(opcode, @header.version)
214
+ destination = @memory.force_readb(pc)
215
+ pc = pc + 1
216
+ end
217
+
218
+ # The opcode may indicate that it's argument is a string literal
219
+ if Opcode.has_string?(opcode, @header.version)
220
+ str = ""
221
+ # Now we read in 2-byte words until the most significant bit is set
222
+ # We unencode them from the ZSCII encoding
223
+
224
+ continue = true
225
+
226
+ alphabet = 0
227
+ if (@header.version < 3)
228
+ alphabet = @alphabet
229
+ end
230
+
231
+ result = @memory.force_readzstr(pc)
232
+ pc = pc + result[0]
233
+ chrs = result[1]
234
+
235
+ # convert the string from ZSCII to UTF8
236
+ operand_types << OperandType::STRING
237
+ operand_values << ZSCII.translate(@alphabet, @header.version, chrs, @abbreviation_table)
238
+
239
+ # determine shift locks
240
+ if (@header.version < 3)
241
+ @alphabet = ZSCII.eval_alphabet(@alphabet, @header.version, chrs, @abbreviation_table)
242
+ end
243
+ end
244
+
245
+ # If the opcode is a branch, we need to pull the offset info
246
+ branch_destination = nil
247
+ branch_condition = false
248
+ if Opcode.is_branch?(opcode, @header.version)
249
+ branch_offset = @memory.force_readb(pc)
250
+ pc = pc + 1
251
+
252
+ # if bit 7 is set, the branch occurs on a true condition
253
+ # false otherwise
254
+ if (branch_offset & 0b10000000) != 0
255
+ branch_condition = true
256
+ end
257
+
258
+ # if bit 6 is clear, the branch offset is 14 bits (6 from first byte
259
+ # and the 8 from the next byte) This is _signed_
260
+ if (branch_offset & 0b01000000) == 0
261
+ branch_offset = branch_offset & 0b111111
262
+ negative = (branch_offset & 0b100000) > 0
263
+ branch_offset = branch_offset << 8
264
+ branch_offset = branch_offset | @memory.force_readb(pc)
265
+ if (negative)
266
+ branch_offset = -(16384 - branch_offset)
267
+ end
268
+ pc = pc + 1
269
+ else # otherwise, the offset is simply the remaining 6 bits _unsigned_
270
+ branch_offset = branch_offset & 0b111111
271
+ end
272
+
273
+ # calculate actual destination from the offset
274
+ if branch_offset == 0 or branch_offset == 1
275
+ # a return
276
+ branch_destination = branch_offset
277
+ else
278
+ branch_destination = pc + branch_offset - 2
279
+ end
280
+ end
281
+
282
+ # Create an Instruction class to hold this metadata
283
+ inst = Instruction.new(opcode, operand_types, operand_values, destination, branch_destination, branch_condition, pc - orig_pc)
284
+
285
+ # Store in the instruction cache
286
+ @instruction_cache[orig_pc] = inst
287
+
288
+ inst
289
+ end
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,156 @@
1
+ # The Z-Machine has a dictionary which it uses to parse input and
2
+ # perform lexical analysis.
3
+
4
+ # The header is located at the address given in the Header at
5
+ # address 0x08
6
+
7
+ require_relative 'memory'
8
+ require_relative 'header'
9
+ require_relative 'zscii'
10
+
11
+ module Gruesome
12
+ module Z
13
+ class Dictionary
14
+ def initialize(memory)
15
+ @memory = memory
16
+ @header = Header.new(@memory.contents)
17
+
18
+ @abbreviation_table = AbbreviationTable.new(@memory)
19
+
20
+ @lookup_cache = {}
21
+ end
22
+
23
+ def address
24
+ @header.dictionary_addr
25
+ end
26
+
27
+ def address_of_entries
28
+ addr = self.address
29
+ num_input_codes = @memory.force_readb(addr)
30
+ addr += 1
31
+ addr += num_input_codes
32
+ addr += 3
33
+
34
+ addr
35
+ end
36
+
37
+ def entry_length
38
+ addr = self.address_of_entries - 3
39
+ return @memory.force_readb(addr)
40
+ end
41
+
42
+ def number_of_words
43
+ addr = self.address_of_entries - 2
44
+ return @memory.force_readw(addr)
45
+ end
46
+
47
+ def word_address(index)
48
+ addr = address_of_entries
49
+ addr += entry_length * index
50
+
51
+ return addr
52
+ end
53
+
54
+ def word(index)
55
+ addr = word_address(index)
56
+
57
+ codes = nil
58
+
59
+ if @header.version <= 3
60
+ codes = @memory.force_readzstr(addr, 4)[1]
61
+ else
62
+ codes = @memory.force_readzstr(addr, 6)[1]
63
+ end
64
+
65
+ str = ZSCII.translate(0, @header.version, codes, @abbreviation_table)
66
+ @lookup_cache[str] = index
67
+ return str
68
+ end
69
+
70
+ def lookup_word(token)
71
+ if token.length > 6 and @header.version <= 3
72
+ token = token[0..5]
73
+ elsif token.length > 9
74
+ token = token[0..8]
75
+ end
76
+ cached_index = @lookup_cache[token]
77
+ if cached_index != nil
78
+ if self.word(cached_index) == token
79
+ return cached_index
80
+ end
81
+ end
82
+
83
+ number_of_words.times do |i|
84
+ if word(i) == token
85
+ cached_index = i
86
+ break
87
+ end
88
+ end
89
+
90
+ cached_index
91
+ end
92
+
93
+ def word_separators
94
+ addr = self.address
95
+
96
+ # the number of word separators
97
+ num_input_codes = @memory.force_readb(addr)
98
+ addr += 1
99
+
100
+ codes = []
101
+ num_input_codes.times do
102
+ codes << @memory.force_readb(addr)
103
+ addr += 1
104
+ end
105
+
106
+ codes = codes.map do |i|
107
+ ZSCII.translate_Zchar(i)
108
+ end
109
+
110
+ return codes
111
+ end
112
+
113
+ def tokenize(line)
114
+ orig_line = line
115
+
116
+ # I. ensure that word separators are surrounded by spaces
117
+ self.word_separators.each do |sep|
118
+ line = line.gsub(sep, " " + sep + " ")
119
+ end
120
+
121
+ # II. break up the line into words delimited by spaces
122
+ tokens = line.split(' ')
123
+
124
+ line = orig_line
125
+ last_pos = 0
126
+ ret = tokens.map do |token|
127
+ index = line.index(token)
128
+ line = line[index+token.size..-1]
129
+
130
+ index = index + last_pos
131
+ last_pos = index + token.size
132
+
133
+ index
134
+ { :position => index, :string => token }
135
+ end
136
+
137
+ ret
138
+ end
139
+
140
+ def parse(tokens)
141
+ ret = []
142
+ tokens.each do |token|
143
+ index = lookup_word(token[:string])
144
+ if index != nil
145
+ addr = word_address(index)
146
+ else
147
+ addr = 0
148
+ end
149
+ ret << { :size => token[:string].size, :address => addr, :position => token[:position] }
150
+ end
151
+
152
+ ret
153
+ end
154
+ end
155
+ end
156
+ end