rubylabs 0.5.5 → 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/lib/marslab.rb +1147 -0
- data/lib/randomlab.rb +436 -201
- data/lib/rubylabs.rb +15 -14
- data/test/mars_test.rb +519 -0
- data/test/random_test.rb +75 -19
- metadata +4 -4
- data/lib/temps.rb +0 -41
- data/test/temps_test.rb +0 -24
data/lib/marslab.rb
ADDED
@@ -0,0 +1,1147 @@
|
|
1
|
+
|
2
|
+
=begin rdoc
|
3
|
+
|
4
|
+
== Core Wars Lab
|
5
|
+
|
6
|
+
Ruby implementation of the MARS virtual machine used in Core Wars.
|
7
|
+
|
8
|
+
The machine implemented here conforms as close as possible to the ICWS'88 standard,
|
9
|
+
as described by Mark Durham in his Introduction to Redcode (http://corewar.co.uk/guides.htm).
|
10
|
+
|
11
|
+
Currently there is no attempt to translate instructions into a numerical format. MARS
|
12
|
+
code is a set of Word objects. Each Word has an opcode and two operands (A and B), all of
|
13
|
+
which are represented as strings. Integer data is represented by a Word where the opcode
|
14
|
+
field is DAT. Other Word objects have executable machine instructions for opcodes.
|
15
|
+
|
16
|
+
To test a single program students can make an instance of a class named MiniMARS, passing
|
17
|
+
it the name of a test program. The test machine will have a small memory, only big enough
|
18
|
+
to hold the test machine, and special versions of the step, dump and other methods.
|
19
|
+
|
20
|
+
The main machine that runs one, two, or three programs simultaneously is in the module
|
21
|
+
named MARS. Methods that assemble, load, and run programs are module methods, e.g.
|
22
|
+
to assemble a program call MARS.assemble(foo).
|
23
|
+
|
24
|
+
=end
|
25
|
+
|
26
|
+
# TODO add [] and []= methods to Word, use it to extract/assign bits or ranges of bits
|
27
|
+
# TODO define ranges for fields, e.g. @@opcode = 0..3; use to get opcode of Word, e.g. instr[@@opcode]
|
28
|
+
# TODO make sure all code uses [] interface (e.g. no more instr.amode[0] == ?#)
|
29
|
+
# TODO pack words into binary -- will kill several birds at once -- speed, trim values to 0..4095, ..
|
30
|
+
# TODO color a cell black when a thread halts after executing an instruction in that cell
|
31
|
+
# TODO MARS.dump
|
32
|
+
# TODO pass load address to contest (which passes it on to Warrior.load)?
|
33
|
+
|
34
|
+
module RubyLabs
|
35
|
+
|
36
|
+
module MARSLab
|
37
|
+
|
38
|
+
class MARSRuntimeException < StandardError
|
39
|
+
end
|
40
|
+
|
41
|
+
class RedcodeSyntaxError < StandardError
|
42
|
+
end
|
43
|
+
|
44
|
+
# An object of the Word class represents a single item from memory, either a machine
|
45
|
+
# instruction or a piece of data. Attributes are the opcode, the A operand mode, and
|
46
|
+
# the B operand (all strings). Instruction execution proceeds according to the description
|
47
|
+
# in Durham's spec.
|
48
|
+
|
49
|
+
class Word
|
50
|
+
|
51
|
+
attr_reader :op, :lineno
|
52
|
+
attr_accessor :a, :b
|
53
|
+
|
54
|
+
def initialize(*args)
|
55
|
+
@op, @a, @b, @lineno = args
|
56
|
+
end
|
57
|
+
|
58
|
+
def inspect
|
59
|
+
sprintf "%s %s %s", @op, @a, @b
|
60
|
+
end
|
61
|
+
|
62
|
+
alias to_s inspect
|
63
|
+
|
64
|
+
# two Words are the same if the strings representing the opcode and
|
65
|
+
# both operands are the same
|
66
|
+
|
67
|
+
def ==(other)
|
68
|
+
return (op == other.op && a == other.a && b == other.b)
|
69
|
+
end
|
70
|
+
|
71
|
+
def clone
|
72
|
+
Word.new(@op.clone, @a.clone, @b.clone, @lineno)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Convert a field specification into an integer value, stripping off a leading
|
76
|
+
# addressing mode character if it's there
|
77
|
+
|
78
|
+
def field_value(field)
|
79
|
+
return @@modes.include?(field[0]) ? field[1..-1].to_i : field.to_i
|
80
|
+
end
|
81
|
+
|
82
|
+
# Return the address of an operand; note that for immediate operands the
|
83
|
+
# address is the address of the current instruction.
|
84
|
+
|
85
|
+
def dereference(field, pc, mem)
|
86
|
+
case field[0]
|
87
|
+
when ?#
|
88
|
+
return pc.current[:addr]
|
89
|
+
when ?@
|
90
|
+
ptrloc = (field_value(field) + pc.current[:addr]) % mem.size
|
91
|
+
ptr = mem.fetch(ptrloc)
|
92
|
+
return (field_value(ptr.b) + ptrloc) % mem.size
|
93
|
+
when ?<
|
94
|
+
ptrloc = (field_value(field) + pc.current[:addr]) % mem.size
|
95
|
+
ptr = mem.fetch(ptrloc)
|
96
|
+
newb = field_value(ptr.b) - 1
|
97
|
+
mem.store_field(ptrloc, (newb % mem.size), :b)
|
98
|
+
return (newb + ptrloc) % mem.size
|
99
|
+
else
|
100
|
+
return (field_value(field) + pc.current[:addr]) % mem.size
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Execute an instruction. The first parameter is the program counter used
|
105
|
+
# to fetch the instruction, the second is the machine's memory array.
|
106
|
+
|
107
|
+
def execute(pc, mem)
|
108
|
+
self.send(@op, pc, mem)
|
109
|
+
return (@op == "DAT") ? (:halt) : (:continue)
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
# The DAT instruction is effectively a "halt", but we still need to dereference
|
115
|
+
# both its operands to generate the side effects in auto-decrement modes.
|
116
|
+
|
117
|
+
def DAT(pc, mem)
|
118
|
+
dereference(@a, pc, mem)
|
119
|
+
dereference(@b, pc, mem)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Durham isn't clear on how to handle immediate moves -- does the immediate value
|
123
|
+
# go in the A or B field of the destination? Guess: B, in case the destination
|
124
|
+
# is a DAT.
|
125
|
+
|
126
|
+
def MOV(pc, mem)
|
127
|
+
raise MARSRuntimeException, "MOV: immediate B-field not allowed" if @b[0] == ?#
|
128
|
+
src = dereference(@a, pc, mem)
|
129
|
+
val = mem.fetch(src)
|
130
|
+
dest = dereference(@b, pc, mem)
|
131
|
+
if @a[0] == ?#
|
132
|
+
mem.store_field(dest, field_value(val.a), :b)
|
133
|
+
else
|
134
|
+
mem.store(dest, val)
|
135
|
+
end
|
136
|
+
pc.log(src)
|
137
|
+
pc.log(dest)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Ambiguity on how to handle immediate A values: add operand to the A or B
|
141
|
+
# field of the destination? Guess: B (for same reasons given for MOV)
|
142
|
+
|
143
|
+
def ADD(pc, mem)
|
144
|
+
raise MARSRuntimeException, "ADD: immediate B-field not allowed" if @b[0] == ?#
|
145
|
+
src = dereference(@a, pc, mem)
|
146
|
+
left_operand = mem.fetch(src)
|
147
|
+
dest = dereference(@b, pc, mem)
|
148
|
+
right_operand = mem.fetch(dest)
|
149
|
+
if @a[0] == ?#
|
150
|
+
mem.store_field(dest, field_value(left_operand.a) + field_value(right_operand.b), :b)
|
151
|
+
else
|
152
|
+
mem.store_field(dest, field_value(left_operand.a) + field_value(right_operand.a), :a)
|
153
|
+
mem.store_field(dest, field_value(left_operand.b) + field_value(right_operand.b), :b)
|
154
|
+
end
|
155
|
+
pc.log(src)
|
156
|
+
pc.log(dest)
|
157
|
+
end
|
158
|
+
|
159
|
+
# See note for ADD, re immediate A operand.
|
160
|
+
|
161
|
+
def SUB(pc, mem)
|
162
|
+
raise MARSRuntimeException, "SUB: immediate B-field not allowed" if @b[0] == ?#
|
163
|
+
src = dereference(@a, pc, mem)
|
164
|
+
right_operand = mem.fetch(src)
|
165
|
+
dest = dereference(@b, pc, mem)
|
166
|
+
left_operand = mem.fetch(dest)
|
167
|
+
if @a[0] == ?#
|
168
|
+
mem.store_field(dest, field_value(left_operand.b) - field_value(right_operand.a), :b)
|
169
|
+
else
|
170
|
+
mem.store_field(dest, field_value(left_operand.a) - field_value(right_operand.a), :a)
|
171
|
+
mem.store_field(dest, field_value(left_operand.b) - field_value(right_operand.b), :b)
|
172
|
+
end
|
173
|
+
pc.log(src)
|
174
|
+
pc.log(dest)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Durham doesn't mention this explicitly, but since a B operand is allowed it implies
|
178
|
+
# we have to dereference it in case it has a side effect.
|
179
|
+
|
180
|
+
def JMP(pc, mem)
|
181
|
+
raise MARSRuntimeException, "JMP: immediate A-field not allowed" if @a[0] == ?#
|
182
|
+
target = dereference(@a, pc, mem) % mem.size
|
183
|
+
dereference(@b, pc, mem)
|
184
|
+
pc.branch(target)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Branch to address specified by A if the B-field of the B operand is zero.
|
188
|
+
|
189
|
+
def JMZ(pc, mem)
|
190
|
+
raise MARSRuntimeException, "JMZ: immediate A-field not allowed" if @a[0] == ?#
|
191
|
+
target = dereference(@a, pc, mem) % mem.size
|
192
|
+
operand = mem.fetch(dereference(@b, pc, mem))
|
193
|
+
if field_value(operand.b) == 0
|
194
|
+
pc.branch(target)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# As in JMZ, but branch if operand is non-zero
|
199
|
+
|
200
|
+
def JMN(pc, mem)
|
201
|
+
raise MARSRuntimeException, "JMN: immediate A-field not allowed" if @a[0] == ?#
|
202
|
+
target = dereference(@a, pc, mem) % mem.size
|
203
|
+
operand = mem.fetch(dereference(@b, pc, mem))
|
204
|
+
if field_value(operand.b) != 0
|
205
|
+
pc.branch(target)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# DJN combines the auto-decrement mode dereference logic with a branch -- take
|
210
|
+
# the branch if the new value of the B field of the pointer is non-zero.
|
211
|
+
|
212
|
+
def DJN(pc, mem)
|
213
|
+
raise MARSRuntimeException, "DJN: immediate A-field not allowed" if @a[0] == ?#
|
214
|
+
target = dereference(@a, pc, mem)
|
215
|
+
operand_addr = dereference(@b, pc, mem)
|
216
|
+
operand = mem.fetch(operand_addr)
|
217
|
+
newb = field_value(operand.b) - 1
|
218
|
+
mem.store_field(operand_addr, (newb % mem.size), :b)
|
219
|
+
if newb != 0
|
220
|
+
pc.branch(target)
|
221
|
+
end
|
222
|
+
pc.log(operand_addr)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Durham just says "compare two fields" if the operand is immediate. Since
|
226
|
+
# B can't be immediate, we just need to look at the A operand and, presumably,
|
227
|
+
# compare it to the A operand of the dereferenced operand fetched by the B field.
|
228
|
+
# If A is not immediate compare two full Words -- including op codes.
|
229
|
+
# The call to pc.next increments the program counter for this thread, which causes
|
230
|
+
# the skip.
|
231
|
+
|
232
|
+
def CMP(pc, mem)
|
233
|
+
raise MARSRuntimeException, "CMP: immediate B-field not allowed" if @b[0] == ?#
|
234
|
+
right = mem.fetch(dereference(@b, pc, mem))
|
235
|
+
if @a[0] == ?#
|
236
|
+
left = field_value(@a)
|
237
|
+
right = field_value(right.a)
|
238
|
+
else
|
239
|
+
left = mem.fetch(dereference(@a, pc, mem))
|
240
|
+
end
|
241
|
+
if left == right
|
242
|
+
pc.next
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Major ambiguity here -- what does it mean for an word A to be "less than"
|
247
|
+
# word B? First assumption, don't compare opcodes. Second, we're just going
|
248
|
+
# to implement one-field comparisons of B fields. Otherwise for full-word operands
|
249
|
+
# we'd need to do a lexicographic compare of A and B fields of both operands, skipping
|
250
|
+
# modes and just comparing values.
|
251
|
+
|
252
|
+
def SLT(pc, mem)
|
253
|
+
raise MARSRuntimeException, "SLT: immediate B-field not allowed" if @b[0] == ?#
|
254
|
+
if @a[0] == ?#
|
255
|
+
left = field_value(@a)
|
256
|
+
else
|
257
|
+
left = field_value(mem.fetch(dereference(@a, pc, mem)).b)
|
258
|
+
end
|
259
|
+
right = field_value(mem.fetch(dereference(@b, pc, mem)).b)
|
260
|
+
if left < right
|
261
|
+
pc.next
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Fork a new thread at the address specified by A. The new thread goes at the end
|
266
|
+
# of the queue. Immediate operands are not allowed. Durham doesn't mention it, but
|
267
|
+
# implies only A is dereferenced, so ignore B.
|
268
|
+
|
269
|
+
def SPL(pc, mem)
|
270
|
+
raise MARSRuntimeException, "SPL: immediate A-field not allowed" if @a[0] == ?#
|
271
|
+
target = dereference(@a, pc, mem)
|
272
|
+
pc.add_thread(target)
|
273
|
+
end
|
274
|
+
|
275
|
+
end # class Word
|
276
|
+
|
277
|
+
|
278
|
+
=begin rdoc
|
279
|
+
A Warrior is a core warrior -- an object of this class is a simple struct that
|
280
|
+
has the program name, assembled code, the starting location, and the symbol table.
|
281
|
+
|
282
|
+
A static method (Warrior.load) will load an assembled Warrior into memory, making
|
283
|
+
sure the max number of programs is not exceeded. The addr attribute is set to the
|
284
|
+
starting location of the program when it is loaded.
|
285
|
+
=end
|
286
|
+
|
287
|
+
class Warrior
|
288
|
+
attr_reader :name, :code, :symbols, :errors
|
289
|
+
|
290
|
+
def initialize(filename)
|
291
|
+
if filename.class == Symbol
|
292
|
+
filename = File.join(@@dataDirectory, filename.to_s + ".txt")
|
293
|
+
end
|
294
|
+
|
295
|
+
if ! File.exists?(filename)
|
296
|
+
raise "Can't find file: #{filename}"
|
297
|
+
end
|
298
|
+
|
299
|
+
@name, @code, @symbols, @errors = MARS.assemble( File.open(filename).readlines )
|
300
|
+
|
301
|
+
if @errors.length > 0
|
302
|
+
puts "Syntax errors in #{filename}:"
|
303
|
+
puts errors
|
304
|
+
@code.clear
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def inspect
|
309
|
+
sprintf "Name: #{@name} Code: #{@code.inspect}"
|
310
|
+
end
|
311
|
+
|
312
|
+
alias to_s inspect
|
313
|
+
|
314
|
+
def Warrior.load(app, addr = :random)
|
315
|
+
if app.class != Warrior
|
316
|
+
puts "Argument must be a Warrior object, not a #{app.class}"
|
317
|
+
return nil
|
318
|
+
end
|
319
|
+
|
320
|
+
if @@entries.length == @@maxEntries
|
321
|
+
puts "Maximum #{@@maxEntries} programs can be loaded at one time"
|
322
|
+
return nil
|
323
|
+
end
|
324
|
+
|
325
|
+
if addr == :random
|
326
|
+
loop do
|
327
|
+
addr = rand(@@mem.size)
|
328
|
+
break if Warrior.check_loc(addr, addr + app.code.length - 1)
|
329
|
+
end
|
330
|
+
else
|
331
|
+
if ! Warrior.check_loc(addr, addr + app.code.length - 1)
|
332
|
+
puts "Address #{addr} too close to another program; #{app.name} not loaded"
|
333
|
+
return nil
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
loc = addr
|
338
|
+
app.code.each do |x|
|
339
|
+
@@mem.store(loc, x)
|
340
|
+
loc = (loc + 1) % @@mem.size
|
341
|
+
end
|
342
|
+
|
343
|
+
id = @@entries.length
|
344
|
+
@@pcs << PC.new(id, addr + app.symbols[:start])
|
345
|
+
@@entries << app
|
346
|
+
|
347
|
+
# save the range of memory locations reserved by this app
|
348
|
+
|
349
|
+
lb = addr - @@params[:buffer]
|
350
|
+
ub = addr + app.code.length + @@params[:buffer] - 1
|
351
|
+
if lb < 0
|
352
|
+
@@in_use << ( 0 .. (addr+app.code.length-1) )
|
353
|
+
@@in_use << ( (@@params[:memSize] + lb) .. (@@params[:memSize]-1) )
|
354
|
+
elsif ub > @@params[:memSize]
|
355
|
+
@@in_use << ( addr .. (@@params[:memSize]-1) )
|
356
|
+
@@in_use << ( 0 .. (ub - @@params[:memSize]) )
|
357
|
+
else
|
358
|
+
@@in_use << (lb..ub)
|
359
|
+
end
|
360
|
+
|
361
|
+
return addr
|
362
|
+
end
|
363
|
+
|
364
|
+
@@in_use = Array.new
|
365
|
+
|
366
|
+
def Warrior.reset
|
367
|
+
@@in_use = Array.new
|
368
|
+
end
|
369
|
+
|
370
|
+
private
|
371
|
+
|
372
|
+
# Return true if a program loaded between lb and ub would not overlap another
|
373
|
+
# program already loaded (including a buffer surrounding loaded programs).
|
374
|
+
|
375
|
+
def Warrior.check_loc(lb, ub)
|
376
|
+
@@in_use.each do |range|
|
377
|
+
return false if range.include?(lb) || range.include?(ub)
|
378
|
+
end
|
379
|
+
return true
|
380
|
+
end
|
381
|
+
|
382
|
+
end # class Warrior
|
383
|
+
|
384
|
+
|
385
|
+
=begin rdoc
|
386
|
+
The PC class is used to represent the set of program counters for a running program.
|
387
|
+
It has an array of locations to hold the next instruction from each thread, plus the
|
388
|
+
index of the thread to use on the next instruction fetch cycle.
|
389
|
+
|
390
|
+
Call next to get the address of the next instruction to execute; as a side effect this
|
391
|
+
call bumps the thread pointer to the next thread in the program. Call add_thread(n)
|
392
|
+
to start a new thread running at location n. Call kill_thread to remove the thread
|
393
|
+
that was used in the most recent call to next (i.e. when that program dies).
|
394
|
+
|
395
|
+
To help visualize the operation of a program the class keeps a history of memory
|
396
|
+
references made by each thread. A call to next automatically records the program counter
|
397
|
+
value, but a semantic routine can also all log(x) to append location x to a history.
|
398
|
+
Call history to get the history vectors for all threads.
|
399
|
+
=end
|
400
|
+
|
401
|
+
class PC
|
402
|
+
attr_reader :id, :thread, :addrs, :current
|
403
|
+
|
404
|
+
@@hmax = 10 # see also @@threadMax in Draw -- allowed to be a different value
|
405
|
+
|
406
|
+
def initialize(id, addr)
|
407
|
+
@id = id
|
408
|
+
@addrs = [addr]
|
409
|
+
@history = [ Array.new ]
|
410
|
+
@thread = 0
|
411
|
+
@current = {:thread => nil, :addr => nil}
|
412
|
+
@first = addr
|
413
|
+
end
|
414
|
+
|
415
|
+
def reset
|
416
|
+
@addrs = [@first]
|
417
|
+
@history.clear
|
418
|
+
@thread = 0
|
419
|
+
@current = {:thread => nil, :addr => nil}
|
420
|
+
return @first
|
421
|
+
end
|
422
|
+
|
423
|
+
def inspect
|
424
|
+
s = "[ "
|
425
|
+
@addrs.each_with_index do |x,i|
|
426
|
+
s << "*" if i == @thread
|
427
|
+
s << x.to_s
|
428
|
+
s << " "
|
429
|
+
end
|
430
|
+
s << "]"
|
431
|
+
end
|
432
|
+
|
433
|
+
alias to_s inspect
|
434
|
+
|
435
|
+
# Not sure if this is right -- new thread appended to list, as per Durham, but
|
436
|
+
# the execution stays with the current thread (thread switch happens in 'next' method)
|
437
|
+
|
438
|
+
def add_thread(addr)
|
439
|
+
@addrs << addr
|
440
|
+
@history << Array.new
|
441
|
+
self
|
442
|
+
end
|
443
|
+
|
444
|
+
def next
|
445
|
+
return nil if @addrs.empty?
|
446
|
+
addr = @addrs[@thread]
|
447
|
+
@current[:thread] = @thread
|
448
|
+
@current[:addr] = addr
|
449
|
+
@addrs[@thread] = (@addrs[@thread] + 1) % @@mem.size
|
450
|
+
@thread = (@thread + 1) % @addrs.size
|
451
|
+
log(addr)
|
452
|
+
return addr
|
453
|
+
end
|
454
|
+
|
455
|
+
def branch(loc)
|
456
|
+
@addrs[@current[:thread]] = loc
|
457
|
+
end
|
458
|
+
|
459
|
+
def kill_thread
|
460
|
+
return 0 if @addrs.empty?
|
461
|
+
@addrs.slice!(@current[:thread])
|
462
|
+
@history.slice!(@current[:thread])
|
463
|
+
@thread -= 1
|
464
|
+
return @addrs.length
|
465
|
+
end
|
466
|
+
|
467
|
+
=begin rdoc
|
468
|
+
record the location of a memory operation in the history vector for the current thread
|
469
|
+
=end
|
470
|
+
|
471
|
+
def log(loc)
|
472
|
+
a = @history[@current[:thread]]
|
473
|
+
a << loc
|
474
|
+
a.shift if a.length > @@hmax
|
475
|
+
end
|
476
|
+
|
477
|
+
def history
|
478
|
+
return @history
|
479
|
+
end
|
480
|
+
|
481
|
+
end # class PC
|
482
|
+
|
483
|
+
=begin rdoc
|
484
|
+
An object of the Memory class is a 1-D array of the specified size. Methods namde
|
485
|
+
store and fetch operate on full words, while store_field and fetch_field operate on partial words.
|
486
|
+
=end
|
487
|
+
|
488
|
+
class Memory
|
489
|
+
|
490
|
+
attr_reader :array
|
491
|
+
|
492
|
+
def initialize(size)
|
493
|
+
@array = Array.new(size)
|
494
|
+
end
|
495
|
+
|
496
|
+
def to_s
|
497
|
+
sprintf "Memory [0..#{@array.size-1}]"
|
498
|
+
end
|
499
|
+
|
500
|
+
=begin rdoc
|
501
|
+
dump(loc,n) -- print the n words in memory starting at loc
|
502
|
+
=end
|
503
|
+
|
504
|
+
def dump(loc, n)
|
505
|
+
(loc...(loc+n)).each do |x|
|
506
|
+
addr = x % @array.size
|
507
|
+
printf "%04d: %s\n", addr, self.fetch(addr)
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
def size
|
512
|
+
return @array.size
|
513
|
+
end
|
514
|
+
|
515
|
+
# Methods for fetching and storing data in memory. According to the standard,
|
516
|
+
# memory is initialized with DAT #0 instructions, but our array has nils; the
|
517
|
+
# fetch method returns a DAT #0 from an uninitialized location.
|
518
|
+
|
519
|
+
def fetch(loc)
|
520
|
+
return ( @array[loc] || Word.new("DAT", "#0", "#0", nil) )
|
521
|
+
end
|
522
|
+
|
523
|
+
def store(loc, val)
|
524
|
+
@array[loc] = val.clone
|
525
|
+
end
|
526
|
+
|
527
|
+
# fetch and store operations that work on partial words
|
528
|
+
|
529
|
+
def fetch_field(loc, field)
|
530
|
+
instr = self.fetch(loc)
|
531
|
+
return field == :a ? instr.a : instr.b
|
532
|
+
end
|
533
|
+
|
534
|
+
def store_field(loc, val, field)
|
535
|
+
instr = self.fetch(loc)
|
536
|
+
part = (field == :a) ? instr.a : instr.b
|
537
|
+
mode = @@modes.include?(part[0]) ? part[0].chr : ""
|
538
|
+
part.replace(mode + val.to_s)
|
539
|
+
self.store(loc, instr)
|
540
|
+
end
|
541
|
+
|
542
|
+
end # class Memory
|
543
|
+
|
544
|
+
=begin rdoc
|
545
|
+
A miniature machine (MiniMARS) object is used to test a program. Pass the name of
|
546
|
+
a Recode program to the constructor and get back a VM that has a memory where this
|
547
|
+
program has been loaded into location 0. Pass an extra parameter to define the size
|
548
|
+
of the memory (otherwise the memory is just big enough to hold the program). Call the
|
549
|
+
step method to execute a single instruction, or run to run the program until it hits
|
550
|
+
a DAT instruction or executes max instructions.
|
551
|
+
=end
|
552
|
+
|
553
|
+
class MiniMARS
|
554
|
+
attr_reader :mem, :pc, :state
|
555
|
+
|
556
|
+
def initialize(file, size = nil)
|
557
|
+
w = Warrior.new(file)
|
558
|
+
@mem = size ? Memory.new(size) : Memory.new(w.code.length)
|
559
|
+
loc = 0
|
560
|
+
w.code.each do |x|
|
561
|
+
@mem.store(loc, x)
|
562
|
+
loc = loc + 1
|
563
|
+
end
|
564
|
+
@pc = PC.new(w.name, w.symbols[:start])
|
565
|
+
@state = :ready
|
566
|
+
end
|
567
|
+
|
568
|
+
def inspect
|
569
|
+
return "#<MiniMARS mem = #{@mem.array.inspect} pc = #{@pc}>"
|
570
|
+
end
|
571
|
+
|
572
|
+
alias to_s inspect
|
573
|
+
|
574
|
+
def status
|
575
|
+
s = "Run: #{@state}"
|
576
|
+
s += " PC: #{@pc}" unless @state == :halt
|
577
|
+
puts s
|
578
|
+
end
|
579
|
+
|
580
|
+
def step
|
581
|
+
return "machine halted" if @state == :halt
|
582
|
+
instr = @mem.fetch(@pc.next)
|
583
|
+
@state = instr.execute(@pc, @mem)
|
584
|
+
return instr
|
585
|
+
end
|
586
|
+
|
587
|
+
def run(nsteps = 1000)
|
588
|
+
count = 0
|
589
|
+
loop do
|
590
|
+
break if @state == :halt || nsteps <= 0
|
591
|
+
self.step
|
592
|
+
nsteps -= 1
|
593
|
+
count += 1
|
594
|
+
end
|
595
|
+
return count
|
596
|
+
end
|
597
|
+
|
598
|
+
def reset
|
599
|
+
@pc.reset
|
600
|
+
puts "warning: memory may not be in initial state"
|
601
|
+
end
|
602
|
+
|
603
|
+
def dump(*args)
|
604
|
+
if args.empty?
|
605
|
+
@mem.dump(0, @mem.array.length)
|
606
|
+
else
|
607
|
+
x = args[0]
|
608
|
+
y = args[1] || x
|
609
|
+
@mem.dump(x, y-x+1)
|
610
|
+
end
|
611
|
+
return nil
|
612
|
+
end
|
613
|
+
|
614
|
+
end # MiniMARS
|
615
|
+
|
616
|
+
=begin rdoc
|
617
|
+
Make a window to display the state of the machine
|
618
|
+
=end
|
619
|
+
|
620
|
+
class Draw
|
621
|
+
|
622
|
+
@@cellSize = 8
|
623
|
+
@@cellRows = 32
|
624
|
+
@@cellCols = 128
|
625
|
+
@@padding = @@cellSize
|
626
|
+
@@traceSize = 10
|
627
|
+
@@cellColor = '#CCCCCC'
|
628
|
+
|
629
|
+
def paintCell(i, color)
|
630
|
+
@cells[i]['fill'] = color
|
631
|
+
end
|
632
|
+
|
633
|
+
def fillCanvas(mem)
|
634
|
+
@cells = []
|
635
|
+
mem.each_with_index do |val, i|
|
636
|
+
x = (i % @@cellCols) * @@cellSize + @@padding
|
637
|
+
y = (i / @@cellCols) * @@cellSize + @@padding
|
638
|
+
@cells << TkcRectangle.new( @canvas, x, y, x+@@cellSize, y+@@cellSize, :outline => "#888888", :fill => @@cellColor )
|
639
|
+
end
|
640
|
+
end
|
641
|
+
|
642
|
+
def reset
|
643
|
+
@cells.each do |cell|
|
644
|
+
cell['fill'] = @@cellColor
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
# Make a range of colors starting from first and going to last in n steps.
|
649
|
+
# First and last are expected to be 3-tuples of integer RGB values. The
|
650
|
+
# result is an array that starts with first, has n-1 intermediate colors,
|
651
|
+
# and ends with last. Example:
|
652
|
+
# makePalette( [255,0,0], [0,0,0], 10)
|
653
|
+
# makes 11 colors starting with red and ending with black.
|
654
|
+
|
655
|
+
def makePalette(first, last, n)
|
656
|
+
d = Array.new(3)
|
657
|
+
3.times { |i| d[i] = (first[i] - last[i]) / n }
|
658
|
+
a = [first]
|
659
|
+
(n-1).times do |i|
|
660
|
+
a << a.last.clone
|
661
|
+
3.times { |j| a.last[j] -= d[j] }
|
662
|
+
end
|
663
|
+
a << last
|
664
|
+
a.map { |c| sprintf("#%02X%02X%02X",c[0],c[1],c[2]) }
|
665
|
+
end
|
666
|
+
|
667
|
+
def updateCells(pc)
|
668
|
+
id = pc.id
|
669
|
+
a = pc.history[pc.current[:thread]]
|
670
|
+
d = @palette[id].length - a.length
|
671
|
+
a.each_with_index do |x, i|
|
672
|
+
paintCell(x, @palette[id][i+d])
|
673
|
+
end
|
674
|
+
end
|
675
|
+
|
676
|
+
def initialize(parent)
|
677
|
+
content = TkFrame.new(parent)
|
678
|
+
@canvas = TkCanvas.new(content, :borderwidth => 1,
|
679
|
+
:width => @@cellCols*@@cellSize+@@padding, :height => @@cellRows*@@cellSize+@@padding)
|
680
|
+
fillCanvas(Array.new(4096))
|
681
|
+
|
682
|
+
@canvas.grid :column => 0, :row => 0, :columnspan => 4, :padx => 10, :pady => 10
|
683
|
+
content.pack :pady => 20
|
684
|
+
|
685
|
+
# initialize palettes with blends from a dingy color to gray, then
|
686
|
+
# add a bright color as the last item
|
687
|
+
|
688
|
+
@palette = [
|
689
|
+
makePalette( [204,204,204], [204,100,100], @@traceSize ),
|
690
|
+
makePalette( [204,204,204], [100,100,204], @@traceSize ),
|
691
|
+
makePalette( [204,204,204], [100,204,100], @@traceSize ),
|
692
|
+
]
|
693
|
+
@palette[0] << "#FF0000"
|
694
|
+
@palette[1] << "#0000FF"
|
695
|
+
@palette[2] << "#00FF00"
|
696
|
+
end
|
697
|
+
|
698
|
+
end # class Draw
|
699
|
+
|
700
|
+
|
701
|
+
=begin rdoc
|
702
|
+
The MARS module is a package that has the methods used to assemble, load, and execute
|
703
|
+
up to three programs at a time.
|
704
|
+
=end
|
705
|
+
|
706
|
+
class MARS
|
707
|
+
|
708
|
+
# -------- Assembler ----------
|
709
|
+
|
710
|
+
# line format:
|
711
|
+
# <label> <opcode> <A-mode><A-field>, <B-mode><B-field> <comment>
|
712
|
+
|
713
|
+
# Parsing methods strip off and return the item they are looking for, or raise
|
714
|
+
# an error if the line doesn't start with a well-formed item
|
715
|
+
|
716
|
+
def MARS.parseLabel(s) # labels start in column 0, can be any length
|
717
|
+
return nil if s =~ /^\s/
|
718
|
+
x = s[/^\w+/] # set x to the label
|
719
|
+
if x.nil? # x is nil if label didn't start with a word char
|
720
|
+
raise RedcodeSyntaxError, "illegal label in '#{s}'"
|
721
|
+
end
|
722
|
+
if @@opcodes.include?(x.upcase)
|
723
|
+
raise RedcodeSyntaxError, "can't use opcode '#{x}' as a label"
|
724
|
+
end
|
725
|
+
s.slice!(x) # remove it from the line
|
726
|
+
return x # return it
|
727
|
+
end
|
728
|
+
|
729
|
+
def MARS.parseOpCode(s)
|
730
|
+
if s !~ /^\s/ # expect opcode to be separated from label (or start of line) by white space
|
731
|
+
raise RedcodeSyntaxError, "illegal label in '#{s}'"
|
732
|
+
end
|
733
|
+
s.lstrip!
|
734
|
+
x = s[/^\w+/] # set x to the opcode
|
735
|
+
if x.nil? || !@@opcodes.include?(x.upcase)
|
736
|
+
raise RedcodeSyntaxError, "unknown opcode '#{x}'"
|
737
|
+
end
|
738
|
+
s.slice!(x) # remove it from the line
|
739
|
+
return x.upcase # return it
|
740
|
+
end
|
741
|
+
|
742
|
+
def MARS.parseOperand(s)
|
743
|
+
s.lstrip!
|
744
|
+
return s if s.length == 0
|
745
|
+
x = s[/^[#{@@modes}]?[+-]?\w+/]
|
746
|
+
if x.nil?
|
747
|
+
raise RedcodeSyntaxError, "illegal operand in '#{s}'"
|
748
|
+
end
|
749
|
+
s.slice!(x)
|
750
|
+
return x.upcase
|
751
|
+
end
|
752
|
+
|
753
|
+
def MARS.parseSeparator(s)
|
754
|
+
s.lstrip!
|
755
|
+
return if s.length == 0
|
756
|
+
if s[0] == ?,
|
757
|
+
s.slice!(0)
|
758
|
+
else
|
759
|
+
raise RedcodeSyntaxError, "operands must be separated by a comma"
|
760
|
+
end
|
761
|
+
end
|
762
|
+
|
763
|
+
def MARS.parse(s)
|
764
|
+
label = MARS.parseLabel(s)
|
765
|
+
op = MARS.parseOpCode(s)
|
766
|
+
a = MARS.parseOperand(s)
|
767
|
+
MARS.parseSeparator(s)
|
768
|
+
b = MARS.parseOperand(s)
|
769
|
+
return [label,op,a,b]
|
770
|
+
end
|
771
|
+
|
772
|
+
def MARS.saveWord(instr, label, code, symbols)
|
773
|
+
if instr.op == "EQU"
|
774
|
+
operand = instr.a
|
775
|
+
arg = operand[/[+-]?\d+/]
|
776
|
+
operand.slice!(arg)
|
777
|
+
raise RedcodeSyntaxError, "EQU operand must be an integer" unless operand.length == 0
|
778
|
+
raise RedcodeSyntaxError, "EQU must have a label" if label.nil?
|
779
|
+
symbols[label.upcase] = arg.to_i
|
780
|
+
elsif instr.op == "END"
|
781
|
+
if n = symbols[instr.a]
|
782
|
+
symbols[:start] = n
|
783
|
+
else
|
784
|
+
raise RedcodeSyntaxError, "unknown operand in END: #{instr.a}" if instr.a.length > 0
|
785
|
+
end
|
786
|
+
else
|
787
|
+
if label
|
788
|
+
symbols[label.upcase] = code.length # "PC" value is number of codes saved so far
|
789
|
+
end
|
790
|
+
code << instr
|
791
|
+
end
|
792
|
+
end
|
793
|
+
|
794
|
+
def MARS.translate(s, symbols, loc)
|
795
|
+
if s.length == 0
|
796
|
+
s.insert(0, "#0")
|
797
|
+
return true
|
798
|
+
end
|
799
|
+
if md = s.match(/[#{@@modes}]?(\w+)/)
|
800
|
+
sym = md[1]
|
801
|
+
return true if sym =~ /^[+-]?\d+$/
|
802
|
+
return false unless symbols.has_key?(sym)
|
803
|
+
val = symbols[sym] - loc
|
804
|
+
s.sub!(sym, val.to_s)
|
805
|
+
return true
|
806
|
+
end
|
807
|
+
return false
|
808
|
+
end
|
809
|
+
|
810
|
+
=begin rdoc
|
811
|
+
Assembler -- pass in an array of strings, one instruction per string, get back
|
812
|
+
the program name, an array of Word objects, a symbol table, and an array
|
813
|
+
of error messages.
|
814
|
+
=end
|
815
|
+
|
816
|
+
def MARS.assemble(strings)
|
817
|
+
code = Array.new # save Word objects here
|
818
|
+
symbols = Hash.new # symbol table
|
819
|
+
errors = Array.new # put error strings here
|
820
|
+
name = "unknown" # default program name
|
821
|
+
name += @@entries.size.to_s
|
822
|
+
symbols[:start] = 0 # default starting address
|
823
|
+
|
824
|
+
# Pass 1 -- create list of Word objects, build the symbol table:
|
825
|
+
|
826
|
+
strings.each_with_index do |line, lineno|
|
827
|
+
line.rstrip!
|
828
|
+
next if line.length == 0 # skip blank lines
|
829
|
+
if line[0] == ?; # comments have ; in column 0
|
830
|
+
if md = line.match(/;\s*name\s+(\w+)/) # does comment have program name?
|
831
|
+
name = md[1] # yes -- save it
|
832
|
+
end
|
833
|
+
next
|
834
|
+
end
|
835
|
+
if n = line.index(";") # if comment at end remove it
|
836
|
+
line.slice!(n..-1)
|
837
|
+
end
|
838
|
+
begin
|
839
|
+
label, op, a, b = MARS.parse(line)
|
840
|
+
# puts "parse '#{label}' '#{op}' '#{a}' '#{b}'"
|
841
|
+
instr = Word.new(op, a, b, lineno+1)
|
842
|
+
MARS.saveWord(instr, label, code, symbols)
|
843
|
+
rescue RedcodeSyntaxError
|
844
|
+
errors << " line #{lineno+1}: #{$!}"
|
845
|
+
end
|
846
|
+
end
|
847
|
+
|
848
|
+
# Pass 2 -- translate labels into ints on each instruction:
|
849
|
+
# TODO -- deal with expressions, e.g. "JMP label + 1"
|
850
|
+
|
851
|
+
code.each_with_index do |instr, loc|
|
852
|
+
if instr.op == "DAT" && instr.b.length == 0
|
853
|
+
instr.a, instr.b = instr.b, instr.a
|
854
|
+
end
|
855
|
+
begin
|
856
|
+
MARS.translate(instr.a, symbols, loc) or raise RedcodeSyntaxError, "unknown/illegal label"
|
857
|
+
MARS.translate(instr.b, symbols, loc) or raise RedcodeSyntaxError, "unknown/illegal label"
|
858
|
+
rescue RedcodeSyntaxError
|
859
|
+
errors << " line #{instr.lineno}: #{$!}"
|
860
|
+
end
|
861
|
+
end
|
862
|
+
|
863
|
+
return [name, code, symbols, errors]
|
864
|
+
end
|
865
|
+
|
866
|
+
# -------- End Assembler ----------
|
867
|
+
|
868
|
+
|
869
|
+
=begin rdoc
|
870
|
+
step() -- execute one instruction from each active program. If a thread dies remove the PC
|
871
|
+
from the array for that program. A program dies when its last thread dies.
|
872
|
+
=end
|
873
|
+
|
874
|
+
def MARS.step
|
875
|
+
if @@entries.empty?
|
876
|
+
puts "no programs loaded"
|
877
|
+
return 0
|
878
|
+
end
|
879
|
+
|
880
|
+
# A list of program ids that terminate on this cycle
|
881
|
+
done = []
|
882
|
+
|
883
|
+
# This loop gets the current instruction from each active program and executes it.
|
884
|
+
# The return value from the execute method is true if the thread executes a DAT
|
885
|
+
# instruction. The other way to stop the thread is if it has a runtime exception.
|
886
|
+
|
887
|
+
@@pcs.each_with_index do |pc, id|
|
888
|
+
loc = pc.next
|
889
|
+
instr = @@mem.fetch(loc)
|
890
|
+
printf("%04d: %s\n", pc.current[:addr], instr) if @@params[:tracing]
|
891
|
+
|
892
|
+
if @@drawing
|
893
|
+
@@drawing.updateCells(pc)
|
894
|
+
sleep(@@params[:pace])
|
895
|
+
end
|
896
|
+
|
897
|
+
begin
|
898
|
+
signal = instr.execute(pc, @@mem)
|
899
|
+
rescue MARSRuntimeException
|
900
|
+
puts "runtime error: #{$!} in instruction on line #{instr.lineno}"
|
901
|
+
signal = :halt
|
902
|
+
end
|
903
|
+
|
904
|
+
# If this thread halted delete the thread location from the program counter's list.
|
905
|
+
# The return value from kill_thread is the number of threads left -- if 0 the
|
906
|
+
# program is done.
|
907
|
+
|
908
|
+
if signal == :halt
|
909
|
+
if pc.kill_thread == 0
|
910
|
+
done << id # old C++ habit -- don't delete from container while iterating....
|
911
|
+
end
|
912
|
+
end
|
913
|
+
|
914
|
+
end
|
915
|
+
|
916
|
+
# Remove any newly terminated programs from the list of program counters, stop the machine
|
917
|
+
# when no programs are left.
|
918
|
+
|
919
|
+
done.each do |x|
|
920
|
+
@@pcs.slice!(x)
|
921
|
+
end
|
922
|
+
|
923
|
+
return @@pcs.size
|
924
|
+
|
925
|
+
end
|
926
|
+
|
927
|
+
=begin rdoc
|
928
|
+
run(n) -- run the programs, stopping when the number of surviving programs is n. For a
|
929
|
+
contest n will be 1, but for testing it can be 0. Another way to return from this method
|
930
|
+
is when a specified number of rounds of instruction executions have taken place.
|
931
|
+
=end
|
932
|
+
|
933
|
+
def MARS.run(nleft = 0)
|
934
|
+
nrounds = @@params[:maxRounds]
|
935
|
+
loop do
|
936
|
+
nprog = MARS.step
|
937
|
+
nrounds -= 1
|
938
|
+
break if nprog == nleft || nrounds <= 0
|
939
|
+
end
|
940
|
+
end
|
941
|
+
|
942
|
+
=begin rdoc
|
943
|
+
state() -- print info about the machine and any programs in memory
|
944
|
+
=end
|
945
|
+
|
946
|
+
def MARS.state
|
947
|
+
puts "MARS CPU with #{@@mem.size}-word memory"
|
948
|
+
if @@entries.length == 0
|
949
|
+
puts "no programs loaded"
|
950
|
+
else
|
951
|
+
for i in 0...@@entries.length
|
952
|
+
puts "Program: #{@@entries[i].name}"
|
953
|
+
puts " code: #{@@entries[i].code.inspect}"
|
954
|
+
puts " threads: #{@@pcs[i]}"
|
955
|
+
end
|
956
|
+
end
|
957
|
+
puts "Options:"
|
958
|
+
@@params.each do |key, val|
|
959
|
+
puts " #{key}: #{val}"
|
960
|
+
end
|
961
|
+
return true
|
962
|
+
end
|
963
|
+
|
964
|
+
|
965
|
+
=begin rdoc
|
966
|
+
set_option(key, val) -- set the value of one of the run-time options (valid
|
967
|
+
only when no programs are loaded)
|
968
|
+
=end
|
969
|
+
|
970
|
+
def MARS.set_option(key, val)
|
971
|
+
if ! @@entries.empty?
|
972
|
+
puts "Options can be set only when no programs are loaded (call 'reset' to clear programs)"
|
973
|
+
return false
|
974
|
+
end
|
975
|
+
case key
|
976
|
+
when :memSize
|
977
|
+
if val.class != Fixnum || val < 1024 || val > 16536
|
978
|
+
puts ":memSize must be an integer between 1024 and 16536"
|
979
|
+
return nil
|
980
|
+
end
|
981
|
+
when :maxRounds, :buffer
|
982
|
+
if val.class != Fixnum
|
983
|
+
puts ":#{key} must be an integer"
|
984
|
+
return nil
|
985
|
+
end
|
986
|
+
when :pace
|
987
|
+
if val.class != Float
|
988
|
+
puts ":#{key} must be an floating point number (e.g. 0.05)"
|
989
|
+
return nil
|
990
|
+
end
|
991
|
+
when :tracing
|
992
|
+
if ! (val.class == TrueClass || val.class == FalseClass)
|
993
|
+
puts ":tracing must be true or false"
|
994
|
+
return nil
|
995
|
+
end
|
996
|
+
else
|
997
|
+
puts "Unknown option: #{key}"
|
998
|
+
return nil
|
999
|
+
end
|
1000
|
+
@@params[key] = val
|
1001
|
+
end
|
1002
|
+
|
1003
|
+
=begin rdoc
|
1004
|
+
view() -- open a visual view of the machine state
|
1005
|
+
=end
|
1006
|
+
|
1007
|
+
def MARS.view
|
1008
|
+
require 'tk'
|
1009
|
+
|
1010
|
+
if ! defined? @@tkroot
|
1011
|
+
@@tkroot = TkRoot.new { title "MARS" }
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
if @@drawing == nil
|
1015
|
+
@@drawing = Draw.new(@tkroot)
|
1016
|
+
@@threads = []
|
1017
|
+
@@threads << Thread.new() do
|
1018
|
+
Tk.mainloop
|
1019
|
+
end
|
1020
|
+
end
|
1021
|
+
end
|
1022
|
+
|
1023
|
+
def MARS.close_view
|
1024
|
+
@@threads[0].kill
|
1025
|
+
@@tkroot.destroy
|
1026
|
+
@@drawing = nil
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
=begin rdoc
|
1030
|
+
Run a contest. Parameters are names of up to three programs. Make a Warrior
|
1031
|
+
object for each one, load them, and call run.
|
1032
|
+
=end
|
1033
|
+
|
1034
|
+
def MARS.contest(*args)
|
1035
|
+
MARS.reset
|
1036
|
+
if args.length < 1 || args.length > 3
|
1037
|
+
puts "Pass one, two, or three program names"
|
1038
|
+
return nil
|
1039
|
+
end
|
1040
|
+
args.each do |x|
|
1041
|
+
Warrior.load(Warrior.new(x))
|
1042
|
+
end
|
1043
|
+
MARS.run(1) # the 1 means stop when only 1 program left running
|
1044
|
+
end
|
1045
|
+
|
1046
|
+
=begin rdoc
|
1047
|
+
reset() -- stop the current contest; clear all PCs, reset memory to all nils
|
1048
|
+
=end
|
1049
|
+
|
1050
|
+
def MARS.reset
|
1051
|
+
@@pcs = Array.new
|
1052
|
+
@@mem = Memory.new(@@params[:memSize])
|
1053
|
+
@@entries = Array.new
|
1054
|
+
Warrior.reset
|
1055
|
+
@@drawing.reset if @@drawing
|
1056
|
+
end
|
1057
|
+
|
1058
|
+
=begin rdoc
|
1059
|
+
Print the code for one of the Redcode source programs.
|
1060
|
+
=end
|
1061
|
+
|
1062
|
+
def MARS.listing(prog, dest = nil)
|
1063
|
+
filename = prog.to_s
|
1064
|
+
filename += ".txt" unless filename =~ /\.txt$/
|
1065
|
+
filename = File.join(@@dataDirectory, filename)
|
1066
|
+
dest = STDOUT if dest.nil?
|
1067
|
+
if !File.exists?(filename)
|
1068
|
+
puts "File not found: #{filename}"
|
1069
|
+
else
|
1070
|
+
File.open(filename).each do |line|
|
1071
|
+
dest.puts line.chomp
|
1072
|
+
end
|
1073
|
+
end
|
1074
|
+
return nil
|
1075
|
+
end
|
1076
|
+
|
1077
|
+
=begin rdoc
|
1078
|
+
Save a copy of a Redcode source program; if no output file name specified
|
1079
|
+
make a file name from the program name.
|
1080
|
+
=end
|
1081
|
+
|
1082
|
+
def MARS.checkout(prog, filename = nil)
|
1083
|
+
filename = prog.to_s + ".txt" if filename.nil?
|
1084
|
+
dest = File.open(filename, "w")
|
1085
|
+
MARS.listing(prog, dest)
|
1086
|
+
dest.close
|
1087
|
+
puts "Copy of #{prog} saved in #{filename}"
|
1088
|
+
end
|
1089
|
+
|
1090
|
+
=begin rdoc
|
1091
|
+
Print a list of programs
|
1092
|
+
=end
|
1093
|
+
|
1094
|
+
def MARS.dir
|
1095
|
+
puts "Redcode programs in #{@@dataDirectory}:"
|
1096
|
+
Dir.open(@@dataDirectory).each do |file|
|
1097
|
+
next if file[0] == ?.
|
1098
|
+
file.slice!(/\.txt/)
|
1099
|
+
puts " " + file
|
1100
|
+
end
|
1101
|
+
return nil
|
1102
|
+
end
|
1103
|
+
|
1104
|
+
end # class MARS
|
1105
|
+
|
1106
|
+
|
1107
|
+
=begin rdoc
|
1108
|
+
Make a test machine
|
1109
|
+
=end
|
1110
|
+
|
1111
|
+
def make_test_machine(file, size = nil)
|
1112
|
+
begin
|
1113
|
+
return MiniMARS.new(file, size)
|
1114
|
+
rescue Exception => e
|
1115
|
+
puts "Failed to make test machine: #{$!}"
|
1116
|
+
return nil
|
1117
|
+
end
|
1118
|
+
end
|
1119
|
+
|
1120
|
+
# -- Initializations -- These are "global" vars in the outer MARSLab scope that are
|
1121
|
+
# accessible to all the classes and modules defined inside MARSLab
|
1122
|
+
|
1123
|
+
@@dataDirectory = File.join(File.dirname(__FILE__), '..', 'data', 'mars')
|
1124
|
+
|
1125
|
+
@@opcodes = ["DAT", "MOV", "ADD", "SUB", "JMP", "JMZ", "JMN", "DJN", "CMP", "SPL", "END", "SLT", "EQU"]
|
1126
|
+
@@modes = "@<#"
|
1127
|
+
|
1128
|
+
@@maxEntries = 3
|
1129
|
+
@@displayMemSize = 4096
|
1130
|
+
|
1131
|
+
@@params = {
|
1132
|
+
:maxRounds => 1000,
|
1133
|
+
:memSize => @@displayMemSize,
|
1134
|
+
:buffer => 100,
|
1135
|
+
:tracing => false,
|
1136
|
+
:pace => 0.01,
|
1137
|
+
}
|
1138
|
+
|
1139
|
+
@@drawing = nil
|
1140
|
+
|
1141
|
+
@@pcs = Array.new
|
1142
|
+
@@mem = Memory.new(@@params[:memSize])
|
1143
|
+
@@entries = Array.new
|
1144
|
+
|
1145
|
+
end # MARSLab
|
1146
|
+
|
1147
|
+
end # RubyLabs
|