import random import string from django.db import models from django.db.models import Sum from django.utils import timezone def generate_code(length): return "".join([random.choice(string.ascii_lowercase) for i in range(length)]) class Game(models.Model): ACCESS_CODE_LENGTH = 6 access_code = models.CharField(db_index=True, unique=True, max_length=ACCESS_CODE_LENGTH) game_turn = models.IntegerField(default=0) GAME_PHASE_LOBBY = 0 GAME_PHASE_GROWTH = 1 GAME_PHASE_SPIRIT = 2 GAME_PHASE_GROWTH_SPIRIT = 3 GAME_PHASE_FAST = 4 GAME_PHASE_BLIGHTED_ISLAND = 5 GAME_PHASE_EVENT_MAIN = 6 GAME_PHASE_EVENT_TOKEN = 7 GAME_PHASE_EVENT_DAHAN = 8 GAME_PHASE_EVENT_COMBINED = 9 GAME_PHASE_FEAR = 50 GAME_PHASE_FEAR_CARDS = [51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65] GAME_PHASE_ENGLAND_BUILD = 100 GAME_PHASE_RAVAGE = 101 GAME_PHASE_BUILD = 102 GAME_PHASE_EXPLORE = 103 GAME_PHASE_SLOW = 104 GAME_PHASE_END = 120 game_phase = models.IntegerField(default=GAME_PHASE_LOBBY) created = models.DateTimeField() ended = models.DateTimeField(null=True, default=None) combined_growth_spirit = models.BooleanField(default=True) england_build = models.BooleanField(default=False) enable_events = models.BooleanField(default=False) combined_event = models.BooleanField(default=False) fear_per_card = models.IntegerField() # from http://stackoverflow.com/a/11821832 def save(self, *args, **kwargs): # object is being created, thus no primary key field yet if not self.pk: # Make sure access_code is unique before using it. access_code = generate_code(Game.ACCESS_CODE_LENGTH) while Game.objects.filter(access_code=access_code).exists(): access_code = generate_code(Game.ACCESS_CODE_LENGTH) self.access_code = access_code self.created = timezone.now() if self.ended is None and self.game_phase == Game.GAME_PHASE_END: self.ended = timezone.now() super(Game, self).save(*args, **kwargs) def next_turn_and_phase(self): if self.game_phase in [Game.GAME_PHASE_LOBBY, Game.GAME_PHASE_SLOW]: turn = self.game_turn+1 if self.combined_growth_spirit: return (turn, Game.GAME_PHASE_GROWTH_SPIRIT) else: return (turn, Game.GAME_PHASE_GROWTH) def next_phase(): if self.game_phase == Game.GAME_PHASE_SPIRIT: return Game.GAME_PHASE_FAST elif self.game_phase == Game.GAME_PHASE_BLIGHTED_ISLAND\ and not self.enable_events\ or self.game_phase in [Game.GAME_PHASE_EVENT_DAHAN, Game.GAME_PHASE_EVENT_COMBINED]: if self.num_available_fear_cards() == 0: return Game.GAME_PHASE_FEAR else: return Game.GAME_PHASE_FEAR_CARDS[0] elif self.game_phase == Game.GAME_PHASE_BLIGHTED_ISLAND\ and self.enable_events: if self.combined_event: return Game.GAME_PHASE_EVENT_COMBINED else: return Game.GAME_PHASE_EVENT_MAIN elif self.game_phase == Game.GAME_PHASE_FEAR or\ self.game_phase in Game.GAME_PHASE_FEAR_CARDS: num_fear = self.fear_cards_in_current_fear_phase() fear_so_far = self.game_phase - Game.GAME_PHASE_FEAR if num_fear > fear_so_far: return self.game_phase + 1 else: if self.england_build: return Game.GAME_PHASE_ENGLAND_BUILD else: return Game.GAME_PHASE_RAVAGE else: return self.game_phase + 1 return (self.game_turn, next_phase()) def get_current_phase(self): return self.phase_set.filter(game_turn=self.game_turn, game_phase=self.game_phase).first() def get_current_total_fear(self): current_phase = self.get_current_phase() if current_phase: return current_phase.starting_fear +\ current_phase.fear_this_phase() else: return 0 def get_fear_to_next_card(self): total_fear = self.get_current_total_fear() leftover_fear = total_fear % self.fear_per_card return self.fear_per_card - leftover_fear def _fear_phase(self, previous): if self.game_phase >= Game.GAME_PHASE_FEAR and not previous: turn = self.game_turn else: turn = self.game_turn - 1 return self.phase_set.filter( game_turn=turn, game_phase__in=[Game.GAME_PHASE_FEAR, Game.GAME_PHASE_FEAR_CARDS[0]]).first() def _fear_cards_as_of_fear_phase(self, previous): fear_phase = self._fear_phase(previous=previous) if fear_phase: return fear_phase.starting_fear // self.fear_per_card else: return 0 def fear_cards_in_current_fear_phase(self): previous_fear_cards = self._fear_cards_as_of_fear_phase(previous=True) current_fear_cards = self._fear_cards_as_of_fear_phase(previous=False) return current_fear_cards - previous_fear_cards def num_available_fear_cards(self): recent_fear_fear_cards = self._fear_cards_as_of_fear_phase( previous=False) current_fear = self.get_current_total_fear() current_fear_cards = current_fear // self.fear_per_card return current_fear_cards - recent_fear_fear_cards def advance_phase(self): current_phase = self.get_current_phase() current_time = timezone.now() if current_phase: current_phase.ended = current_time current_phase.save() starting_fear = current_phase.starting_fear +\ current_phase.fear_this_phase() else: starting_fear = 0 (next_turn, next_phase_id) = self.next_turn_and_phase() self.game_turn = next_turn self.game_phase = next_phase_id self.save() next_phase = self.get_current_phase() if next_phase is None: next_phase = Phase.objects.create( game=self, game_turn=next_turn, game_phase=next_phase_id, starting_fear=starting_fear, started=current_time) else: next_phase.started = current_time next_phase.starting_fear = starting_fear next_phase.ended = None next_phase.save() self.player_set.update(ready=False) return next_phase def revert_phase(self): previous_phase =\ self.phase_set.filter(game_turn=self.game_turn, game_phase__lt=self.game_phase)\ .order_by('game_phase').last() if not previous_phase: previous_phase = self.phase_set\ .filter(game_turn=self.game_turn-1)\ .order_by('game_phase')\ .last() if not previous_phase: return previous_phase.ended = None previous_phase.save() self.game_turn = previous_phase.game_turn self.game_phase = previous_phase.game_phase self.save() self.player_set.update(ready=True) return previous_phase @staticmethod def get_name_for_phase_id(phase): if phase == Game.GAME_PHASE_LOBBY: return "Game Setup" elif phase == Game.GAME_PHASE_GROWTH: return "Growth" elif phase == Game.GAME_PHASE_SPIRIT: return "Spirit" elif phase == Game.GAME_PHASE_GROWTH_SPIRIT: return "Growth/Spirit" elif phase == Game.GAME_PHASE_FAST: return "Fast Actions" elif phase == Game.GAME_PHASE_BLIGHTED_ISLAND: return "Blighted Island" elif phase == Game.GAME_PHASE_EVENT_MAIN: return "Event (main/top)" elif phase == Game.GAME_PHASE_EVENT_TOKEN: return "Event (token/middle)" elif phase == Game.GAME_PHASE_EVENT_DAHAN: return "Event (dahan/bottom)" elif phase == Game.GAME_PHASE_EVENT_COMBINED: return "Event" elif phase == Game.GAME_PHASE_FEAR: return "Fear (no fear cards)" elif phase in Game.GAME_PHASE_FEAR_CARDS: return "Fear Card #" + str(phase-Game.GAME_PHASE_FEAR) elif phase == Game.GAME_PHASE_ENGLAND_BUILD: return "England Extra Build" elif phase == Game.GAME_PHASE_RAVAGE: return "Ravage" elif phase == Game.GAME_PHASE_BUILD: return "Build" elif phase == Game.GAME_PHASE_EXPLORE: return "Explore" elif phase == Game.GAME_PHASE_SLOW: return "Slow Actions" elif phase == Game.GAME_PHASE_END: return "Game Over" def get_current_phase_name(self): res = Game.get_name_for_phase_id(self.game_phase) if self.game_phase in Game.GAME_PHASE_FEAR_CARDS: res += ' (of %d)' % self.fear_cards_in_current_fear_phase() return res class Player(models.Model): game = models.ForeignKey(Game, on_delete=models.CASCADE, db_index=True) name = models.CharField(max_length=80) SPIRIT_NAMES = [ # From https://spiritislandwiki.com/index.php?title=List_of_Spirits # Base game ("Lightning's Swift Strike", "SI", "L"), ("River Surges in Sunlight", "SI", "L"), ("Vital Strength of the Earth", "SI", "L"), ("Shadows Flicker Like Flame", "SI", "L"), ("Thunderspeaker", "SI", "M"), ("A Spread of Rampant Green", "SI", "M"), ("Ocean's Hungry Grasp", "SI", "H"), ("Bringer of Dreams and Nightmares", "SI", "H"), # Branch and Claw ("Keeper of the Forbidden Wilds", "BC", "M"), ("Sharp Fangs Behind the Leaves", "BC", "M"), # Promo Pack 1 ("Serpent Slumbering Beneath the Island", "P1", "H"), ("Heart of the Wildfire", "P1", "H"), # Jagged Earth ("Stone's Unyielding Defiance", "JE", "M"), ("Shifting Memory of Ages", "JE", "M"), ("Grinning Trickster Stirs Up Trouble", "JE", "M"), ("Lure of the Deep Wilderness", "JE", "M"), ("Many Minds Move as One", "JE", "M"), ("Volcano Looming High", "JE", "M"), ("Shroud of Silent Mist", "JE", "H"), ("Vengeance as a Burning Plague", "JE", "H"), ("Starlight Seeks Its Form", "JE", "V"), ("Fractured Days Split the Sky", "JE", "V"), # Promo Pack 2 ("Downpour Drenches the World", "P2", "H"), ("Finder of Paths Unseen", "P2", "V"), # Horizons of Spirit Island ("Devouring Teeth Lurk Underfoot", "HS", "L"), ("Eyes Watch From the Trees", "HS", "L"), ("Fathomless Mud of the Swamp", "HS", "L"), ("Rising Heat of Stone and Sand", "HS", "L"), ("Sun-Bright Whirlwind", "HS", "L"), ] spirit = models.IntegerField() order = models.IntegerField() ready = models.BooleanField(default=False) unique_together = (("game", "name"), ("game", "order")) def get_spirit_name(self): return Player.SPIRIT_NAMES[self.spirit][0] @staticmethod def enumerate_spirit_names(): return [(i, f"[{spirit[1]},{spirit[2]}] {spirit[0]}") for (i, spirit) in enumerate(Player.SPIRIT_NAMES)] class Phase(models.Model): game = models.ForeignKey(Game, on_delete=models.CASCADE, db_index=True) game_turn = models.IntegerField() game_phase = models.IntegerField() started = models.DateTimeField() ended = models.DateTimeField(null=True, default=None) # fear total at the start of this phase starting_fear = models.IntegerField(default=0) unique_together = (("game", "game_turn", "game_phase")) def fear_this_phase(self): return self.fear_set.aggregate( fear=Sum('pure_fear') + Sum('towns_destroyed') + 2*Sum('cities_destroyed'))['fear'] or 0 class Fear(models.Model): phase = models.ForeignKey(Phase, on_delete=models.CASCADE, db_index=True) player = models.ForeignKey(Player, on_delete=models.CASCADE, db_index=True) effect = models.IntegerField(default=0) # "pure" = not from destroying a town or city pure_fear = models.IntegerField(default=0) towns_destroyed = models.IntegerField(default=0) cities_destroyed = models.IntegerField(default=0) unique_together = (("phase", "player", "effect"))