game.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import logging
  2. from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
  3. from fastapi_camelcase import CamelModel
  4. from pydantic import conint, constr
  5. from sqlalchemy.orm import Session
  6. from .. import scoring
  7. from ..schemas import GameConfig, GameModeEnum, Guess, ScoreMethodEnum
  8. from ..db import get_db, queries, models
  9. from ..point_gen import points, ExhaustedSourceError, reverse_geocode
  10. logger = logging.getLogger(__name__)
  11. router = APIRouter()
  12. class LinkedGame(CamelModel):
  13. linked_game: str
  14. class JoinGame(CamelModel):
  15. player_name: constr(min_length=1)
  16. class GuessResult(CamelModel):
  17. distance: int = None
  18. score: int = 0
  19. total_score: int
  20. @router.put("")
  21. async def create_game(config: GameConfig, bg: BackgroundTasks, db: Session = Depends(get_db)):
  22. try:
  23. coords = await points.get_points(config)
  24. except ExhaustedSourceError:
  25. logger.exception(f"Failed to generate enough points for {config}")
  26. raise HTTPException(status_code=501, detail="Sufficient points could not be generated quickly enough")
  27. game_id = queries.create_game(db, config, coords)
  28. bg.add_task(points.restock_source, config)
  29. return { "gameId": game_id }
  30. def get_game(game_id: str, db: Session = Depends(get_db)) -> models.Game:
  31. game = queries.get_game(db, game_id)
  32. if game is None:
  33. raise HTTPException(status_code=404, detail="Game not found")
  34. return game
  35. @router.get("/{game_id}/config", response_model=GameConfig)
  36. def get_game_config(game: models.Game = Depends(get_game)):
  37. return game
  38. @router.get("/{game_id}/coords")
  39. def get_game_coords(game: models.Game = Depends(get_game)):
  40. return {
  41. str(coord.round_number): {
  42. "lat": coord.latitude,
  43. "lng": coord.longitude,
  44. "country": coord.country_code,
  45. }
  46. for coord in game.coordinates
  47. }
  48. @router.post("/{game_id}/join")
  49. def join_game(game_id: str, join: JoinGame, db: Session = Depends(get_db)):
  50. get_game(game_id, db) # confirm this game exists
  51. player_id = queries.join_game(db, game_id, join.player_name)
  52. if player_id is None:
  53. raise HTTPException(status_code=409, detail="Player name in use")
  54. return { "playerId": player_id }
  55. @router.get("/{game_id}/players")
  56. def get_players(game: models.Game = Depends(get_game)):
  57. return { "players": [
  58. {
  59. "name": p.player_name,
  60. "currentRound": queries.get_next_round_number(p),
  61. "totalScore": queries.get_total_score(p),
  62. "guesses": {
  63. str(g.round_number): {
  64. "lat": g.latitude,
  65. "lng": g.longitude,
  66. "country": g.country_code,
  67. "score": g.round_score,
  68. "timeRemaining": g.time_remaining,
  69. } for g in p.guesses
  70. }
  71. } for p in game.players
  72. ] }
  73. def get_player(game_id: str, player_id: str, db: Session = Depends(get_db)) -> models.Player:
  74. player = queries.get_player(db, player_id)
  75. if player is None:
  76. raise HTTPException(status_code=404, detail="Player not found")
  77. if player.game_id != game_id:
  78. raise HTTPException(status_code=404, detail="Player not in game")
  79. return player
  80. @router.get("/{game_id}/players/{player_id}/current")
  81. def get_current_round(db: Session = Depends(get_db), player: models.Player = Depends(get_player)):
  82. coord = queries.get_next_coordinate(db, player)
  83. if coord is None:
  84. return {
  85. "currentRound": None,
  86. "coord": None,
  87. "timer": None,
  88. }
  89. return {
  90. "currentRound": coord.round_number,
  91. "coord": {
  92. "lat": coord.latitude,
  93. "lng": coord.longitude,
  94. "country": coord.country_code,
  95. },
  96. "timer": queries.get_next_round_time(player),
  97. }
  98. @router.put("/{game_id}/linked", status_code=204)
  99. def set_linked_game(game_id: str, linked_game: LinkedGame, db: Session = Depends(get_db)):
  100. queries.link_game(db, game_id, linked_game.linked_game)
  101. @router.get("/{game_id}/linked")
  102. def get_linked_game(game: models.Game = Depends(get_game)):
  103. return { "linkedGame": game.linked_game }
  104. @router.get("/{game_id}/round/{round_number}/first")
  105. def get_first_submitter(game_id: str, round_number: conint(gt=0), db: Session = Depends(get_db)):
  106. return { "first": queries.get_first_submitter(db, game_id, round_number) }
  107. @router.post("/{game_id}/round/{round_number}/guess/{player_id}", response_model=GuessResult)
  108. async def submit_guess(round_number: conint(gt=0),
  109. guess: Guess,
  110. db: Session = Depends(get_db),
  111. game: models.Game = Depends(get_game),
  112. player: models.Player = Depends(get_player)):
  113. target = queries.get_coordinate(db, player.game_id, round_number)
  114. country_code = await reverse_geocode(guess.lat, guess.lng)
  115. match game.score_method:
  116. case ScoreMethodEnum.country_distance:
  117. score_fn = scoring.score if country_code == target.country_code else scoring.score_pow
  118. score, distance = score_fn((target.latitude, target.longitude), (guess.lat, guess.lng))
  119. case ScoreMethodEnum.country_race:
  120. score = scoring.score_country_race(target.country_code, country_code, guess.time_remaining, game.timer)
  121. distance = None
  122. case ScoreMethodEnum.hard:
  123. score, distance = scoring.score_hard((target.latitude, target.longitude), (guess.lat, guess.lng))
  124. case ScoreMethodEnum.nightmare:
  125. score, distance = scoring.score_nightmare((target.latitude, target.longitude), (guess.lat, guess.lng))
  126. case ScoreMethodEnum.ramp:
  127. score, distance = scoring.score((target.latitude, target.longitude), (guess.lat, guess.lng))
  128. score *= 1 + ((round_number - 1) * 0.5)
  129. case ScoreMethodEnum.ramp_hard:
  130. score, distance = scoring.score_hard((target.latitude, target.longitude), (guess.lat, guess.lng))
  131. score *= 1 + ((round_number - 1) * 0.5)
  132. case _:
  133. score, distance = scoring.score((target.latitude, target.longitude), (guess.lat, guess.lng))
  134. if game.round_point_cap is not None:
  135. score = min(score, max(0, game.round_point_cap - sum(g.round_score for p in game.players for g in p.guesses if g.round_number == round_number)))
  136. if game.game_mode == GameModeEnum.gun_game and round_number == game.rounds and queries.get_first_submitter(db, game.game_id, round_number) is None:
  137. # first to submit in the last round of gun game gets 10k bonus points to ensure they are the winner of the whole game
  138. score += 10_000
  139. added = queries.add_guess(db, guess, player, country_code, round_number, score)
  140. if not added:
  141. raise HTTPException(status_code=409, detail="Already submitted guess for this round")
  142. total_score = queries.get_total_score(player)
  143. return GuessResult(distance=distance, score=score, total_score=total_score)
  144. @router.post("/{game_id}/round/{round_number}/timeout/{player_id}", response_model=GuessResult, response_model_include={"total_score"})
  145. def submit_timeout(round_number: conint(gt=0), db: Session = Depends(get_db), player: models.Player = Depends(get_player)):
  146. added = queries.add_timeout(db, player, round_number)
  147. if not added:
  148. raise HTTPException(status_code=409, detail="Already submitted guess for this round")
  149. total_score = queries.get_total_score(player)
  150. return GuessResult(total_score=total_score)