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.
 
 
 
 
 

504 lines
14 KiB

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