No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

robot.go 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. package botserv
  2. import (
  3. "log"
  4. "math"
  5. "math/rand"
  6. v "bitbucket.org/hackerbots/vector"
  7. )
  8. type Robot struct {
  9. Id string `json:"id"`
  10. Name string `json:"name"`
  11. Message string `json:"-"`
  12. Stats Stats `json:"-"`
  13. TargetSpeed float32 `json:"-"`
  14. Speed float32 `json:"speed"`
  15. Health int `json:"health"`
  16. RepairCounter float32 `json:"repair"`
  17. ScanCounter float32 `json:"scan_bonus"`
  18. ActiveScan bool `json:"-"`
  19. Position v.Point2d `json:"position"`
  20. Heading v.Vector2d `json:"heading"`
  21. DesiredHeading *v.Vector2d `json:"-"`
  22. MoveTo *v.Point2d `json:"-"`
  23. FireAt *v.Point2d `json:"-"`
  24. Scanners []Scanner `json:"scanners"`
  25. LastFired int `json:"-"`
  26. Collision *Collision `json:"collision"`
  27. Hit bool `json:"hit"`
  28. Probe *v.Point2d `json:"probe"`
  29. ProbeResult *Collision `json:"probe_result"`
  30. gameStats *BotStats `json:-`
  31. Delta float32 `json:-`
  32. Idg *IdGenerator
  33. }
  34. type Collision struct {
  35. v.Point2d
  36. Type string `json:"type"`
  37. }
  38. // This is the subset of data we send to players about robots
  39. // that are not theirs.
  40. type OtherRobot struct {
  41. Id string `json:"id"`
  42. Name string `json:"name"`
  43. Position v.Point2d `json:"position"`
  44. Heading v.Vector2d `json:"heading"`
  45. Health int `json:"health"`
  46. }
  47. func (r Robot) GetTruncatedDetails() OtherRobot {
  48. return OtherRobot{
  49. Id: r.Id,
  50. Name: r.Name,
  51. Position: r.Position,
  52. Heading: r.Heading,
  53. Health: r.Health,
  54. }
  55. }
  56. type RobotSorter struct {
  57. Robots []OtherRobot
  58. }
  59. func (s RobotSorter) Len() int {
  60. return len(s.Robots)
  61. }
  62. func (s RobotSorter) Swap(i, j int) {
  63. s.Robots[i], s.Robots[j] = s.Robots[j], s.Robots[i]
  64. }
  65. func (s RobotSorter) Less(i, j int) bool {
  66. return s.Robots[i].Id < s.Robots[j].Id
  67. }
  68. // TODO - how do I not duplicate this code???
  69. type AllRobotSorter struct {
  70. Robots []BotHealth
  71. }
  72. func (s AllRobotSorter) Len() int {
  73. return len(s.Robots)
  74. }
  75. func (s AllRobotSorter) Swap(i, j int) {
  76. s.Robots[i], s.Robots[j] = s.Robots[j], s.Robots[i]
  77. }
  78. func (s AllRobotSorter) Less(i, j int) bool {
  79. return s.Robots[i].RobotId < s.Robots[j].RobotId
  80. }
  81. type Stats struct {
  82. Hp int `json:"hp"`
  83. Speed float32 `json:"speed"`
  84. Acceleration float32 `json:"acceleration"`
  85. WeaponRadius int `json:"weapon_radius"`
  86. ScannerRadius int `json:"scanner_radius"`
  87. TurnSpeed int `json:"turn_speed"`
  88. FireRate int `json:"fire_rate"`
  89. WeaponDamage int `json:"weapon_damage"`
  90. WeaponSpeed float32 `json:"weapon_speed"`
  91. }
  92. // We request stats using an integer between 1 and 100, the
  93. // integer values map to sensible min-max ranges
  94. type StatsRequest struct {
  95. Hp int `json:"hp"`
  96. Speed int `json:"speed"`
  97. Acceleration int `json:"acceleration"`
  98. WeaponRadius int `json:"weapon_radius"`
  99. ScannerRadius int `json:"scanner_radius"`
  100. TurnSpeed int `json:"turn_speed"`
  101. FireRate int `json:"fire_rate"`
  102. WeaponDamage int `json:"weapon_damage"`
  103. WeaponSpeed int `json:"weapon_speed"`
  104. }
  105. func DeriveStats(request StatsRequest) Stats {
  106. s := Stats{}
  107. // Conversion Tables
  108. var hp_min float32 = 20.0
  109. var hp_max float32 = 200.0
  110. s.Hp = int(((float32(request.Hp) / 100.0) * (hp_max - hp_min)) + hp_min)
  111. var speed_min float32 = 40.0
  112. var speed_max float32 = 200.0
  113. s.Speed = ((float32(request.Speed) / 100.0) * (speed_max - speed_min)) + speed_min
  114. var accel_min float32 = 20.0
  115. var accel_max float32 = 200.0
  116. s.Acceleration = ((float32(request.Acceleration) / 100.0) * (accel_max - accel_min)) + accel_min
  117. var wep_rad_min float32 = 5.0
  118. var wep_rad_max float32 = 60.0
  119. s.WeaponRadius = int(((float32(request.WeaponRadius) / 100.0) * (wep_rad_max - wep_rad_min)) + wep_rad_min)
  120. var scan_rad_min float32 = 100.0
  121. var scan_rad_max float32 = 400.0
  122. s.ScannerRadius = int(((float32(request.ScannerRadius) / 100.0) * (scan_rad_max - scan_rad_min)) + scan_rad_min)
  123. var turn_spd_min float32 = 30.0
  124. var turn_spd_max float32 = 300.0
  125. s.TurnSpeed = int(((float32(request.TurnSpeed) / 100.0) * (turn_spd_max - turn_spd_min)) + turn_spd_min)
  126. var fire_rate_min float32 = 10.0
  127. var fire_rate_max float32 = 2000.0
  128. s.FireRate = int(fire_rate_max+300.0) - int(((float32(request.FireRate)/100.0)*(fire_rate_max-fire_rate_min))+fire_rate_min)
  129. var weapon_damage_min float32 = 0.0
  130. var weapon_damage_max float32 = 20.0
  131. s.WeaponDamage = int(((float32(request.WeaponDamage) / 100.0) * (weapon_damage_max - weapon_damage_min)) + weapon_damage_min)
  132. var weapon_speed_min float32 = 80.0
  133. var weapon_speed_max float32 = 600.0
  134. s.WeaponSpeed = float32(((float32(request.WeaponSpeed) / 100.0) * (weapon_speed_max - weapon_speed_min)) + weapon_speed_min)
  135. return s
  136. }
  137. type Instruction struct {
  138. Message *string `json:"message,omitempty"`
  139. MoveTo *v.Point2d `json:"move_to,omitempty"`
  140. Heading *v.Vector2d `json:"heading,omitempty"`
  141. FireAt *v.Point2d `json:"fire_at,omitempty"`
  142. Probe *v.Point2d `json:"probe,omitempty"`
  143. TargetSpeed *float32 `json:"target_speed,omitempty"`
  144. Repair *bool `json:"repair,omitempty"`
  145. Scan *bool `json:"scan,omitempty"`
  146. }
  147. // returns collision, the intersection point, and the robot with whom r has
  148. // collided, if this happened.
  149. func (r *Robot) checkCollisions(g *Game, probe v.Vector2d) (bool, *v.Point2d, *Robot) {
  150. finalCollision := false
  151. collision := false
  152. closest := float32(math.Inf(1))
  153. var intersection *v.Point2d
  154. var finalRobot *Robot
  155. // TODO: this needs moved to the conf?
  156. botSize := float32(5.0)
  157. botPolygon := v.OrientedSquare(r.Position, r.Heading, botSize)
  158. // Check Walls
  159. r_walls := v.AABB2d{A: v.Point2d{X: botSize, Y: botSize}, B: v.Point2d{X: g.width - botSize, Y: g.height - botSize}}
  160. collision, _, wallIntersect := v.RectIntersection(r_walls, r.Position, probe)
  161. if collision && wallIntersect != nil {
  162. finalCollision = collision
  163. if dist := r.Position.Sub(*wallIntersect).Mag(); dist < closest {
  164. intersection = wallIntersect
  165. closest = dist
  166. }
  167. }
  168. // Check Other Bots
  169. for player := range g.players {
  170. for _, bot := range player.Robots {
  171. if bot.Id == r.Id {
  172. continue
  173. }
  174. player_rect := v.OrientedSquare(bot.Position, bot.Heading, botSize)
  175. collision, move_collision, translation := v.PolyPolyIntersection(
  176. botPolygon, probe, player_rect)
  177. if collision || move_collision {
  178. finalCollision = collision
  179. p := r.Position.Add(probe).Add(translation.Scale(1.2))
  180. if dist := r.Position.Sub(p).Mag(); dist < closest {
  181. intersection = &p
  182. closest = dist
  183. finalRobot = bot
  184. }
  185. }
  186. }
  187. }
  188. // Check Obstacles
  189. for _, obj := range g.obstacles {
  190. // collision due to motion:
  191. collision, move_collision, translation := v.PolyPolyIntersection(
  192. botPolygon, probe, obj.Bounds.ToPolygon())
  193. if collision || move_collision {
  194. finalCollision = collision
  195. p := r.Position.Add(probe).Add(translation.Scale(1.1))
  196. if dist := r.Position.Sub(p).Mag(); dist < closest {
  197. intersection = &p
  198. closest = dist
  199. }
  200. }
  201. // collision due to probe
  202. collision, _, wallIntersect := v.RectIntersection(obj.Bounds, r.Position, probe)
  203. if collision && wallIntersect != nil {
  204. finalCollision = collision
  205. if dist := r.Position.Sub(*wallIntersect).Mag(); dist < closest {
  206. intersection = wallIntersect
  207. closest = dist
  208. }
  209. }
  210. }
  211. return finalCollision, intersection, finalRobot
  212. }
  213. func (r *Robot) Tick(g *Game) {
  214. r.Collision = nil
  215. r.Hit = false
  216. r.scan(g)
  217. // Adjust Speed
  218. if r.Speed < r.TargetSpeed {
  219. r.Speed += (r.Stats.Acceleration * r.Delta)
  220. if r.Speed > r.TargetSpeed {
  221. r.Speed = r.TargetSpeed
  222. }
  223. } else if float32(math.Abs(float64(r.Speed-r.TargetSpeed))) > v.Epsilon {
  224. r.Speed -= (r.Stats.Acceleration * r.Delta)
  225. // Cap reverse to 1/2 speed
  226. if r.Speed < (-0.5 * r.TargetSpeed) {
  227. r.Speed = (-0.5 * r.TargetSpeed)
  228. }
  229. } else {
  230. r.Speed = r.TargetSpeed
  231. }
  232. // Adjust Heading
  233. current_heading := r.Heading
  234. if current_heading.Mag() == 0 && r.MoveTo != nil {
  235. // We may have been stopped before this and had no heading
  236. current_heading = r.MoveTo.Sub(r.Position).Normalize()
  237. }
  238. new_heading := current_heading
  239. if r.MoveTo != nil {
  240. // Where do we WANT to be heading?
  241. new_heading = r.MoveTo.Sub(r.Position).Normalize()
  242. }
  243. if r.DesiredHeading != nil {
  244. // Where do we WANT to be heading?
  245. new_heading = r.DesiredHeading.Normalize()
  246. }
  247. if new_heading.Mag() > 0 {
  248. // Is our direction change too much? Hard coding to 5 degrees/s for now
  249. angle := v.Angle(current_heading, new_heading) * v.Rad2deg
  250. dir := 1.0
  251. if angle < 0 {
  252. dir = -1.0
  253. }
  254. // Max turn radius in this case is in degrees per second
  255. if float32(math.Abs(float64(angle))) > (float32(r.Stats.TurnSpeed) * r.Delta) {
  256. // New heading should be a little less, take current heading and
  257. // rotate by the max turn radius per frame.
  258. rot := (float32(r.Stats.TurnSpeed) * r.Delta) * v.Deg2rad
  259. new_heading = current_heading.Rotate(rot * float32(dir))
  260. }
  261. move_vector := new_heading.Scale(r.Speed * r.Delta)
  262. collision, intersection_point, hit_robot := r.checkCollisions(g, move_vector)
  263. if collision {
  264. dmg := int(math.Abs(float64(r.Speed)) / 10.0)
  265. if dmg <= 0 {
  266. // All collisions need to do at least a little damage,
  267. // otherwise robots could get stuck and never die
  268. dmg = 1
  269. }
  270. r.Collision = &Collision{
  271. Point2d: *intersection_point,
  272. Type: "obstacle",
  273. }
  274. if hit_robot != nil {
  275. r.Collision.Type = "robot"
  276. }
  277. if hit_robot != nil {
  278. hit_robot.Health -= dmg
  279. hit_robot.Speed = (hit_robot.Speed * 0.5)
  280. // hit_robot.Heading = r.Heading
  281. if hit_robot.Health <= 0 {
  282. hit_robot.gameStats.Deaths++
  283. r.gameStats.Kills++
  284. }
  285. }
  286. if r.Position != *intersection_point {
  287. r.Position = *intersection_point
  288. }
  289. r.Health -= dmg
  290. r.MoveTo = &r.Position
  291. r.Speed = (r.Speed * -0.5)
  292. // r.Heading = r.Heading.Scale(-1.0)
  293. if r.Health <= 0 {
  294. r.gameStats.Deaths++
  295. r.gameStats.Suicides++
  296. }
  297. } else {
  298. r.Position = r.Position.Add(move_vector)
  299. if new_heading.Mag() > 0 {
  300. r.Heading = new_heading
  301. } else {
  302. log.Printf("Zero Heading %v", new_heading)
  303. }
  304. }
  305. }
  306. // We only self repair when we're stopped
  307. if math.Abs(float64(r.Speed)) < v.Epsilon && r.RepairCounter > 0 {
  308. r.RepairCounter -= r.Delta
  309. if r.RepairCounter < 0 {
  310. r.Health += g.repair_hp
  311. if r.Health > r.Stats.Hp {
  312. r.Health = r.Stats.Hp
  313. }
  314. r.RepairCounter = g.repair_rate
  315. }
  316. }
  317. // We are only allowed to scan when we're stopped
  318. if math.Abs(float64(r.Speed)) < v.Epsilon && r.ActiveScan {
  319. r.ScanCounter += r.Delta * float32(r.Stats.ScannerRadius) * 0.1
  320. } else if r.ScanCounter > 0 {
  321. r.ScanCounter -= r.Delta * float32(r.Stats.ScannerRadius) * 0.05
  322. if r.ScanCounter <= 0 {
  323. r.ScanCounter = 0
  324. }
  325. }
  326. if r.FireAt != nil {
  327. proj := r.fire(g)
  328. if proj != nil {
  329. g.projectiles[proj] = true
  330. }
  331. }
  332. if r.Probe != nil && r.ProbeResult == nil {
  333. probe_vector := r.Probe.Sub(r.Position)
  334. coll, pos, robo := r.checkCollisions(g, probe_vector)
  335. if coll {
  336. r.ProbeResult = &Collision{
  337. Point2d: *pos,
  338. Type: "obstacle",
  339. }
  340. if robo != nil {
  341. r.ProbeResult.Type = "robot"
  342. }
  343. }
  344. }
  345. }
  346. func (r *Robot) scan(g *Game) {
  347. r.Scanners = r.Scanners[:0]
  348. for player := range g.players {
  349. for _, bot := range player.Robots {
  350. if bot.Id == r.Id || bot.Health <= 0 {
  351. continue
  352. }
  353. dist := v.Distance(bot.Position, r.Position)
  354. if dist < float32(r.Stats.ScannerRadius+int(r.ScanCounter)) {
  355. s := Scanner{
  356. Id: bot.Id,
  357. Type: "robot",
  358. }
  359. r.Scanners = append(r.Scanners, s)
  360. }
  361. }
  362. }
  363. for proj := range g.projectiles {
  364. if proj.Owner == r {
  365. continue
  366. }
  367. dist := v.Distance(proj.Position, r.Position)
  368. if dist < float32(r.Stats.ScannerRadius+int(r.ScanCounter)) {
  369. s := Scanner{
  370. Id: proj.Id,
  371. Type: "projectile",
  372. }
  373. r.Scanners = append(r.Scanners, s)
  374. }
  375. }
  376. for splo := range g.splosions {
  377. dist := v.Distance(splo.Position, r.Position)
  378. if dist < float32(r.Stats.ScannerRadius+int(r.ScanCounter)) {
  379. s := Scanner{
  380. Id: splo.Id,
  381. Type: "explosion",
  382. }
  383. r.Scanners = append(r.Scanners, s)
  384. }
  385. }
  386. }
  387. func (r *Robot) fire(g *Game) *Projectile {
  388. // Throttle the fire rate
  389. time_since_fired := (float32(g.turn) * (r.Delta * 1000)) - (float32(r.LastFired) * (r.Delta * 1000))
  390. if time_since_fired < float32(r.Stats.FireRate) {
  391. return nil
  392. }
  393. r.LastFired = g.turn
  394. r.gameStats.Shots++
  395. return &Projectile{
  396. Id: r.Idg.Hash(),
  397. Position: r.Position,
  398. MoveTo: *r.FireAt,
  399. Damage: r.Stats.WeaponDamage,
  400. Radius: r.Stats.WeaponRadius,
  401. Speed: r.Stats.WeaponSpeed,
  402. Owner: r,
  403. }
  404. }
  405. func (r *Robot) reset(g *Game) {
  406. for {
  407. start_pos := v.Point2d{
  408. X: rand.Float32() * float32(g.width),
  409. Y: rand.Float32() * float32(g.height),
  410. }
  411. r.MoveTo = &start_pos
  412. r.Position = start_pos
  413. r.Health = r.Stats.Hp
  414. // Check Obstacles
  415. retry := false
  416. for _, obj := range g.obstacles {
  417. _, inside, _ := v.RectIntersection(obj.Bounds, r.Position, v.Vector2d{0, 0})
  418. if inside {
  419. retry = true
  420. }
  421. }
  422. if !retry {
  423. break
  424. }
  425. }
  426. }