1
1
(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
19
288
" 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 ))
23
316
24
317
(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."
26
325
[player-index]
27
326
(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