Skip to content

Commit 35d4c36

Browse files
committed
Provides basic Clojure bot loop for chasing the ball
1 parent 08b028c commit 35d4c36

File tree

2 files changed

+349
-24
lines changed

2 files changed

+349
-24
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ buildNumber.properties
1919
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
2020
.mvn/wrapper/maven-wrapper.jar
2121

22+
##
23+
## Clojure
24+
##
25+
.nrepl-port
26+
2227
##
2328
## Bot Project
2429
##
+344-24
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,350 @@
11
(ns net.lambeaux.bots.control
2-
(:import (rlbot ControllerState Bot)))
3-
4-
(defn create-controller-state
5-
"Creates a controller state from the raw data."
6-
[steer throttle pitch yaw roll jump? boost? handbrake? use-item?]
7-
(reify ControllerState
8-
(getSteer [this] (float steer))
9-
(getThrottle [this] (float throttle))
10-
(getPitch [this] (float pitch))
11-
(getYaw [this] (float yaw))
12-
(getRoll [this] (float roll))
13-
(holdJump [this] jump?)
14-
(holdBoost [this] boost?)
15-
(holdHandbrake [this] handbrake?)
16-
(holdUseItem [this] use-item?)))
17-
18-
(defn- process-packet
2+
(:require [clojure.pprint :as pretty])
3+
(:import (rlbot ControllerState Bot)
4+
(rlbot.flat GameTickPacket)
5+
(rlbot.manager BotLoopRenderer)
6+
(java.awt Color Point)
7+
(rlbot.vector Vector3)))
8+
9+
;; ----------------------------------------------------------------------
10+
;; ### Bot state
11+
;;
12+
;; This will get reworked to be less stateful but given the nature of
13+
;; the program it's necessary for now.
14+
15+
(def last-game-packet-capture (atom nil))
16+
(def last-game-map-capture (atom nil))
17+
(def bot-model-state (atom nil))
18+
19+
;; ----------------------------------------------------------------------
20+
;; ### Math
21+
;;
22+
;; Building blocks of 3D vector math.
23+
24+
(comment
25+
"Be cautious about plugging into these formulae and expecting correct answers,
26+
especially when programming in C or Java. Math libraries for a programming language
27+
can do unexpected things if you are not careful. There are three places to be
28+
especially cautious:
29+
- The argument for sin(), cos(), tan() is expected in radians.
30+
- The return value of atan() is in radians.
31+
- The argument for most math functions is expected to be a double.
32+
In C, if you supply a float or an int, you won't get a error message,
33+
just a horribly incorrect answer.
34+
- There are several versions of 'arc tan' in most C libraries, each for a different range of output values.")
35+
36+
(defn- sum
37+
"Adds two vectors together."
38+
[v1 v2]
39+
[(+ (first v1) (first v2)) (+ (second v1) (second v2))])
40+
41+
(defn- sub
42+
"Subtracts v2 from v1."
43+
[v1 v2]
44+
[(- (first v1) (first v2)) (- (second v1) (second v2))])
45+
46+
(defn- mag
47+
"Returns the magnitude of a vector."
48+
[v]
49+
(Math/sqrt (+ (Math/pow (first v) 2) (Math/pow (second v) 2))))
50+
51+
(defn- dir
52+
"Returns the direction of a vector in positive degrees between 0 and 360."
53+
[v]
54+
(let [rel-deg (Math/toDegrees (Math/atan2 (second v) (first v)))]
55+
(if (< rel-deg 0) (+ rel-deg 360) rel-deg)))
56+
57+
(defn- norm
58+
"Returns a unit vector pointing in the same direction as the input vector."
59+
[v]
60+
(let [m (mag v)] [(/ (first v) m) (/ (second v) m)]))
61+
62+
(defn- dot
63+
"Returns the dot product of two vectors."
64+
([v1 v2]
65+
(dot (mag v1) (mag v2) (dir v1) (dir v2)))
66+
([mag1 mag2 angle1 angle2]
67+
(dot mag1 mag2 (Math/abs ^Double (- angle1 angle2))))
68+
([mag1 mag2 theta]
69+
(* mag1 mag2 (Math/cos (Math/toRadians theta)))))
70+
71+
(defn- parts
72+
"Returns the component vector of a magnitude and direction."
73+
[mag dir]
74+
[(* mag (Math/cos (Math/toRadians dir))) (* mag (Math/sin (Math/toRadians dir)))])
75+
76+
(defn- matrix-dot
77+
"An implementation of dot product according to the rules of column matrices."
78+
[v1 v2]
79+
(+ (* (first v1) (first v2)) (* (second v1) (second v2))))
80+
81+
(comment
82+
;;
83+
;; Basics
84+
(sum [1.0 1.0] [2.0 3.0])
85+
(sub [5.0 5.0] [3.0 4.0])
86+
(mag [1.0 1.0])
87+
(dir [1.0 1.0])
88+
(parts (mag [1.0 1.0]) (dir [1.0 1.0]))
89+
;;
90+
;; Unit vectors
91+
(norm [1.0 1.0])
92+
(mag (norm [1.0 1.0]))
93+
;;
94+
;; Dot product
95+
(dot [-2.0 -20.0] [5.0 2.0])
96+
(dot (mag [-2.0 -20.0]) (mag [5.0 2.0]) (dir [-2.0 -20.0]) (dir [5.0 2.0]))
97+
(dot (mag [-2.0 -20.0]) (mag [5.0 2.0]) (Math/abs (- (dir [-2.0 -20.0]) (dir [5.0 2.0]))))
98+
(matrix-dot [-2.0 -20.0] [5.0 2.0])
99+
;;
100+
;; Dot product - reverse relationship / verification
101+
(* 20.1 5.38 (Math/cos (Math/toRadians 117.512)))
102+
(Math/toDegrees
103+
(Math/acos
104+
(/ -50.0 (* (mag [-2.0 -20.0])
105+
(mag [5.0 2.0])))))
106+
;;
107+
;; Dot product - fix decimal precision (result should be zero)
108+
(dot [1.0 1.0] [-1.0 1.0])
109+
(dot [-1.0 1.0] [1.0 1.0])
110+
(float (dot [1.0 1.0] [-1.0 1.0]))
111+
(float (dot [-1.0 1.0] [1.0 1.0]))
112+
;;
113+
;; Issues
114+
(dot (mag [1.0 1.0]) (mag [-1.0 1.0]) (- (dir [1.0 1.0]) (dir [-1.0 1.0])))
115+
(Math/toRadians -90.0)
116+
(/ (Math/PI))
117+
(Math/cos (Math/toRadians 0))
118+
(Math/toRadians 45)
119+
;;
120+
;; Other
121+
(Math/toDegrees (Math/atan2 1.0 1.0))
122+
(Math/toDegrees (Math/atan 1.0)))
123+
124+
;; ----------------------------------------------------------------------
125+
;; ### Adapters
126+
;;
127+
;; Transformers for the bot data model and some standard representations.
128+
129+
(def default-input
130+
{:steer 0.0
131+
:throttle 0.0
132+
:pitch 0.0
133+
:yaw 0.0
134+
:roll 0.0
135+
:jump? false
136+
:boost? false
137+
:handbreak? false
138+
:item? false})
139+
140+
(def default-model
141+
;; Won't know index or renderer until bot init but it's still part of the model
142+
{:player-index nil
143+
:renderer nil
144+
:game-maps (list)
145+
:control-maps (list default-input)})
146+
147+
(defn- control-map->controller-state [controls]
148+
(let [{steer :steer
149+
throttle :throttle
150+
pitch :pitch
151+
roll :roll
152+
yaw :yaw
153+
jump? :jump?
154+
boost? :boost?
155+
handbrake? :handbreak?
156+
item? :item?} controls]
157+
(reify ControllerState
158+
(getSteer [this] (float steer))
159+
(getThrottle [this] (float throttle))
160+
(getPitch [this] (float pitch))
161+
(getRoll [this] (float roll))
162+
(getYaw [this] (float yaw))
163+
(holdJump [this] jump?)
164+
(holdBoost [this] boost?)
165+
(holdHandbrake [this] handbrake?)
166+
(holdUseItem [this] item?))))
167+
168+
(defn- game-packet->game-map
169+
"Converts the Java packet object to a Clojure map. The x-axis is negated in a
170+
similar fashion to the example Java bot to align the quadrant system correctly."
171+
[^GameTickPacket packet]
172+
(let [ball-location (-> packet .ball .physics .location)
173+
ball-velocity (-> packet .ball .physics .velocity)
174+
ball-ang-velocity (-> packet .ball .physics .angularVelocity)
175+
ball-rotation (-> packet .ball .physics .rotation)
176+
player-location (-> packet (.players 0) .physics .location)
177+
player-velocity (-> packet (.players 0) .physics .velocity)
178+
player-ang-velocity (-> packet (.players 0) .physics .angularVelocity)
179+
player-rotation (-> packet (.players 0) .physics .rotation)
180+
expand-vector3 (fn [v3] [(* -1.0 (.x v3)) (.y v3) (.z v3)])]
181+
{:ball-location (expand-vector3 ball-location)
182+
:ball-velocity (expand-vector3 ball-velocity)
183+
:ball-angular-velocity (expand-vector3 ball-ang-velocity)
184+
:ball-rotation {:pitch (.pitch ball-rotation)
185+
:roll (.roll ball-rotation)
186+
:yaw (.yaw ball-rotation)}
187+
:player-location (expand-vector3 player-location)
188+
:player-velocity (expand-vector3 player-velocity)
189+
:player-angular-velocity (expand-vector3 player-ang-velocity)
190+
:player-rotation {:pitch (.pitch player-rotation)
191+
:roll (.roll player-rotation)
192+
:yaw (.yaw player-rotation)}}))
193+
194+
(defn- game-map->string
195+
"Creates a data string for printing on the Rocket League UI; helps keep data
196+
elements from aggressively wrapping and unwrapping as they vary in size."
197+
[game-map]
198+
(->> game-map
199+
(map (fn [[k v]] (str k
200+
(System/lineSeparator)
201+
" "
202+
(print-str v)
203+
(System/lineSeparator))))
204+
(reduce str)))
205+
206+
;; ----------------------------------------------------------------------
207+
;; ### Core
208+
;;
209+
;; Primary bot control logic.
210+
211+
(comment
212+
(println (with-out-str (pretty/pprint {:name "steve" :pass "true"})))
213+
(println (game-map->string {:name "steve" :pass "true"}))
214+
@last-game-packet-capture
215+
@last-game-map-capture
216+
@bot-model-state)
217+
218+
(defn- with-color
219+
([color opacity]
220+
(with-color (.getRed color) (.getGreen color) (.getBlue color) (* 255 opacity)))
221+
([r g b a]
222+
(Color. (int r) (int g) (int b) (int a))))
223+
224+
(defn- draw-line!
225+
([v1 v2]
226+
(draw-line! Color/MAGENTA v1 v2))
227+
([color v1 v2]
228+
(let [renderer (:renderer @bot-model-state)]
229+
(when renderer
230+
(.drawLine3d renderer
231+
color
232+
;; re-adjust the x-axis when posting back to the game
233+
(Vector3. (float (* -1.0 (first v1))) (float (second v1)) (float 50))
234+
(Vector3. (float (* -1.0 (first v2))) (float (second v2)) (float 50)))))))
235+
236+
(defn- draw-rect! [color x y w h filled?]
237+
(let [renderer (:renderer @bot-model-state)]
238+
(when renderer
239+
(.drawRectangle2d renderer color (Point. x y) w h filled?))))
240+
241+
(defn- draw-string! [st color x y scale-x scale-y]
242+
(let [renderer (:renderer @bot-model-state)]
243+
(when renderer
244+
(.drawString2d renderer st color (Point. x y) scale-x scale-y))))
245+
246+
(defn- correction-angle
247+
"Returns the number of radians v-in needs to be rotated by
248+
in order to line up with v-goal."
249+
[v-in v-goal]
250+
(let [r-in (Math/atan2 (second v-in) (first v-in))
251+
r-goal (Math/atan2 (second v-goal) (first v-goal))]
252+
(if
253+
(<= (Math/abs (- r-in r-goal)) Math/PI)
254+
(- r-goal r-in)
255+
(let [r-in-pos (if (< r-in 0)
256+
(+ r-in (* Math/PI 2))
257+
r-in)
258+
r-goal-pos (if (< r-goal 0)
259+
(+ r-goal (* Math/PI 2))
260+
r-goal)]
261+
(- r-goal-pos r-in-pos)))))
262+
263+
(defn- nose-vector [game-map]
264+
(let [player-rotation (:player-rotation game-map)
265+
{pitch :pitch roll :roll yaw :yaw} player-rotation]
266+
[(* -1 (Math/cos pitch) (Math/cos yaw))
267+
(* (Math/cos pitch) (Math/sin yaw))
268+
(Math/sin pitch)]))
269+
270+
(defn- drive-to-ball [game-map throttle]
271+
(let [{ball-location :ball-location
272+
player-location :player-location} game-map
273+
car-to-ball (sub ball-location player-location)
274+
car-direction (nose-vector game-map)
275+
correction (correction-angle car-direction car-to-ball)]
276+
{:control-maps (list (conj default-input
277+
[:throttle throttle]
278+
[:steer (if (> correction 0) -1.0 1.0)]))}))
279+
280+
(defn- drive-forward [throttle]
281+
{:control-maps (list (conj default-input
282+
[:throttle throttle]))})
283+
284+
(defn- drive-nowhere []
285+
{:control-maps (list default-input)})
286+
287+
(defn- next-bot-model
19288
"This is the core update function for a Clojure bot that maps
20-
game packets to controller inputs frame-by-frame."
21-
[game-packet]
22-
(create-controller-state 0.0, 1.0, 0.0, 0.0, 0.0, false, false, false, false))
289+
game packets to controller inputs frame-by-frame. It is modeled
290+
as a reduce operation of (bot-state, packet) -> (bot-state) and
291+
lays the foundation for expressing bots as values."
292+
[bot-model game-map]
293+
;; Draw quadrants.
294+
(draw-line! Color/BLUE [0 0] [0 5000])
295+
(draw-line! Color/BLUE [0 0] [5000 0])
296+
(draw-line! Color/RED [0 0] [0 -5000])
297+
(draw-line! Color/RED [0 0] [-5000 0])
298+
;; Draw target and nose.
299+
(draw-line! Color/MAGENTA
300+
(:ball-location game-map)
301+
(:player-location game-map))
302+
(draw-line! Color/CYAN
303+
(:player-location game-map)
304+
(->> game-map
305+
nose-vector
306+
(repeat 150)
307+
(into (list (:player-location game-map)))
308+
(reduce sum)))
309+
;; Draw live game data.
310+
(draw-rect! (with-color Color/BLACK 0.75) 5 25 700 350 true)
311+
(draw-string! (game-map->string game-map) Color/WHITE 10 40 1 1)
312+
;; Standard processing, the value of the last function is returned.
313+
(drive-nowhere)
314+
(drive-forward 0.5)
315+
(drive-to-ball game-map 0.5))
23316

24317
(defn create-bot
25-
"Creates the actual bot impl that will be ticked every frame."
318+
"Entry point for a Clojure bot (see BotServerImpl). Builds the bot and sets initial
319+
state.
320+
321+
Creates the actual bot impl that will be ticked every frame. The (reify) function
322+
allows Clojure to return an implementation of a Java interface. Also handles some
323+
state management for the bot which allows the core tick function logic (next-bot-model)
324+
to remain stateless."
26325
[player-index]
27326
(reify Bot
28-
(getIndex [this] (do (println "Fetching index") player-index))
29-
(processInput [this packet] (process-packet packet))
30-
(retire [this] (println (str "Retiring sample bot " player-index)))))
327+
(getIndex [this] (println (str "Fetching index " player-index)) player-index)
328+
(retire [this] (println (str "Retiring sample bot " player-index)))
329+
(processInput [this packet]
330+
;; Define initial conditions. Done in the game loop in case of hot reload which
331+
;; resets namespace vars.
332+
(when (nil? @bot-model-state)
333+
(swap! bot-model-state
334+
(constantly (merge default-model {:player-index player-index
335+
:renderer (BotLoopRenderer/forBotLoop this)}))))
336+
;; Compute some values we need to reuse.
337+
(let [game-map (game-packet->game-map packet)
338+
bot-model (next-bot-model @bot-model-state game-map)]
339+
;; Record the game packet / map on every hot reload (side-effect).
340+
(if (nil? @last-game-packet-capture) (swap! last-game-packet-capture (constantly packet)))
341+
(if (nil? @last-game-map-capture) (swap! last-game-map-capture (constantly game-map)))
342+
;; Record the new bot model for next time (side-effect).
343+
(swap! bot-model-state
344+
(constantly (merge default-model bot-model {:player-index player-index
345+
:renderer (BotLoopRenderer/forBotLoop this)})))
346+
;; Return the latest controller state.
347+
(-> bot-model
348+
:control-maps
349+
first
350+
control-map->controller-state)))))

0 commit comments

Comments
 (0)