fear_tracker/fear_tracker/models.py

330 lines
13 KiB
Python

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"))