robotwar/src/robotwar/robot.clj

170 lines
7.3 KiB
Clojure

(ns robotwar.robot
(:use [robotwar.constants]
[clojure.pprint :only [pprint]])
(:require [robotwar.brain :as brain]
[robotwar.register :as register]
[robotwar.physics :as physics]))
; TODO: deal with bumping into walls.
(defn init-robot
"takes a robot-idx, a program, and a robot attribute map and returns a robot.
The distance and distance/time units are all in decimeters and
decimeters per second. Yes, you read that right. Don't ask. It fits
best with the original specs of the game."
[idx src-code attributes]
{:idx idx
:pos-x (:pos-x attributes)
:pos-y (:pos-y attributes)
:aim (:aim attributes)
:damage (:damage attributes)
:v-x 0.0
:v-y 0.0
:desired-v-x 0.0
:desired-v-y 0.0
:shot-timer 0.0
:brain (brain/init-brain src-code (register/init-registers idx))})
(defn update-robots
"takes a world and a function, and returns a world
with its robots updated by passing them through the function"
[world f]
(update-in world [:robots] f))
(defn update-robot
"takes a robot index, a world, and a function, and returns a world
with the robot updated by passing it through the function"
[robot-idx world f]
(update-in world [:robots robot-idx] f))
(defn update-shot-timer
"takes a robot and returns one with the :shot-timer updated"
[{shot-timer :shot-timer :as robot}]
(merge robot {:shot-timer
(max (- shot-timer *GAME-SECONDS-PER-TICK*) 0.0)}))
(defn move-robot
"takes a robot and returns it, moved through space.
helper function for tick-robot."
[{:keys [pos-x pos-y v-x v-y desired-v-x desired-v-y] :as robot}]
(let [max-accel-x (Math/copySign MAX-ACCEL desired-v-x)
max-accel-y (Math/copySign MAX-ACCEL desired-v-y)
{new-pos-x :d, new-v-x :v} (physics/d-and-v-given-desired-v
pos-x
v-x
desired-v-x
max-accel-x
*GAME-SECONDS-PER-TICK*)
{new-pos-y :d, new-v-y :v} (physics/d-and-v-given-desired-v
pos-y
v-y
desired-v-y
max-accel-y
*GAME-SECONDS-PER-TICK*)]
(merge robot {:pos-x new-pos-x
:pos-y new-pos-y
:v-x new-v-x
:v-y new-v-y})))
(defn collide-two-robots
"takes a vector of robots, two robot-indexes (an acting robot
and a target robot), and returns a vector of robots with those
two altered if the actor has collided with the target.
Right now they're just behaving like square billiard balls --
all momentum from one is transferred to the other when they collide.
To account for overshoot during the tick, the position of the actor
is set to but up against the target.
Does not currently calculate damage. when it does, it will
need to only assign each robot half the damage, because the other
half will be assigned when the other robot it ticks through its own turn."
[robots actor-idx target-idx]
(let [actor (get robots actor-idx)
target (get robots target-idx)
dist-x (- (:pos-x target) (:pos-x actor))
dist-y (- (:pos-y target) (:pos-y actor))
abs-dist-x (Math/abs dist-x)
abs-dist-y (Math/abs dist-y)
min-dist (* ROBOT-RADIUS 2)
colliding (and (< abs-dist-x min-dist)
(< abs-dist-y min-dist)
(if (> abs-dist-x abs-dist-y) :x :y))]
(if colliding
(let [new-actor (case colliding
:x (assoc
actor
:damage (dec (:damage actor))
:v-x (:v-x target)
:pos-x (- (:pos-x target)
(Math/copySign min-dist dist-x)))
:y (assoc
actor
:damage (dec (:damage actor))
:v-y (:v-y target)
:pos-y (- (:pos-y target)
(Math/copySign min-dist dist-y))))
new-target (case colliding
:x (assoc
target
:damage (dec (:damage target))
:v-x (:v-x actor))
:y (assoc
target
:damage (dec (:damage target))
:v-y (:v-y actor)))]
{colliding (assoc robots actor-idx new-actor, target-idx new-target)})
{nil robots})))
(defn collide-all-robots
"takes a vector of robots and an actor-idx,
and returns a vector of robots with any collisions that have occurred
(may be at most one x-collision and at most one y-collision)."
; TODO: this is remarkably inefficient, and checks the collisions
; twice in a lot of cases. Sort this out when we sort out the whole :x and :y issue.
[robots actor-idx]
(let [target-idxs (filter #(not= actor-idx %) (range (count robots)))
collided-robots-x (or (some (fn [target-idx]
(:x (collide-two-robots
robots
actor-idx
target-idx)))
target-idxs)
robots)
collided-robots-y (or (some (fn [target-idx]
(:y (collide-two-robots
collided-robots-x
actor-idx
target-idx)))
target-idxs)
collided-robots-x)]
collided-robots-y))
(defn tick-robot
"takes a robot and a world and returns the new state of the world
after the robot has taken its turn.
TODO: add support for collision with walls first (right now it just
stops when it gets there, and doesn't get damaged or bounce),
then support for collision with other robots."
[{robot-idx :idx :as robot} world]
(if false
; replace this real damage line when we
; get robot death implemented correctly: (if (<= (:damage robot) 0)
world
(let [ticked-world (brain/tick-brain
robot
world
register/read-register
register/write-register)
shot-timer-updated-world (update-robot
robot-idx
ticked-world
update-shot-timer)
moved-world (update-robot
robot-idx
shot-timer-updated-world
move-robot)
collision-detected-world (update-robots
moved-world
#(collide-all-robots % robot-idx))]
collision-detected-world)))