diff --git a/src/robotwar/brain.clj b/src/robotwar/brain.clj new file mode 100644 index 0000000..00e9197 --- /dev/null +++ b/src/robotwar/brain.clj @@ -0,0 +1,82 @@ +(ns robotwar.brain + (:use [clojure.string :only [join]] + [clojure.pprint :only [pprint]] + [robotwar.kernel-lexicon])) + +(def op-map (into {} (for [op robotwar.kernel-lexicon/op-commands] + [op (case op + "/" #(int (Math/round (float (/ %1 %2)))) + "#" not= + (-> op read-string eval))]))) + +(defn read-register + "a function to query the robot housing this brain, for information + from the registers. takes a reg-name, a robot and a world, + and returns the running of the register's read function on the world." + [{read :read} world] + (read world)) + +(defn write-register + "a function to create a new world when the brain pushes data to a register. + takes a reg-name, a robot, a world, and data, + and returns the running of the register's write function on the data and the world." + [{write :write} world data] + (write world data)) + +(defn init-brain + "initialize the brain, meaning all the internal state variables that go along + with the robot program when it's running, except for the registers, + which are queried from the world (or the robot -- haven't decided yet)." + [program] + {:acc 0 + :instr-ptr 0 + :call-stack [] + :program program}) + +(defn resolve-arg [{arg-val :val arg-type :type} registers labels world] + "resolves an instruction argument to a numeric value + (either an arithmetic or logical comparison operand, or an instruction pointer)." + (case arg-type + :label (labels arg-val) + :number arg-val + :register (read-register (registers arg-val) world) + nil)) + +(defn step-brain + "takes a `world` and a pathway to a brain in that world, called `brain-path`. + + Only the brain (the internal state of the robot) + will be different when we pass it back, for all of the operations + except 'TO', which may also alter the external state of the robot, or the wider world. + + (returns the current state of the world untouched if the instruction pointer + has gone beyond the end of the program. TODO: maybe have an error for that." + + [robot world] + (let [{:keys [registers brain]} robot + {:keys [program acc instr-ptr call-stack]} brain + {:keys [instrs labels]} program] + ;(println acc instr-ptr call-stack instrs labels program brain robot) + (if (>= instr-ptr (count instrs)) + world + (let [[{command :val} arg] ((:instrs program) instr-ptr) + resolve #(resolve-arg % registers labels world) + assoc-world-brain #(assoc-in world [:robots (:idx robot) :brain] (into brain %))] + (case command + "GOTO" (assoc-world-brain {:instr-ptr (resolve arg)}) + "GOSUB" (assoc-world-brain {:instr-ptr (resolve arg) + :call-stack (conj call-stack (inc instr-ptr))}) + "ENDSUB" (assoc-world-brain {:instr-ptr (peek call-stack) + :call-stack (pop call-stack)}) + ("IF", ",") (assoc-world-brain {:instr-ptr (inc instr-ptr) + :acc (resolve arg)}) + ("+" "-" "*" "/") (assoc-world-brain {:instr-ptr (inc instr-ptr) + :acc ((op-map command) acc (resolve arg))}) + ("=" ">" "<" "#") (if ((op-map command) acc (resolve arg)) + (assoc-world-brain {:instr-ptr (inc instr-ptr)}) + (assoc-world-brain {:instr-ptr (+ instr-ptr 2)})) + "TO" (write-register + (registers (:val arg)) + (assoc-world-brain {:instr-ptr (inc instr-ptr)}) + acc)))))) + diff --git a/src/robotwar/core.clj b/src/robotwar/core.clj index 86ddff2..5a6fc8a 100644 --- a/src/robotwar/core.clj +++ b/src/robotwar/core.clj @@ -1,35 +1,29 @@ (ns robotwar.core (:use [clojure.pprint] - (robotwar foundry robot world game-lexicon))) + (robotwar foundry brain robot world game-lexicon brain-test))) -(def src-code1 " START - 0 TO A - TEST - IF A > 2 GOTO START - GOSUB INCREMENT - GOTO TEST - 100 TO A - INCREMENT - A + 1 TO A - ENDSUB - 200 TO A ") - -(def src-code2 "WAIT GOTO WAIT") -(def src-code3 "500 TO RANDOM RANDOM RANDOM RANDOM") - -(def world (init-world 30 30 (map #(assemble reg-names %) [src-code1 src-code2 src-code3]))) - -(def step (fn [initial-state n] - (nth (iterate tick-robot initial-state) n))) - -; pretty-prints a robot-state with line numbers, +; this is a hacky place for messing with stuff. currently imports +; all the test data from brain-test, and the function below uses +; some of those variables to +; pretty-print a robot-state with line numbers for the program instructions, ; and only the registers you want. Very convenient. +; +; it takes a world-tick number and a robot index number, and prettyprints a robot +; with line numbers for the program instructions, and only the registers specified. +; (also it only prints the values of the registers, not the register-maps with +; their ugly full system-names of the read and write functions.) Very convenient. -(def ppt (fn [program n & reg-keys] - (let [state (step (init-robot-state program {}) n)] - (pprint (into (assoc-in - state - [:program :instrs] - (zipmap (range) (get-in state [:program :instrs]))) - {:registers (select-keys (:registers state) reg-keys)}))))) +(def get-robot (fn [world-tick-idx robot-idx] + ((:robots (get-world world-tick-idx robot-idx)) robot-idx))) +(def ppt (fn [world-tick-idx robot-idx & [reg-keys]] + (let [{:keys [brain registers] :as robot} (get-robot world-tick-idx robot-idx)] + (pprint + (into robot + {:brain (assoc-in + brain + [:program :instrs] + (sort (zipmap (range) (get-in brain [:program :instrs])))) + :registers (sort (into {} (for [[reg-name reg-map] + (select-keys registers reg-keys)] + {reg-name (:val reg-map)})))}))))) diff --git a/src/robotwar/robot.clj b/src/robotwar/robot.clj index 7a426da..465ce61 100644 --- a/src/robotwar/robot.clj +++ b/src/robotwar/robot.clj @@ -1,82 +1,79 @@ (ns robotwar.robot (:use [clojure.string :only [join]] - (robotwar kernel-lexicon game-lexicon))) + (robotwar brain game-lexicon))) -; TODO: remove the game-lexicon dependency above, when it's no longer needed -; (i.e. when we've moved the resolve-register logic out of this module) - -(def op-map (zipmap op-commands - (map (fn [op] - (case op - "/" #(int (Math/round (float (/ %1 %2)))) - "#" not= - (-> op read-string eval))) - op-commands))) - -(defn resolve-register [registers reg] - (case reg - "RANDOM" (rand-int (registers reg)) - "DATA" (registers (reg-names (registers "INDEX"))) - (registers reg))) - -(defn resolve-arg [{arg-val :val arg-type :type} registers labels] - "resolves an instruction argument to a numeric value - (either an arithmetic or logical comparison operand, or an instruction pointer)." - (case arg-type - :label (labels arg-val) - :number arg-val - :register (resolve-register registers arg-val) - nil)) - -(def registers-with-effect-on-world #{"SHOT" "RADAR" "SPEEDX" "SPEEDY"}) - -(defn tick-robot - "takes as input a data structure representing all that the robot's brain - needs to know about the world: - - 1) The robot program, consisting of a vector of two-part instructions - (a command, followed by an argument or nil) as well as a map of labels to - instruction numbers - 2) The instruction pointer (an index number for the instruction vector) - 3) The value of the accumulator, or nil - 4) The call stack (a vector of instruction pointers to lines following - GOSUB calls) - 5) The contents of all the registers - - After executing one instruction, tick-robot returns the updated verion of all of the above, - plus an optional :action field, to notify the world if the SHOT, SPEEDX, SPEEDY or RADAR - registers have been pushed to." - - [{:keys [acc instr-ptr call-stack registers program] :as state}] - (let [[{command :val} {unresolved-arg-val :val :as arg}] ((program :instrs) instr-ptr) - resolve #(resolve-arg % registers (program :labels))] - (case command - "GOTO" (into state {:instr-ptr (resolve arg)}) - "GOSUB" (into state {:instr-ptr (resolve arg) - :call-stack (conj call-stack (inc instr-ptr))}) - "ENDSUB" (into state {:instr-ptr (peek call-stack) - :call-stack (pop call-stack)}) - ("IF", ",") (into state {:instr-ptr (inc instr-ptr) - :acc (resolve arg)}) - ("+" "-" "*" "/") (into state {:instr-ptr (inc instr-ptr) - :acc ((op-map command) acc (resolve arg))}) - ("=" ">" "<" "#") (if ((op-map command) acc (resolve arg)) - (into state {:instr-ptr (inc instr-ptr)}) - (into state {:instr-ptr (+ instr-ptr 2)})) - "TO" (let [return-state (into state {:instr-ptr (inc instr-ptr) - :registers (into registers {unresolved-arg-val acc})})] - (if (registers-with-effect-on-world unresolved-arg-val) - (conj return-state {:action unresolved-arg-val}) - return-state))))) - -(defn init-robot-state - "initialize all the state variables that go along - with the robot program when it's running. - (Optionally, pass in a hash-map of register names and values)." - [program reg-names & [registers]] - {:program program - :acc 0 - :instr-ptr 0 - :registers (into (zipmap reg-names (repeat 0)) - registers) - :call-stack []}) +; TODO: Fill out this module. +; Probably it will consist mostly of +; 0) An init function, to initialize all the fields containing +; the external robot information. I think this should be +; SEPARATE FROM THE REGISTERS, even the ones that are similar. +; 1) Specialty read and write functions for the registers +; 2) Code to deal with the flag when the robot fires a shot (probably this +; will just involve passing the flag up to the world) +; 3) Something else I can't remember. Maybe put some of the init-register +; and default-register and register-handling-in-general code in this +; module, instead of in brain. Something to think about. +; 4) GENERAL NOTES: A) CHANGE STRINGS TO KEYWORDS EARLY ON. +; B) CHANGE SOME OF THESE MODULE LOADINGS FROM +; "USE" TO "REFER", SO THAT THEY HAVE TO USE +; FULLY-QUALIFIED NAMES. THAT MIGHT MAKE THINGS +; A BIT CLEARER. THE NAMES CAN BE SHORTENED QUITE A BIT, +; WHEN LOADED INTO THE MODULES. +; +;(defn make-default-read [register] +; "takes a register and returns the default version of its :read function, +; which ignores the `world` parameter and just returns +; the :val field from the register." +; (fn [_] +; (:val register))) +; +;(defn make-default-write [robot-idx reg-name] +; "takes a robot-idx and a reg-name to locate a register, and +; returns the default version of that register's :write function, +; which takes a world parameter and a data value and returns the +; world with the data value assoc'd into it." +; (fn [world data] +; (assoc-in world [:robots robot-idx :registers reg-name :val] data))) +; +;(def default-data 0) +; +;(defn default-register [robot-idx reg-name] +; (init-register +; reg-name)) +; +; +;(defn init-robot +; [program x y] +; {:pos-x x +; :pos-y y +; :veloc-x 0 +; :veloc-y 0 +; :accel-x 0 +; :accel-y 0 +; :damage 100}) +; +;(defn init-world +; "initialize all the variables for a robot world" +; [width height programs] +; {:width width +; :height height +; :shells [] +; :robots (vec (map-indexed (fn [idx program] +; {:brain (init-brain +; program +; reg-names +; {(init-register "X" +; default-read +; default-write +; (rand-int width)) +; (init-register "Y" +; default-read +; default-write +; (rand-int height))}) +; :icon (str idx)}) +; programs))}) +; +;(defn tick-robot +; [robot world] +; (let [ticked (tick-brain robot world)] +; )) diff --git a/src/robotwar/world.clj b/src/robotwar/world.clj index e6e295e..c61f8af 100644 --- a/src/robotwar/world.clj +++ b/src/robotwar/world.clj @@ -1,40 +1,47 @@ (ns robotwar.world (:use [clojure.string :only [join]] - (robotwar foundry robot))) - -(defn init-world - "initialize all the variables for a robot world" - [width height programs] - {:width width - :height height - :shells [] - :robots (map-indexed (fn [idx program] - {:internal-state (init-robot-state program - {"X" (rand-int width) - "Y" (rand-int height)}) - :external-state {:icon (str idx)}}) - programs)}) - -(defn tick-world - "TODO" - [world-state]) - -(defn arena-text-grid - "outputs the arena, with borders" - [{:keys [width height robots]}] - (let [horiz-border-char "-" - vert-border-char "+" - header-footer (apply str (repeat (+ width 2) horiz-border-char)) - field (for [y (range height), x (range width)] - (some (fn [{{{robot-x "X" robot-y "Y"} :registers} :internal-state - {icon :icon} :external-state}] - (if (= [x y] [robot-x robot-y]) - icon - " ")) - robots))] - (str header-footer - "\n" - (join "\n" (map #(join (apply str %) (repeat 2 vert-border-char)) - (partition width field))) - "\n" - header-footer))) + (robotwar foundry brain robot game-lexicon))) +; +;(defn init-world +; "initialize all the variables for a robot world" +; [width height programs] +; {:width width +; :height height +; :shells [] +; :robots (vec (map-indexed (fn [idx program] +; {:brain (init-brain +; program +; reg-names +; {(init-register "X" +; default-read +; default-write +; (rand-int width)) +; (init-register "Y" +; default-read +; default-write +; (rand-int height))}) +; :icon (str idx)}) +; programs))}) +; +;(defn tick-world +; "TODO" +; [world-state]) +; +;(defn arena-text-grid +; "outputs the arena, with borders" +; [{:keys [width height robots]}] +; (let [horiz-border-char "-" +; vert-border-char "+" +; header-footer (apply str (repeat (+ width 2) horiz-border-char)) +; field (for [y (range height), x (range width)] +; (some (fn [{{{robot-x "X" robot-y "Y"} :registers} :internal-state, icon :icon}] +; (if (= [x y] [robot-x robot-y]) +; icon +; " ")) +; robots))] +; (str header-footer +; "\n" +; (join "\n" (map #(join (apply str %) (repeat 2 vert-border-char)) +; (partition width field))) +; "\n" +; header-footer))) diff --git a/test/robotwar/brain_test.clj b/test/robotwar/brain_test.clj new file mode 100644 index 0000000..2a536fc --- /dev/null +++ b/test/robotwar/brain_test.clj @@ -0,0 +1,106 @@ +(ns robotwar.brain-test + (:use [clojure.test] + [robotwar.brain]) + (:require (robotwar foundry game-lexicon))) + +(def src-codes [ ; program 0: multi-use program + " START + 0 TO A + TEST + IF A > 2 GOTO START + GOSUB INCREMENT + GOTO TEST + 100 TO A + INCREMENT + A + 1 TO A + ENDSUB + 200 TO A " + + ; program 1: to test RANDOM register + " 1000 TO RANDOM + RANDOM RANDOM RANDOM RANDOM RANDOM + RANDOM RANDOM RANDOM RANDOM RANDOM " + + ; program 2: to test INDEX/DATA pair of registers + " 300 TO A + 1 TO INDEX + DATA " ]) + +(def len (count src-codes)) +(def idx-range (range len)) + +(def robot-register-maps + (for [idx idx-range] + (into {} (for [reg-name robotwar.game-lexicon/reg-names] + (let [path-to-val [:robots idx :registers reg-name :val]] + {reg-name {:read (fn [world] + (get-in world path-to-val)) + :write (fn [world data] + (assoc-in world path-to-val data)) + :val 0}}))))) + +(def brains (map (comp init-brain (partial robotwar.foundry/assemble robotwar.game-lexicon/reg-names)) + src-codes)) + +(def robots (vec (map (fn [idx brain robot-registers] + {:idx idx + :brain brain + :registers robot-registers}) + idx-range + brains + robot-register-maps))) + +(def initial-world {:robots robots}) + +(def worlds (map first (iterate (fn [[{robots :robots :as world} idx]] + [(step-brain (robots idx) world) (mod (inc idx) len)]) + [initial-world 0]))) + +(def get-world (fn [world-tick-idx robot-idx] + (let [world-idx (+ (* world-tick-idx len) robot-idx)] + (nth worlds world-idx)))) + +(deftest branching-test + (testing "comparison statement should cause jump in instr-ptr" + (is (= (get-in (get-world 4 0) [:robots 0 :brain :instr-ptr]) + 5)))) + +(deftest arithmetic-test + (testing "addition" + (is (= (get-in (get-world 7 0) [:robots 0 :brain :acc]) + 1)))) + +(deftest gosub-test + (testing "gosub should move instr-ptr and add the return-ptr to the call stack" + (is (let [{:keys [instr-ptr call-stack]} (get-in (get-world 5 0) [:robots 0 :brain])] + (= [instr-ptr call-stack] + [9 [6]]))))) + +(deftest endsub-test + (testing "endsub pops instr-ptr off call stack and goes there" + (is (let [{:keys [instr-ptr call-stack]} (get-in (get-world 9 0) [:robots 0 :brain])] + (= [instr-ptr call-stack] + [6 []]))))) + +(deftest push-test + (testing "pushing number to register" + (is (= (get-in (get-world 8 0) [:robots 0 :registers "A" :val]) + 1)))) + +;(deftest random-test +; (testing "push to random register and pull from it to receive a number +; of unequal numbers less than the number that was pushed" +; (is (let [random-pairs (map (fn [n] +; (let [{{random "RANDOM"} :registers, acc :acc} +; (nth random-check-history n)] +; [random acc])) +; (range 3 13))] +; (and (every? #{1000} (map first random-pairs)) +; (every? #(< -1 % 1000) (map second random-pairs)) +; (apply not= (map second random-pairs))))))) +; +;(deftest index-data-pair-test +; (testing "registers whose index numbers are pushed to INDEX can +; be referenced by accessing DATA" +; (is (= (get-in (nth index-data-check-history 5) [:registers "A"]) +; 300)))) diff --git a/test/robotwar/foundry_test.clj b/test/robotwar/foundry_test.clj index bb8e9de..edbfba4 100644 --- a/test/robotwar/foundry_test.clj +++ b/test/robotwar/foundry_test.clj @@ -1,8 +1,8 @@ (ns robotwar.foundry-test - (:require [clojure.test :refer :all] - [robotwar.foundry :refer :all] - [robotwar.game-lexicon :refer :all]) - (:use [clojure.string :only [join]])) + (:use (clojure [string :only [join]] + [test]) + [robotwar.foundry]) + (:require [robotwar.game-lexicon])) (def line1 "IF DAMAGE # D GOTO MOVE ; comment or something") (def line2 "AIM-17 TO AIM ; other comment") @@ -212,47 +212,47 @@ (deftest parse-token-register (testing "parsing register token" - (is (= (parse-token reg-names {:token-str "AIM"}) + (is (= (parse-token robotwar.game-lexicon/reg-names {:token-str "AIM"}) {:val "AIM", :type :register})))) (deftest parse-token-command-word (testing "parsing command token (word)" - (is (= (parse-token reg-names {:token-str "GOTO"}) + (is (= (parse-token robotwar.game-lexicon/reg-names {:token-str "GOTO"}) {:val "GOTO", :type :command})))) (deftest parse-token-command-operator (testing "parsing command token (operator)" - (is (= (parse-token reg-names {:token-str "#"}) + (is (= (parse-token robotwar.game-lexicon/reg-names {:token-str "#"}) {:val "#", :type :command})))) (deftest parse-token-number (testing "parsing number token" - (is (= (parse-token reg-names {:token-str "-17"})) + (is (= (parse-token robotwar.game-lexicon/reg-names {:token-str "-17"})) {:val -17, :type :number}))) (deftest parse-token-label (testing "parsing label token" - (is (= (parse-token reg-names {:token-str "SCAN"}) + (is (= (parse-token robotwar.game-lexicon/reg-names {:token-str "SCAN"}) {:val "SCAN", :type :label})))) (deftest parse-token-error (testing "parsing error token" - (is (= (parse-token reg-names {:token-str "-GOTO"}) + (is (= (parse-token robotwar.game-lexicon/reg-names {:token-str "-GOTO"}) {:val "Invalid word or symbol", :type :error})))) (deftest parse-tokens-minus-sign (testing "parsing tokens with a binary minus sign" - (is (= (parse reg-names lexed-tokens2) + (is (= (parse robotwar.game-lexicon/reg-names lexed-tokens2) parsed-tokens2)))) (deftest parse-tokens-negative-sign (testing "parsing tokens with a unary negative sign" - (is (= (parse reg-names lexed-tokens3) + (is (= (parse robotwar.game-lexicon/reg-names lexed-tokens3) parsed-tokens3)))) (deftest parse-tokens-error (testing "parsing tokens with an invalid operator" - (is (= (parse reg-names lexed-tokens4) + (is (= (parse robotwar.game-lexicon/reg-names lexed-tokens4) parsed-tokens4)))) (def minus-sign-disambiguated-tokens2 parsed-tokens2) @@ -294,17 +294,17 @@ (deftest assemble-test-success (testing "compiling successfully" - (is (= (assemble reg-names (join "\n" [line1 line2 line3])) + (is (= (assemble robotwar.game-lexicon/reg-names (join "\n" [line1 line2 line3])) multi-line-assembled)))) (deftest assemble-test-failure (testing "assemble results in error" - (is (= (assemble reg-names (join "\n" [line1 line2 line3 line4])) + (is (= (assemble robotwar.game-lexicon/reg-names (join "\n" [line1 line2 line3 line4])) multi-line-assembled-error)))) (deftest preserving-line-and-pos-metadata-test (testing "line and pos metadata preserved through assembly process" - (is (= (meta (get-in (assemble reg-names (join "\n" [line1 line2 line3])) + (is (= (meta (get-in (assemble robotwar.game-lexicon/reg-names (join "\n" [line1 line2 line3])) [:instrs 8 1])) {:line 3, :pos 14})))) diff --git a/test/robotwar/robot_test.clj b/test/robotwar/robot_test.clj deleted file mode 100644 index 7ed5f4a..0000000 --- a/test/robotwar/robot_test.clj +++ /dev/null @@ -1,86 +0,0 @@ -(ns robotwar.robot-test - (:use [clojure.test] - (robotwar foundry robot game-lexicon))) - -(def src-codes [ ; program 0: multi-use program - " START - 0 TO A - TEST - IF A > 2 GOTO START - GOSUB INCREMENT - GOTO TEST - 100 TO A - INCREMENT - A + 1 TO A - ENDSUB - 200 TO A " - - ; program 1: to test RANDOM register - " 1000 TO RANDOM - RANDOM - RANDOM - RANDOM - RANDOM - RANDOM - RANDOM - RANDOM - RANDOM - RANDOM - RANDOM " - - ; program 2: to test INDEX/DATA pair of registers - " 300 TO A - 1 TO INDEX - DATA " ]) - -(def robot-history #(iterate tick-robot (init-robot-state (assemble reg-names %) {}))) -(def robot-histories (map robot-history src-codes)) -(def multi-use-history (nth robot-histories 0)) -(def random-check-history (nth robot-histories 1)) -(def index-data-check-history (nth robot-histories 2)) - -(deftest branching-test - (testing "comparison statement should cause jump in instr-ptr" - (is (= (:instr-ptr (nth multi-use-history 4)) - 5)))) - -(deftest arithmetic-test - (testing "addition" - (is (= (:acc (nth multi-use-history 7)) - 1)))) - -(deftest gosub-test - (testing "gosub should move instr-ptr and add the return-ptr to the call stack" - (is (let [{:keys [instr-ptr call-stack]} (nth multi-use-history 5)] - (= [instr-ptr call-stack] - [9 [6]]))))) - -(deftest endsub-test - (testing "endsub pops instr-ptr off call stack and goes there" - (is (let [{:keys [instr-ptr call-stack]} (nth multi-use-history 9)] - (= [instr-ptr call-stack] - [6 []]))))) - -(deftest push-test - (testing "pushing number to register" - (is (= (get-in (nth multi-use-history 8) [:registers "A"]) - 1)))) - -(deftest random-test - (testing "push to random register and pull from it to receive a number - of unequal numbers less than the number that was pushed" - (is (let [random-pairs (map (fn [n] - (let [{{random "RANDOM"} :registers, acc :acc} - (nth random-check-history n)] - [random acc])) - (range 3 13))] - (and (every? #{1000} (map first random-pairs)) - (every? #(< -1 % 1000) (map second random-pairs)) - (apply not= (map second random-pairs))))))) - -(deftest index-data-pair-test - (testing "registers whose index numbers are pushed to INDEX can - be referenced by accessing DATA" - (is (= (get-in (nth index-data-check-history 5) [:registers "A"]) - 300)))) -