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.

480 lines
13 KiB

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