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 14KB

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