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

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