335 lines
12 KiB
Python
335 lines
12 KiB
Python
from __future__ import unicode_literals
|
|
|
|
from datetime import datetime, timedelta
|
|
import random
|
|
import string
|
|
|
|
from django.db import models, transaction
|
|
from django.utils import timezone
|
|
|
|
from .helpers import mission_size, mission_size_string
|
|
|
|
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_PHASE_LOBBY = 0
|
|
GAME_PHASE_ROLE = 1
|
|
GAME_PHASE_PICK = 2
|
|
GAME_PHASE_VOTE = 3
|
|
GAME_PHASE_MISSION = 4
|
|
GAME_PHASE_ASSASSIN = 5
|
|
GAME_PHASE_END = 6
|
|
game_phase = models.IntegerField(default=GAME_PHASE_LOBBY)
|
|
times_started = models.IntegerField(null=False, default=0)
|
|
display_history = models.NullBooleanField()
|
|
private_voting = models.NullBooleanField()
|
|
player_assassinated = models.ForeignKey('Player', null=True, default=None,
|
|
related_name='+',
|
|
on_delete=models.PROTECT)
|
|
created = models.DateTimeField()
|
|
ended = models.DateTimeField(null=True, default=None)
|
|
next_game = models.OneToOneField('self', null=True, default=None,
|
|
related_name='previous_game',
|
|
on_delete=models.SET_DEFAULT)
|
|
|
|
# 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)
|
|
|
|
_game_phase_strings = {
|
|
GAME_PHASE_LOBBY: 'lobby',
|
|
GAME_PHASE_ROLE: 'role',
|
|
GAME_PHASE_PICK: 'pick',
|
|
GAME_PHASE_VOTE: 'vote',
|
|
GAME_PHASE_MISSION: 'mission',
|
|
GAME_PHASE_ASSASSIN: 'assassin',
|
|
GAME_PHASE_END: 'end',
|
|
}
|
|
def game_phase_string(self):
|
|
return Game._game_phase_strings[self.game_phase]
|
|
|
|
def num_players(self):
|
|
return self.player_set.count()
|
|
|
|
# The global settings make everything atomic... this is just to annotate
|
|
# that it's important that we never create two next games.
|
|
@transaction.atomic
|
|
def create_or_get_next_game(self):
|
|
if self.next_game is None and self.game_phase == self.GAME_PHASE_END:
|
|
self.next_game = Game.objects.create()
|
|
self.save()
|
|
return self.next_game
|
|
|
|
class Player(models.Model):
|
|
game = models.ForeignKey(Game, on_delete=models.CASCADE, db_index=True)
|
|
SECRET_ID_LENGTH = 8
|
|
secret_id = models.CharField(db_index=True, max_length=SECRET_ID_LENGTH)
|
|
name = models.CharField(max_length=80)
|
|
ROLE_SPY = -1
|
|
ROLE_ASSASSIN = -2
|
|
ROLE_MORGANA = -3
|
|
ROLE_MORDRED = -4
|
|
ROLE_OBERON = -5
|
|
ROLE_GOOD = 1
|
|
ROLE_MERLIN = 2
|
|
ROLE_PERCIVAL = 3
|
|
role = models.IntegerField(null=True, default=None)
|
|
order = models.IntegerField(null=True, default=None)
|
|
ready = models.BooleanField(default=False)
|
|
joined = models.DateTimeField()
|
|
last_accessed = models.DateTimeField()
|
|
# names are unique in a game
|
|
unique_together = (("game", "name"), ("game", "secret_id"))
|
|
|
|
def is_expired(self):
|
|
return timezone.now() - self.last_accessed > timedelta(seconds=10)
|
|
|
|
def change_secret_id(self):
|
|
# Make sure secret_id is unique before using it.
|
|
secret_id = generate_code(Player.SECRET_ID_LENGTH)
|
|
while Player.objects.filter(game=self.game,
|
|
secret_id=secret_id).exists():
|
|
secret_id = generate_code(Player.SECRET_ID_LENGTH)
|
|
self.secret_id = secret_id
|
|
|
|
# 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:
|
|
self.change_secret_id()
|
|
self.joined = timezone.now()
|
|
self.last_accessed = timezone.now()
|
|
super(Player, self).save(*args, **kwargs)
|
|
|
|
def is_spy(self):
|
|
if self.role is None:
|
|
return None
|
|
elif self.role < 0:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def team(self):
|
|
if self.is_spy() is None:
|
|
return None
|
|
elif self.is_spy():
|
|
return 'spy'
|
|
else:
|
|
return 'resistance'
|
|
|
|
def role_string(self):
|
|
if self.role is None:
|
|
return None
|
|
elif self.role == Player.ROLE_SPY:
|
|
return "Minion of Mordred"
|
|
elif self.role == Player.ROLE_ASSASSIN:
|
|
return "Assassin"
|
|
elif self.role == Player.ROLE_MORGANA:
|
|
return "Morgana"
|
|
elif self.role == Player.ROLE_MORDRED:
|
|
return "Mordred"
|
|
elif self.role == Player.ROLE_OBERON:
|
|
return "Oberon"
|
|
elif self.role == Player.ROLE_GOOD:
|
|
return "Loyal servant of Arthur"
|
|
elif self.role == Player.ROLE_MERLIN:
|
|
return "Merlin"
|
|
elif self.role == Player.ROLE_PERCIVAL:
|
|
return "Percival"
|
|
|
|
def is_oberon(self):
|
|
return self.role == Player.ROLE_OBERON
|
|
|
|
def is_merlin(self):
|
|
return self.role == Player.ROLE_MERLIN
|
|
|
|
def is_percival(self):
|
|
return self.role == Player.ROLE_PERCIVAL
|
|
|
|
def is_assassin(self):
|
|
return self.role == Player.ROLE_ASSASSIN
|
|
|
|
def is_morgana(self):
|
|
return self.role == Player.ROLE_MORGANA
|
|
|
|
def is_mordred(self):
|
|
return self.role == Player.ROLE_MORDRED
|
|
|
|
def sees_as_spy(self, other):
|
|
if other.is_spy():
|
|
if self.is_merlin() and not other.is_mordred():
|
|
return True
|
|
elif self.is_spy() and\
|
|
not self.is_oberon() and not other.is_oberon():
|
|
return True
|
|
return False
|
|
|
|
def appears_as_merlin(self):
|
|
return self.is_merlin() or self.is_morgana()
|
|
|
|
class GameRoundManager(models.Manager):
|
|
def get_current_game_round(self, game):
|
|
return self.filter(game=game).order_by('-round_num').first()
|
|
|
|
class GameRound(models.Model):
|
|
game = models.ForeignKey(Game, on_delete=models.CASCADE, db_index=True)
|
|
round_num = models.IntegerField()
|
|
unique_together = (("game", "round_num"),)
|
|
mission_passed = models.NullBooleanField()
|
|
|
|
objects = GameRoundManager()
|
|
|
|
def winner_string(self):
|
|
if self.mission_passed is None:
|
|
return ''
|
|
elif self.mission_passed:
|
|
return 'resistance'
|
|
else:
|
|
return 'spy'
|
|
|
|
def result_string(self):
|
|
if self.mission_passed is None:
|
|
return ''
|
|
elif self.mission_passed:
|
|
return 'pass'
|
|
else:
|
|
return 'fail'
|
|
|
|
def mission_size_tuple(self):
|
|
num_players = Player.objects.filter(game=self.game).count()
|
|
return mission_size(num_players=num_players,
|
|
round_num=self.round_num)
|
|
|
|
def mission_size_string(self):
|
|
return mission_size_string(self.mission_size_tuple())
|
|
|
|
def num_players_on_mission(self):
|
|
return self.mission_size_tuple()[0]
|
|
|
|
def num_fails_required(self):
|
|
return self.mission_size_tuple()[1]
|
|
|
|
def num_fails(self):
|
|
if self.mission_passed is None:
|
|
return None
|
|
else:
|
|
return self.missionaction_set.filter(played_success=False).count()
|
|
|
|
def played_fail(self):
|
|
if self.mission_passed is None:
|
|
return None
|
|
else:
|
|
fail_actions = self.missionaction_set.filter(played_success=False)
|
|
return [action.player for action in fail_actions]
|
|
|
|
class MissionAction(models.Model):
|
|
game_round = models.ForeignKey(GameRound, on_delete=models.CASCADE, db_index=True)
|
|
player = models.ForeignKey(Player, on_delete=models.CASCADE)
|
|
unique_together = (("game", "player"),)
|
|
played_success = models.BooleanField()
|
|
|
|
class VoteRoundManager(models.Manager):
|
|
def get_current_vote_round(self, game=None, game_round=None):
|
|
if game_round is None:
|
|
game_round = GameRound.objects.get_current_game_round(game=game)
|
|
return self.filter(game_round=game_round).order_by('-vote_num').first()
|
|
|
|
class VoteRound(models.Model):
|
|
game_round = models.ForeignKey(GameRound, on_delete=models.CASCADE, db_index=True)
|
|
vote_num = models.IntegerField()
|
|
unique_together = (("game_round", "vote_num"),)
|
|
VOTE_STATUS_WAITING = 0
|
|
VOTE_STATUS_VOTING = 1
|
|
VOTE_STATUS_VOTED = 2
|
|
vote_status = models.IntegerField(default=VOTE_STATUS_WAITING)
|
|
leader = models.ForeignKey(Player, on_delete=models.CASCADE, related_name='vote_round_leader')
|
|
chosen = models.ManyToManyField(Player, related_name='vote_round_chosen')
|
|
started = models.DateTimeField()
|
|
chose_team = models.DateTimeField(null=True, default=None)
|
|
voted = models.DateTimeField(null=True, default=None)
|
|
|
|
objects = VoteRoundManager()
|
|
|
|
def is_team_finalized(self):
|
|
return self.vote_status != VoteRound.VOTE_STATUS_WAITING
|
|
|
|
def is_waiting_on_leader(self):
|
|
return self.vote_status == VoteRound.VOTE_STATUS_WAITING
|
|
|
|
def is_currently_voting(self):
|
|
return self.vote_status == VoteRound.VOTE_STATUS_VOTING
|
|
|
|
def is_voting_complete(self):
|
|
return self.vote_status == VoteRound.VOTE_STATUS_VOTED
|
|
|
|
def is_first_vote(self):
|
|
return self.vote_num == 1
|
|
|
|
def is_chosen_correct_size(self):
|
|
return self.chosen.count() == self.game_round.num_players_on_mission()
|
|
|
|
def is_final_vote(self):
|
|
return self.vote_num == 5
|
|
|
|
def previous_vote(self):
|
|
if self.is_first_vote():
|
|
return None
|
|
return VoteRound.objects.get(game_round=self.game_round,
|
|
vote_num=self.vote_num-1)
|
|
|
|
def vote_totals(self):
|
|
num_players = self.game_round.game.num_players()
|
|
if self.playervote_set.count() == num_players:
|
|
accepts = self.playervote_set.filter(accept=True).count()
|
|
rejects = num_players - accepts
|
|
return {'accepts': accepts, 'rejects': rejects}
|
|
else:
|
|
return None
|
|
|
|
def team_approved(self):
|
|
votes = self.vote_totals()
|
|
if votes:
|
|
return votes['accepts'] > votes['rejects']
|
|
else:
|
|
return None
|
|
|
|
def next_leader(self):
|
|
game = self.game_round.game
|
|
num_players = Player.objects.filter(game=game).count()
|
|
next_leader_order = (self.leader.order + 1) % num_players
|
|
next_leader = Player.objects.get(game=game,
|
|
order=next_leader_order)
|
|
return next_leader
|
|
|
|
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.
|
|
self.started = timezone.now()
|
|
if self.vote_status == VoteRound.VOTE_STATUS_VOTING:
|
|
self.chose_team = timezone.now()
|
|
elif self.vote_status == VoteRound.VOTE_STATUS_VOTED:
|
|
self.voted = timezone.now()
|
|
super(VoteRound, self).save(*args, **kwargs)
|
|
|
|
class PlayerVote(models.Model):
|
|
vote_round = models.ForeignKey(VoteRound, on_delete=models.CASCADE, db_index=True)
|
|
player = models.ForeignKey(Player, on_delete=models.CASCADE)
|
|
unique_together = (("vote_round", "player"),)
|
|
accept = models.BooleanField()
|