class User(AbstractBaseUser): id = ShortUUIDField(prefix="usr", max_length=128, primary_key=True) username = models.CharField(max_length=39, unique=True) email = models.CharField(max_length=512) USERNAME_FIELD = 'username' REQUIRED_FIELDS = [] objects = UserManager() class Meta: app_label = 'authentication' @property def is_admin(self): return self.username.lower() in [ "dlsteuer", "joram", "brandonb927", "coldog", "matthieudolci", "codeallthethingz" ] def assigned_to_team(self): from apps.tournament.models import TeamMember try: team = TeamMember.objects.get(user_id=self.id).team return True except TeamMember.DoesNotExist: pass return False
class Team(models.Model): id = ShortUUIDField(prefix="tem", max_length=128, primary_key=True) name = models.CharField(max_length=128) description = models.TextField() snake = models.ForeignKey(Snake, on_delete=models.CASCADE) @property def snakes(self): users = [tm.user for tm in TeamMember.objects.filter(team=self)] snakes = [us.snake for us in UserSnake.objects.filter(user__in=users)] return snakes @property def tournament_snakes(self): from apps.tournament.models import TournamentSnake return TournamentSnake.objects.filter(snake__in=self.snakes) @property def available_tournaments(self): from apps.tournament.models import Tournament return Tournament.objects.filter( status=Tournament.REGISTRATION).exclude( id__in=[ts.tournament.id for ts in self.tournament_snakes]) class Meta: app_label = "tournament"
class Team(models.Model): id = ShortUUIDField(prefix="tem", max_length=128, primary_key=True) name = models.CharField(max_length=128) description = models.TextField(verbose_name="Back Story") can_register_in_tournaments = models.BooleanField(default=False) team_members = models.ManyToManyField(Profile) @property def snakes(self): return [s for s in Snake.objects.filter(profile__in=self.profiles)] @property def profiles(self): return [profile for profile in self.team_members.all()] @property def tournament_snakes(self): from apps.tournament.models import TournamentSnake return TournamentSnake.objects.filter(snake__in=self.snakes) @property def available_tournaments(self): from apps.tournament.models import Tournament return Tournament.objects.filter( status=Tournament.REGISTRATION).exclude( id__in=[ts.tournament.id for ts in self.tournament_snakes]) def __str__(self): return self.name class Meta: app_label = "tournament"
class Snake(models.Model): id = ShortUUIDField(prefix="snk", max_length=128, primary_key=True) name = models.CharField(max_length=128) url = models.CharField(max_length=128) class Meta: app_label = 'snake'
class Tournament(models.Model): LOCKED = "LO" # Not started, but nobody can register HIDDEN = "HI" # Able to add snakes manually (invite-only) REGISTRATION = "RE" # Publicly viewable and opt-in-able IN_PROGRESS = "PR" COMPLETE = "JR" STATUSES = ( (LOCKED, "Locked"), (HIDDEN, "Hidden"), (REGISTRATION, "Registration"), (IN_PROGRESS, "In Progress"), (COMPLETE, "Complete"), ) id = ShortUUIDField(prefix="trn", max_length=128, primary_key=True) casting_uri = models.CharField(default="", max_length=1024) status = models.CharField(max_length=2, choices=STATUSES, default=LOCKED) name = models.CharField(max_length=256) date = models.DateField() snakes = models.ManyToManyField(Snake, through="TournamentSnake", through_fields=("tournament", "snake")) engine_url = models.CharField(default=settings.ENGINE_URL, max_length=128) @property def brackets(self): return TournamentBracket.objects.filter(tournament=self) @property def is_registration_open(self): return self.status == self.REGISTRATION def __str__(self): return f"Tournament {self.name}"
class GameSnake(BaseModel): id = ShortUUIDField(prefix="gs", max_length=128, primary_key=True) snake = models.ForeignKey(Snake, on_delete=models.CASCADE) game = models.ForeignKey(Game, on_delete=models.CASCADE) death = models.CharField(default="pending", max_length=128) turns = models.IntegerField(default=0) name = models.CharField(default="", max_length=128)
class User(AbstractBaseUser, PermissionsMixin): id = ShortUUIDField(prefix="usr", max_length=128, primary_key=True) username = models.CharField( max_length=39, unique=True) # 39 is max GitHub username length email = models.CharField(max_length=512) is_staff = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) is_commentator = models.BooleanField(default=False) USERNAME_FIELD = "username" REQUIRED_FIELDS = [] objects = UserManager() class Meta: app_label = "authentication" @property def is_admin(self): return self.is_superuser def assigned_to_team(self): from apps.tournament.models import TeamMember return TeamMember.objects.filter(user_id=self.id).exists()
class User(AbstractBaseUser, PermissionsMixin): id = ShortUUIDField(prefix="usr", max_length=128, primary_key=True) username = models.CharField( max_length=39, unique=True ) # 39 is max GitHub username length email = models.CharField(max_length=512) is_staff = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) USERNAME_FIELD = "username" REQUIRED_FIELDS = [] objects = UserManager() class Meta: app_label = "authentication" @property def is_admin(self): return self.username.lower() in [ "brandonb927", "bvanvugt", "codeallthethingz", "coldog", "dlsteuer", "joram", "matthieudolci", "tristan-swu", ] def assigned_to_team(self): from apps.tournament.models import TeamMember return TeamMember.objects.filter(user_id=self.id).exists()
class Team(models.Model): id = ShortUUIDField(prefix="tem", max_length=128, primary_key=True) name = models.CharField(max_length=128) description = models.TextField() snake = models.ForeignKey(Snake, on_delete=models.CASCADE) class Meta: app_label = 'tournament'
class GameSnake(BaseModel): id = ShortUUIDField(prefix='gs', max_length=128, primary_key=True) snake = models.ForeignKey(Snake, on_delete=models.CASCADE) game = models.ForeignKey(Game, on_delete=models.CASCADE) death = models.CharField(default='pending', max_length=128) turns = models.IntegerField(default=0) class Meta: app_label = 'game'
class Snake(BaseModel): id = ShortUUIDField(prefix="snk", max_length=128, primary_key=True) name = models.CharField(max_length=128) url = models.CharField(max_length=128) def __str__(self): return f'{self.name}' class Meta: app_label = 'snake'
class Team(models.Model): id = ShortUUIDField(prefix="tem", max_length=128, primary_key=True) name = models.CharField(max_length=128) description = models.TextField() snake = models.ForeignKey(Snake, on_delete=models.CASCADE) @property def snakes(self): users = [tm.user for tm in TeamMember.objects.filter(team=self)] snakes = [us.snake for us in UserSnake.objects.filter(user__in=users)] return snakes class Meta: app_label = 'tournament'
class HeatGame(models.Model): UNWATCHED = "UW" WATCHING = "W" WATCHED = "WD" STATUSES = ( (UNWATCHED, "Not Casted Yet"), (WATCHING, "Casting"), (WATCHED, "Casted"), ) id = ShortUUIDField(prefix="hga", max_length=128, primary_key=True) status = models.CharField(max_length=2, choices=STATUSES, default=UNWATCHED) number = models.IntegerField(default=1) heat = models.ForeignKey(Heat, on_delete=models.CASCADE) game = models.ForeignKey("core.Game", on_delete=models.DO_NOTHING) objects = HeatGameManager() @property def snakes(self): if self.previous is None: return self.heat.snakes return [ s for s in self.previous.snakes if s.snake != self.previous.winner.snake ] @property def winner(self): if not hasattr(self, "_winner") or self._winner is None: self._winner = self.game.winner() return self._winner @property def human_readable_status(self): for (short, name) in self.STATUSES: if self.status == short: return name @property def previous(self): if self.number == 1: return None return HeatGame.objects.get(number=self.number - 1, heat=self.heat)
class Snake(BaseModel): id = ShortUUIDField(prefix="snk", max_length=128, primary_key=True) name = models.CharField( max_length=128, validators=[MinLengthValidator(3), MaxLengthValidator(50)] ) url = models.CharField(max_length=128) healthy = models.BooleanField( default=False, verbose_name="Did this snake respond to /ping" ) profile = models.ForeignKey(Profile, on_delete=models.CASCADE) is_public = models.BooleanField( default=False, verbose_name="Allow anyone to add this snake to a game" ) objects = SnakeManager() @property def public_name(self): return f"{self.profile.username} / {self.name}" def update_healthy(self): url = str(self.url) if not url.endswith("/"): url = url + "/" ping_url = urljoin(url, "ping") self.healthy = False try: status_code = self.make_ping_request(ping_url) if status_code == 200: self.healthy = True except Exception as e: logger.warning(f'Failed to ping "{self}": {e}') self.save(update_fields=["healthy"]) def __str__(self): return f"{self.public_name}" def make_ping_request(self, ping_url): response = requests.post(ping_url, timeout=1, verify=False) status_code = response.status_code return status_code @property def games(self): return self.game_set.all()
class SnakeLeaderboard(BaseModel): """ Tracks a snakes involvement in the leaderboard. """ def __init__(self, *args, **kwargs): self._rank = False super().__init__(*args, **kwargs) id = ShortUUIDField(prefix="slb", max_length=128, primary_key=True) snake = models.ForeignKey(Snake, null=True, on_delete=models.CASCADE) mu = models.FloatField(null=True) sigma = models.FloatField(null=True) unhealthy_counter = models.IntegerField(null=True) @property def rank(self): return self.mu or 25 @classmethod def ranked(cls): snakes = list(SnakeLeaderboard.objects.all()) return sorted(snakes, key=lambda s: s.rank, reverse=True) def __str__(self): return f"{self.snake.name}" def reset_unhealthy_counter(self): if self.unhealthy_counter is not None and self.unhealthy_counter > 0: self.unhealthy_counter = 0 self.save(update_fields=["unhealthy_counter"]) def is_unhealthy(self): if self.unhealthy_counter is not None and self.unhealthy_counter > 5: return True return False def increase_unhealthy_counter(self): if self.unhealthy_counter is None: self.unhealthy_counter = 0 self.unhealthy_counter += 1 self.save(update_fields=["unhealthy_counter"]) class Meta: app_label = "leaderboard"
class User(AbstractBaseUser): id = ShortUUIDField(prefix="usr", max_length=128, primary_key=True) username = models.CharField(max_length=39, unique=True) email = models.CharField(max_length=512) USERNAME_FIELD = 'username' REQUIRED_FIELDS = [] objects = UserManager() class Meta: app_label = 'authentication' def assigned_to_team(self): from apps.tournament.models import TeamMember try: team = TeamMember.objects.get(user_id=self.id).team return True except TeamMember.DoesNotExist: pass return False
class TournamentBracket(models.Model): id = ShortUUIDField(prefix="tbr", max_length=128, primary_key=True) name = models.CharField(max_length=256) tournament = models.ForeignKey(Tournament, on_delete=models.CASCADE) board_width = models.IntegerField(default=11) board_height = models.IntegerField(default=11) board_food = models.IntegerField(default=2) board_max_turns_to_next_food_spawn = models.IntegerField(null=True, blank=True, default=15) snakes = models.ManyToManyField(Snake, through="TournamentSnake", through_fields=("bracket", "snake")) cached_rounds = None header_row = ["Round", "Heat", "Team Name", "Team ID", "Team Snake URL"] def update_heat_games(self): if self.latest_round is not None: for heat in self.latest_round.heats: for hg in heat.games: if hg.game.engine_id is not None: hg.game.update_from_engine() hg.game.save() def create_next_round(self): if self.latest_round is not None: if self.latest_round.status != "complete": raise RoundNotCompleteException if (self.latest_round.heats.count() == 1 and len(self.latest_round.snakes) == 2): raise TournamentBracketCompleteException num = max([r.number for r in self.rounds] + [0]) + 1 self.cached_rounds = None # clear out cached data return Round.objects.create(number=num, tournament_bracket=self) @property def rounds(self): if self.cached_rounds is None: self.cached_rounds = list(self.round_set.all().prefetch_related( "heat_set__heatgame_set__game", "heat_set__heatgame_set__game__gamesnake_set", "heat_set__snakeheat_set__snake", ).order_by("number")) return self.cached_rounds @property def latest_round(self): if len(self.rounds) == 0: return None return self.rounds[-1] def get_complete_final_round(self): if self.latest_round is None: return None # the final round has exactly one heat if self.latest_round.heats.count() != 1: return None last_heat = self.latest_round.heats.first() if len(last_heat.games) != 1: return None if self.latest_round.status != "complete": return None if len(last_heat.games[-1].snakes) != 2: return None return self.latest_round @property def winners(self): last_round = self.get_complete_final_round() if last_round is None: return False first_place_game = last_round.heats[0].games[0] # It's a tie! if first_place_game.winner is None: return [] # first snake first_place = first_place_game.winner # second snake top_two_snakes = first_place_game.game.game_snakes second_place = [ snake for snake in top_two_snakes if snake != first_place ][0] third_place_round = last_round.previous if third_place_round is None: return [first_place, second_place] # third snake third_place_game = third_place_round.heats[0].games[0] third_place = [ gs for gs in third_place_game.game.game_snakes if gs.snake not in [first_place.snake, second_place.snake] ][0] return [first_place, second_place, third_place] @property def runner_ups(self): winners = self.winners if winners is False: return False round = self.latest_round if round is None: return [] while True: if len(round.snakes) >= 4: break if round.previous is None: break round = round.previous final_six_game = round.heats[0].games[0] final_six = final_six_game.game.game_snakes game_snakes = final_six.exclude( snake_id__in=[w.snake_id for w in winners]) return game_snakes @property def snake_count(self): return self.snakes.count() def game_details(self): games = [] for r in self.rounds: for heat in r.heats: for hg in heat.games: status = hg.game.status if hg.game is not None else None games.append({ "id": hg.game.id, "url": generate_game_url(hg.game), "status": status, "round": r.number, "heat": heat.number, "heat_game": hg.number, }) return games def export(self): rows = [self.header_row] for r in self.rounds: for heat in r.heats: for snake in heat.snakes: row = [ f"Round {r.number}", f"Heat {heat.number}", snake.team.name, snake.team.id, snake.snake.url, ] rows.append(row) return rows def __str__(self): return f"[{self.tournament.name}] {self.name}" class Meta: app_label = "tournament"
class Game(BaseModel): """ Game tracks a game started on the engine locally in the snake database. You can initialize a game through this model and call run() to start the game. Then, you can also call update_from_engine() at any point to refresh the game state from the engine onto this model. Creating a game looks like: game = Game(...) # instance created with config, ready to go game.create() # game snakes created, and any other future pre-game things game.run() # (OPTIONAL) sent to engine, and now it's running! """ class NotCreatedError(Exception): pass class Status: PENDING = "pending" CREATED = "created" RUNNING = "running" ERROR = "error" STOPPED = "stopped" COMPLETE = "complete" id = ShortUUIDField(prefix="gam", max_length=128, primary_key=True) engine_id = models.CharField(null=True, max_length=128) status = models.CharField(default=Status.PENDING, max_length=30) turn = models.IntegerField(default=0) width = models.IntegerField() height = models.IntegerField() max_turns_to_next_food_spawn = models.IntegerField(default=15) snakes = models.ManyToManyField(Snake) engine_url = models.CharField(null=True, max_length=128) objects = GameQuerySet.as_manager() def config(self): """ Fetch the engine configuration. """ config = { "width": self.width, "height": self.height, "maxTurnsToNextFoodSpawn": self.max_turns_to_next_food_spawn, "food": self.snakes.count(), "snakeTimeout": 500, "snakes": [{ "name": gs.name if len(gs.name) > 0 else gs.snake.public_name, "url": gs.snake.url, "id": gs.id, } for gs in self.gamesnake_set.all()], } return config @transaction.atomic def create(self): """ Call the engine to create the game. Returns the game id. """ config = self.config() self.engine_id = engine.create(config, self.engine_url) self.status = Game.Status.CREATED self.save() return self.engine_id def run(self): """ Call the engine to start the game. Returns the game id. """ engine.run(self.engine_id, self.engine_url) return self.engine_id def engine_status(self): return engine.status(self.engine_id, self.engine_url) def update_from_engine(self): """ Update the status and snake statuses from the engine. """ if self.engine_id is None: raise self.NotCreatedError("Game is not created") with transaction.atomic(): status = engine.status(self.engine_id, self.engine_url) self.status = status["status"] self.turn = status["turn"] for game_snake in self.gamesnake_set.all(): snake_status = status["snakes"][game_snake.id] game_snake.death = snake_status["death"] game_snake.save() self.save() return status @property def game_snakes(self): return self.gamesnake_set.all() def alive_game_snakes(self): return self.game_snakes.filter(death="pending") def winner(self): if self.status == self.Status.COMPLETE: living_snakes = self.alive_game_snakes() if living_snakes.count() == 1: return living_snakes.first()
class Round(models.Model): NAME_FINAL_6 = "The Final 6" NAME_FINAL_3 = "The Final 3" NAME_FINAL_2 = "The Final 2" id = ShortUUIDField(prefix="rnd", max_length=128, primary_key=True) number = models.IntegerField(default=1) tournament_bracket = models.ForeignKey(TournamentBracket, on_delete=models.CASCADE) objects = RoundManager() @property def previous(self): try: return Round.objects.get( number=self.number - 1, tournament_bracket=self.tournament_bracket) except Round.DoesNotExist: return None @property def snakes(self): if self.number == 1: return [s for s in self.tournament_bracket.snakes.all()] return [s.snake for s in self.previous.winners] @property def _heat_count(self): if not hasattr(self, "__heat_count"): self.__heat_count = self.heats.count() return self.__heat_count @property def name(self): if self._heat_count > 1: return f"Round {self.number}" num_snakes = self.snake_count if num_snakes == 2: return self.NAME_FINAL_2 if num_snakes == 3: return self.NAME_FINAL_3 return self.NAME_FINAL_6 @property def snake_count(self): return len(self.snakes) @property def winners(self): winners = [] for heat in self.heats: winners += heat.winners return winners @property def heats(self): return (self.heat_set.all().order_by("number").prefetch_related( "heatgame_set", "snakeheat_set")) @property def status(self): for heat in self.heats: if heat.status != "complete": return heat.status return "complete" def __str__(self): return f"[{self.tournament_bracket.tournament.name}:{self.tournament_bracket.name}] {self.name}" class Meta: app_label = "tournament" unique_together = ("number", "tournament_bracket")
class Game(BaseModel): """ Game tracks a game started on the engine locally in the snake database. You can initialize a game through this model and call run() to start the game. Then, you can also call update_from_engine() at any point to refresh the game state from the engine onto this model. Creating a game looks like: game = Game(...) # instance created with config, ready to go game.create() # game snakes created, and any other future pre-game things game.run() # sent to engine, and now it's running! """ class Status: PENDING = 'pending' RUNNING = 'running' ERROR = 'error' STOPPED = 'stopped' COMPLETE = 'complete' id = ShortUUIDField(prefix='gam', max_length=128, primary_key=True) team = models.ForeignKey(Team, on_delete=models.SET_NULL, null=True) engine_id = models.CharField(null=True, max_length=128) status = models.CharField(default=Status.PENDING, max_length=30) turn = models.IntegerField(default=0) width = models.IntegerField() height = models.IntegerField() food = models.IntegerField() def __init__(self, *args, **kwargs): self.snakes = kwargs.get('snakes', []) if 'snakes' in kwargs: del kwargs['snakes'] super().__init__(*args, **kwargs) def config(self): """ Fetch the engine configuration. """ config = { 'width': self.width, 'height': self.height, 'food': self.food, 'snakes': [], } for snake in self.get_snakes(): config['snakes'].append({ 'name': snake.snake.name, 'url': snake.snake.url, 'id': snake.id, }) return config def create(self): with transaction.atomic(): # Note: Creating GameSnake # objects used to happen in the overridden save model function. # Saving the game here ensures there is an ID to use when # creating GameSnake objects. This is a bit of a hack because # of the way Game was implemented initially and then adapted to # support multiple of the same Snake in a Game. self.save() for s in self.snakes: snake = Snake.objects.get(id=s['id']) GameSnake.objects.create(snake=snake, game=self) def run(self): """ Call the engine to start the game. Returns the game id. """ config = self.config() self.engine_id = engine.run(config) self.save() return self.engine_id def update_from_engine(self): """ Update the status and snake statuses from the engine. """ with transaction.atomic(): status = engine.status(self.engine_id) self.status = status['status'] self.turn = status['turn'] for game_snake in self.get_snakes(): snake_status = status['snakes'][game_snake.id] game_snake.death = snake_status['death'] game_snake.save() self.save() return status def get_snakes(self): return GameSnake.objects.filter( game_id=self.id).prefetch_related('snake') def alive_game_snakes(self): return self.get_snakes().filter(death="pending") def winner(self): if self.status == self.Status.COMPLETE: living_snakes = self.alive_game_snakes() if len(living_snakes) == 1: return self.alive_game_snakes()[0] @property def leaderboard_game(self): from apps.leaderboard.models import GameLeaderboard try: return self.gameleaderboard except GameLeaderboard.DoesNotExist: return None class Meta: app_label = 'game'
class Heat(models.Model): id = ShortUUIDField(prefix="hea", max_length=128, primary_key=True) number = models.IntegerField(default=1) round = models.ForeignKey(Round, on_delete=models.CASCADE) desired_games = models.IntegerField(default=2) cached_heatgames = None def __str__(self): return f"[{self.round.tournament_bracket.tournament.name}:{self.round.tournament_bracket.name}:{self.round.name}] Heat {self.number}" @property def snakes(self): return self.snakeheat_set.all().prefetch_related( "snake", "heat", "heat__heatgame_set") @property def ordered_snakes(self): snakes = list(self.snakes) ordered_snakes = [] for s in snakes: if s.first: ordered_snakes.append(s) snakes.remove(s) break for s in snakes: if s.second: ordered_snakes.append(s) snakes.remove(s) break for s in snakes: if s.third: ordered_snakes.append(s) snakes.remove(s) break for s in snakes: ordered_snakes.append(s) return ordered_snakes @property def games(self): if self.cached_heatgames is None: self.cached_heatgames = list( self.heatgame_set.all().order_by("number")) return self.cached_heatgames @property def latest_game(self): if len(self.games) == 0: return None return self.games[0] @property def winners(self): winners = [] for game in self.games: if game.winner is not None: winners.append(game.winner) return winners @property def status(self): if len(self.games) < self.desired_games: return "running" for hg in self.games: if hg.game.status is not Game.Status.COMPLETE: return hg.game.status return "complete" def create_next_game(self): if len(self.games) >= self.desired_games: raise DesiredGamesReachedValidationError() n = len(self.games) + 1 if (self.latest_game is not None and self.latest_game.game.status != Game.Status.COMPLETE): raise Exception("can't create next game") hg = HeatGame.objects.create(heat=self, number=n) return hg class Meta: app_label = "tournament"