Class: RubyLabs::MARSLab::MARS
- Inherits:
-
Object
- Object
- RubyLabs::MARSLab::MARS
- Defined in:
- lib/marslab.rb
Overview
MARS
The MARS class defines a singleton object that has the methods used to assemble, load, and execute up to three Redcode programs at a time.
Class Method Summary collapse
-
.assemble(strings) ⇒ Object
A simple two-pass assembler for Redcode.
-
.checkout(prog, filename = nil) ⇒ Object
Save a copy of a Redcode source program.
-
.close_view ⇒ Object
Close the RubyLabs Canvas window.
-
.contest(*args) ⇒ Object
Load up to three core warrior programs and start a contest, returning when only one program is still running or when the maximum number of turns have been taken.
-
.dir ⇒ Object
Print a list of programs in the MARSLab data directory.
-
.listing(prog, dest = nil) ⇒ Object
Print a listing of a Redcode source programs.
-
.parse(s) ⇒ Object
Helper method called by the assembler to break an input line into its constituent parts.
-
.parseLabel(s) ⇒ Object
Labels start in column 0, can be any length.
-
.parseOpCode(s) ⇒ Object
Expect opcodes to be separated from labels (or start of line) by white space.
-
.parseOperand(s) ⇒ Object
:nodoc:.
-
.parseSeparator(s) ⇒ Object
:nodoc:.
-
.reset ⇒ Object
Reset the machine state by clearing memory and removing all programs from the list of active warriors.
-
.run(nleft = 0) ⇒ Object
Run the programs in memory, stopping when the number of surviving programs is
nleft
. -
.saveWord(instr, label, code, symbols) ⇒ Object
:nodoc:.
-
.set_option(key, val) ⇒ Object
Set the value of one of the run-time options.
-
.state ⇒ Object
Print information about the machine and any programs in memory.
-
.step ⇒ Object
Execute one instruction from each active program.
-
.translate(s, symbols, loc) ⇒ Object
:nodoc:.
-
.view(userOptions = {}) ⇒ Object
Initialize the RubyLabs canvas with a drawing of the MARS VM main memory.
Class Method Details
.assemble(strings) ⇒ Object
A simple two-pass assembler for Redcode. The input is an array of strings read from a file. The result of the call is an array of 4 items:
name
-
the name of the program (if there was a name pseudo-op in the source code)
code
-
the assembled code, in the form of an array of Word objects
symbols
-
a Hash with labels and their values
errors
-
an array of error messages
882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 |
# File 'lib/marslab.rb', line 882 def MARS.assemble(strings) code = Array.new # save Word objects here symbols = Hash.new # symbol table errors = Array.new # put error strings here name = "unknown" # default program name name += @@entries.size.to_s symbols[:start] = 0 # default starting address # Pass 1 -- create list of Word objects, build the symbol table: strings.each_with_index do |line, lineno| line.rstrip! next if line.length == 0 # skip blank lines if line[0] == ?; # comments have ; in column 0 if md = line.match(/;\s*name\s+(\w+)/) # does comment have program name? name = md[1] # yes -- save it end next end if n = line.index(";") # if comment at end remove it line.slice!(n..-1) end begin label, op, a, b = MARS.parse(line) instr = Word.new(op, a, b, lineno+1) MARS.saveWord(instr, label, code, symbols) rescue RedcodeSyntaxError errors << " line #{lineno+1}: #{$!}" end end # Pass 2 -- translate labels into ints on each instruction: # TODO -- deal with expressions, e.g. "JMP label + 1" code.each_with_index do |instr, loc| if instr.op == "DAT" && instr.b.length == 0 instr.a, instr.b = instr.b, instr.a end begin MARS.translate(instr.a, symbols, loc) or raise RedcodeSyntaxError, "unknown/illegal label" MARS.translate(instr.b, symbols, loc) or raise RedcodeSyntaxError, "unknown/illegal label" rescue RedcodeSyntaxError errors << " line #{instr.lineno}: #{$!}" end end return [name, code, symbols, errors] end |
.checkout(prog, filename = nil) ⇒ Object
Save a copy of a Redcode source program. A program named “x” is saved in a file named “x.txt” in the current directory unless a different file name is passed as a second argument. If the argument is a symbol, it is interpreted as the name of a program in the MARSLab data directory; if it is a string, it should be the name of a file in the current directory.
1183 1184 1185 1186 1187 1188 1189 |
# File 'lib/marslab.rb', line 1183 def MARS.checkout(prog, filename = nil) filename = prog.to_s + ".txt" if filename.nil? dest = File.open(filename, "w") MARS.listing(prog, dest) dest.close puts "Copy of #{prog} saved in #{filename}" end |
.close_view ⇒ Object
Close the RubyLabs Canvas window.
1115 1116 1117 |
# File 'lib/marslab.rb', line 1115 def MARS.close_view Canvas.close end |
.contest(*args) ⇒ Object
Load up to three core warrior programs and start a contest, returning when only one program is still running or when the maximum number of turns have been taken. Arguments can be symbols, in which case they are names of programs in the MARSLab data directory, or strings, in which case they are interpreted as names of Redcode files in the current directory.
Example:
>>!blue MARS.contest(:imp, :dwarf)
=> nil
1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 |
# File 'lib/marslab.rb', line 1129 def MARS.contest(*args) MARS.reset if args.length < 1 || args.length > 3 puts "Pass one, two, or three program names" return nil end args.each do |x| Warrior.load(Warrior.new(x)) end MARS.run(1) # the 1 means stop when only 1 program left running end |
.dir ⇒ Object
Print a list of programs in the MARSLab data directory.
1193 1194 1195 1196 1197 1198 1199 1200 1201 |
# File 'lib/marslab.rb', line 1193 def MARS.dir puts "Redcode programs in #{@@marsDirectory}:" Dir.open(@@marsDirectory).each do |file| next if file[0] == ?. file.slice!(/\.txt/) puts " " + file end return nil end |
.listing(prog, dest = nil) ⇒ Object
Print a listing of a Redcode source programs. If the argument is a symbol, it is interpreted as the name of a program in the MARSLab data directory; if it is a string, it should be the name of a file in the current directory.
1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 |
# File 'lib/marslab.rb', line 1161 def MARS.listing(prog, dest = nil) filename = prog.to_s filename += ".txt" unless filename =~ /\.txt$/ filename = File.join(@@marsDirectory, filename) dest = STDOUT if dest.nil? if !File.exists?(filename) puts "File not found: #{filename}" else File.open(filename).each do |line| dest.puts line.chomp end end return nil end |
.parse(s) ⇒ Object
Helper method called by the assembler to break an input line into its constituent parts. Calls its own helpers named parseLabel
, parseOpCode
, and parseOperand
; see marslab.rb for more information.
828 829 830 831 832 833 834 835 |
# File 'lib/marslab.rb', line 828 def MARS.parse(s) label = MARS.parseLabel(s) op = MARS.parseOpCode(s) a = MARS.parseOperand(s) MARS.parseSeparator(s) b = MARS.parseOperand(s) return [label,op,a,b] end |
.parseLabel(s) ⇒ Object
Labels start in column 0, can be any length
775 776 777 778 779 780 781 782 783 784 785 786 |
# File 'lib/marslab.rb', line 775 def MARS.parseLabel(s) # :nodoc: return nil if s =~ /^\s/ x = s[/^\w+/] # set x to the label if x.nil? # x is nil if label didn't start with a word char raise RedcodeSyntaxError, "illegal label in '#{s}'" end if @@opcodes.include?(x.upcase) raise RedcodeSyntaxError, "can't use opcode '#{x}' as a label" end s.slice!(x) # remove it from the line return x # return it end |
.parseOpCode(s) ⇒ Object
Expect opcodes to be separated from labels (or start of line) by white space
790 791 792 793 794 795 796 797 798 799 800 801 |
# File 'lib/marslab.rb', line 790 def MARS.parseOpCode(s) # :nodoc: if s !~ /^\s/ raise RedcodeSyntaxError, "illegal label in '#{s}'" end s.lstrip! x = s[/^\w+/] # set x to the opcode if x.nil? || !@@opcodes.include?(x.upcase) raise RedcodeSyntaxError, "unknown opcode '#{x}'" end s.slice!(x) # remove it from the line return x.upcase # return it end |
.parseOperand(s) ⇒ Object
:nodoc:
803 804 805 806 807 808 809 810 811 812 |
# File 'lib/marslab.rb', line 803 def MARS.parseOperand(s) # :nodoc: s.lstrip! return s if s.length == 0 x = s[/^[#{@@modes}]?[+-]?\w+/] if x.nil? raise RedcodeSyntaxError, "illegal operand in '#{s}'" end s.slice!(x) return x.upcase end |
.parseSeparator(s) ⇒ Object
:nodoc:
814 815 816 817 818 819 820 821 822 |
# File 'lib/marslab.rb', line 814 def MARS.parseSeparator(s) # :nodoc: s.lstrip! return if s.length == 0 if s[0] == ?, s.slice!(0) else raise RedcodeSyntaxError, "operands must be separated by a comma" end end |
.reset ⇒ Object
Reset the machine state by clearing memory and removing all programs from the list of active warriors.
1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 |
# File 'lib/marslab.rb', line 1144 def MARS.reset @@pcs = Array.new @@mem = Memory.new(@@params[:memSize]) @@entries = Array.new Warrior.reset if @@drawing @@drawing.cells.each do |x| x.fill = @@drawing.[:cellColor] end end return true end |
.run(nleft = 0) ⇒ Object
Run the programs in memory, stopping when the number of surviving programs is nleft
.
To hold a contest that continues until only one program survives call Mars.run(1)
, but when testing a single program the call can be Mars.run(0)
. This method will also return when the maximum number of instructions has been executed (determined by the runtime parameter maxRounds
).
996 997 998 999 1000 1001 1002 1003 |
# File 'lib/marslab.rb', line 996 def MARS.run(nleft = 0) nrounds = @@params[:maxRounds] loop do nprog = MARS.step nrounds -= 1 break if nprog == nleft || nrounds <= 0 end end |
.saveWord(instr, label, code, symbols) ⇒ Object
:nodoc:
837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 |
# File 'lib/marslab.rb', line 837 def MARS.saveWord(instr, label, code, symbols) # :nodoc: if instr.op == "EQU" operand = instr.a arg = operand[/[+-]?\d+/] operand.slice!(arg) raise RedcodeSyntaxError, "EQU operand must be an integer" unless operand.length == 0 raise RedcodeSyntaxError, "EQU must have a label" if label.nil? symbols[label.upcase] = arg.to_i elsif instr.op == "END" if n = symbols[instr.a] symbols[:start] = n else raise RedcodeSyntaxError, "unknown operand in END: #{instr.a}" if instr.a.length > 0 end else if label symbols[label.upcase] = code.length # "PC" value is number of codes saved so far end code << instr end end |
.set_option(key, val) ⇒ Object
Set the value of one of the run-time options. The options, and their default values, are:
- memSize
-
size of the virtual machine memory (4096 words)
- buffer
-
minimum distance between code for two warriors (100 words)
- tracing
-
when true, print detailed trace as machine runs (false)
- pause
-
amount of time to pause between screen updates when programs are run (0.01 sec)
- maxRounds
-
number of turns to give each program before calling a draw (1000)
Example:
>>!blue MARS.set_option(:pause, 0.5)
=> 0.5
Note: Call MARS.state to see the current settings for the options.
1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 |
# File 'lib/marslab.rb', line 1039 def MARS.set_option(key, val) if ! @@entries.empty? puts "Options can be set only when no programs are loaded (call 'reset' to clear programs)" return false end case key when :memSize if val.class != Fixnum || val < 1024 || val > 16536 puts ":memSize must be an integer between 1024 and 16536" return nil end when :maxRounds, :buffer if val.class != Fixnum puts ":#{key} must be an integer" return nil end when :pause if val.class != Float puts ":#{key} must be an floating point number (e.g. 0.05)" return nil end when :tracing if ! (val.class == TrueClass || val.class == FalseClass) puts ":tracing must be true or false" return nil end else puts "Unknown option: #{key}" puts "Call MARS.state to see a list of options and their current settings." return nil end @@params[key] = val end |
.state ⇒ Object
Print information about the machine and any programs in memory.
1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 |
# File 'lib/marslab.rb', line 1007 def MARS.state puts "MARS CPU with #{@@mem.size}-word memory" if @@entries.length == 0 puts "no programs loaded" else for i in 0...@@entries.length puts "Program: #{@@entries[i].name}" puts " code: #{@@entries[i].code.inspect}" puts " threads: #{@@pcs[i]}" end end puts "Options:" @@params.each do |key, val| puts " #{key}: #{val}" end return true end |
.step ⇒ Object
Execute one instruction from each active program. If a thread dies remove the PC from the array for that program. A program dies when its last thread dies. The return value is the number of programs still running.
937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 |
# File 'lib/marslab.rb', line 937 def MARS.step if @@entries.empty? puts "no programs loaded" return 0 end # A list of program ids that terminate on this cycle done = [] # This loop gets the current instruction from each active program and executes it. # The return value from the execute method is true if the thread executes a DAT # instruction. The other way to stop the thread is if it has a runtime exception. @@pcs.each_with_index do |pc, id| loc = pc.next instr = @@mem.fetch(loc) printf("%04d: %s\n", pc.current[:addr], instr) if @@params[:tracing] if @@drawing MARS.updateCells(pc) sleep(@@params[:pause]) end begin signal = instr.execute(pc, @@mem) rescue MARSRuntimeException puts "runtime error: #{$!} in instruction on line #{instr.lineno}" signal = :halt end # If this thread halted delete the thread location from the program counter's list. # The return value from kill_thread is the number of threads left -- if 0 the # program is done. if signal == :halt if pc.kill_thread == 0 done << id # old C++ habit -- don't delete from container while iterating.... end end end # Remove any newly terminated programs from the list of program counters, stop the machine # when no programs are left. done.each do |x| @@pcs.slice!(x) end return @@pcs.size end |
.translate(s, symbols, loc) ⇒ Object
:nodoc:
859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 |
# File 'lib/marslab.rb', line 859 def MARS.translate(s, symbols, loc) # :nodoc: if s.length == 0 s.insert(0, "#0") return true end if md = s.match(/[#{@@modes}]?(\w+)/) sym = md[1] return true if sym =~ /^[+-]?\d+$/ return false unless symbols.has_key?(sym) val = symbols[sym] - loc s.sub!(sym, val.to_s) return true end return false end |
.view(userOptions = {}) ⇒ Object
Initialize the RubyLabs canvas with a drawing of the MARS VM main memory. The display will show one rectangle for each memory cell. When a core warrior is loaded into memory, the rectangles for the cells it occupies will change color, with a different color for each contestant. As a program runs, any memory cells it references will be filled with that program’s color. To keep the screen from having too much color, cells gradually fade back to gray if they are not referenced for a long time.
1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 |
# File 'lib/marslab.rb', line 1081 def MARS.view(userOptions = {} ) = @@viewOptions.merge(userOptions) cellsize = [:cellSize] padding = [:padding] width = [:cellCols] * cellsize + 2*padding height = [:cellRows] * cellsize + 2*padding Canvas.init(width, height, "MARSLab") cells = [] for i in 0...@@displayMemSize x = (i % [:cellCols]) * cellsize + padding y = (i / [:cellCols]) * cellsize + padding cells << Canvas::Rectangle.new( x, y, x+cellsize, y+cellsize, :outline => "#888888", :fill => [:cellColor] ) end palettes = [ Canvas.palette( [204,204,204], [204,100,100], [:traceSize] ), Canvas.palette( [204,204,204], [100,100,204], [:traceSize] ), Canvas.palette( [204,204,204], [100,204,100], [:traceSize] ), ] palettes[0] << "#FF0000" palettes[1] << "#0000FF" palettes[2] << "#00FF00" @@drawing = MARSView.new(cells, palettes, ) return true end |