You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
330 lines
13 KiB
Python
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"))
|