from io import BytesIO import json import math import random import qrcode from django.db import transaction from django.http import HttpResponse, Http404 from django.shortcuts import get_object_or_404, redirect, render from django.views.decorators.http import require_safe,\ require_POST,\ require_http_methods from django.urls import reverse from .forms import NewGameForm, JoinGameForm, StartGameForm from .helpers import mission_size, mission_size_string from .models import Game, GameRound, MissionAction,\ Player, PlayerVote, VoteRound # helpers to interpret arguments def lookup_access_code(func): def with_game(request, access_code, *args, **kwargs): game = get_object_or_404(Game, access_code=access_code.lower()) return func(request, game, *args, **kwargs) return with_game def lookup_player_secret(func): def with_player(request, game, player_secret, *args, **kwargs): player = get_object_or_404(Player, game=game, secret_id=player_secret) return func(request, game, player, *args, **kwargs) return with_player def nums_to_int(func): def with_int(request, game, player, round_num, vote_num, *args, **kwargs): return func(request, game, player, int(round_num), int(vote_num), *args, **kwargs) return with_int # views @require_safe def index(request): return render(request, 'index.html') @require_http_methods(["HEAD", "GET", "POST"]) def enter_code(request): if request.method == 'POST': form = JoinGameForm(request.POST) if form.is_valid(): game = form.cleaned_data.get('game') player = form.cleaned_data.get('player') if player is None: if game.game_phase == Game.GAME_PHASE_END: return redirect('game_results', access_code=game.access_code) else: return redirect('observe', access_code=game.access_code) return redirect('game', access_code=game.access_code, player_secret=player.secret_id) else: form = JoinGameForm() return render(request, 'join_game.html', {'form': form}) @require_http_methods(["HEAD", "GET", "POST"]) def new_game(request): if request.method == 'POST': form = NewGameForm(request.POST) if form.is_valid(): game = Game.objects.create() name = form.cleaned_data.get('name') if name is None: return redirect('observe', access_code=game.access_code) player = Player.objects.create(game=game, name=name) return redirect('game', access_code=game.access_code, player_secret=player.secret_id) else: form = NewGameForm() return render(request, 'new_game.html', {'form': form}) @lookup_access_code @require_safe def join_game(request, game): if game.game_phase == Game.GAME_PHASE_END: return redirect('game_results', access_code=game.access_code) else: form = JoinGameForm(initial={'game': game.access_code}) return render(request, 'join_game.html', {'access_code': game.access_code, 'form': form}) @lookup_access_code @require_safe def qr_code(request, game): join_url = reverse('join_game', kwargs={'access_code': game.access_code}) join_url = request.build_absolute_uri(join_url) img = qrcode.make(join_url) output = BytesIO() img.save(output, "PNG") return HttpResponse(output.getvalue(), content_type='image/png') @lookup_access_code @require_safe def observe(request, game): if game.game_phase == game.GAME_PHASE_END and game.next_game is not None: return redirect('observe', access_code=game.next_game.access_code) return _game(request, game, None) @lookup_access_code @require_safe def game_results(request, game): if game.game_phase != Game.GAME_PHASE_END: raise Http404() return _game(request, game, None, {'results_only': True}) def game_status_string(game, player): game_status_object = {} game_status_object['game_phase'] = game.game_phase_string() players = game.player_set.order_by('order', 'joined', 'name') num_players = players.count() if game.game_phase == Game.GAME_PHASE_LOBBY: game_status_object['players'] = [p.name for p in players] try: rounds = [mission_size_string(mission_size(num_players=num_players, round_num=round_num)) for round_num in range(1,6)] except ValueError: rounds = ['' for round_num in range(1,6)] game_status_object['rounds'] = rounds elif game.game_phase == Game.GAME_PHASE_ROLE: game_status_object['times_started'] = game.times_started ready_players = game.player_set.filter(ready=True).order_by('order') game_status_object['ready'] = [{'name': p.name, 'order': p.order} for p in ready_players] else: vote_round = VoteRound.objects.get_current_vote_round(game) game_status_object['round_num'] = vote_round.game_round.round_num game_status_object['vote_num'] = vote_round.vote_num if game.game_phase == Game.GAME_PHASE_PICK\ or game.game_phase == Game.GAME_PHASE_VOTE: chosen = vote_round.chosen.order_by('order').all() game_status_object['chosen'] = [p.name for p in chosen] game_status_object['you_chosen'] = player in chosen if game.game_phase == Game.GAME_PHASE_VOTE: votes_cast = vote_round.playervote_set.count() game_status_object['missing_votes_count'] = num_players - votes_cast player_vote = vote_round.playervote_set.filter(player=player) if player_vote: if player_vote.get().accept: game_status_object['player_vote'] = 'accept' else: game_status_object['player_vote'] = 'reject' else: game_status_object['player_vote'] = 'none' if game.game_phase == Game.GAME_PHASE_END and game.next_game is not None: game_status_object['next_game'] = game.next_game.access_code return json.dumps(game_status_object) @lookup_access_code @require_safe @transaction.non_atomic_requests def observe_status(request, game): return HttpResponse(game_status_string(game, None)) @lookup_access_code @lookup_player_secret @require_safe @transaction.non_atomic_requests def status(request, game, player): player.save() return HttpResponse(game_status_string(game, player)) def game_base_context(game, player): players = game.player_set.order_by('order', 'joined', 'name') num_players = players.count() context = {} context['status'] = game_status_string(game, player) context['access_code'] = game.access_code context['is_observer'] = player is None if player is not None: context['player_secret'] = player.secret_id context['player'] = player context['players'] = players context['num_players'] = num_players context['game_rounds'] = game.gameround_set.all().order_by('round_num') context['num_spies'] = len([p for p in players if p.is_spy()]) spy_roles = [p.role_string() for p in players if p.is_spy() and p.role != Player.ROLE_SPY] if spy_roles: spy_roles.sort() context['spy_roles'] = spy_roles context['num_resistance'] = len([p for p in players if not p.is_spy()]) resistance_roles = [p.role_string() for p in players if not p.is_spy() and p.role != Player.ROLE_GOOD] if resistance_roles and all(r is not None for r in resistance_roles): resistance_roles.sort() context['resistance_roles'] = resistance_roles if game.display_history is not None: context['display_history'] = game.display_history if game.private_voting is not None: context['private_voting'] = game.private_voting try: round_scores = {} for round_num in range(1, 6): round_scores[round_num] = {'mission_size': mission_size_string(mission_size(num_players=num_players, round_num=round_num)), 'result': ''} for game_round in game.gameround_set.all(): round_scores[game_round.round_num]['result'] = game_round.result_string() context['round_scores'] = round_scores except ValueError: pass if game.game_phase != Game.GAME_PHASE_LOBBY: context['game_has_mordred'] = players.filter(role=Player.ROLE_MORDRED)\ .exists() if player is None: context['visible_spies'] = [] else: context['visible_spies'] = [p for p in players if player.sees_as_spy(p)] if player.is_percival(): possible_merlins = " or ".join([p.name for p in players if p.appears_as_merlin()]) context['possible_merlins'] = possible_merlins return context def deterministic_random_boolean(seed): r = random.getstate() random.seed(seed) res = random.choice([True, False]) random.setstate(r) return res @lookup_access_code @lookup_player_secret @require_safe def game(request, game, player): player.save() # update last_accessed return _game(request, game, player) def _game(request, game, player, extra_context=None): context = game_base_context(game, player) if extra_context is not None: context.update(extra_context) if game.game_phase == Game.GAME_PHASE_LOBBY: context['form'] = StartGameForm() return render(request, 'lobby.html', context) elif game.game_phase == Game.GAME_PHASE_ROLE: context['times_started'] = game.times_started return render(request, 'role_phase.html', context) elif game.game_phase == Game.GAME_PHASE_PICK: vote_round = VoteRound.objects.get_current_vote_round(game=game) assert vote_round.vote_status == VoteRound.VOTE_STATUS_WAITING context['chosen'] = vote_round.chosen.order_by('order').all() vote_rejected = not vote_round.is_first_vote() context['vote_rejected'] = vote_rejected if vote_rejected: context['previous_vote'] = vote_round.previous_vote().vote_totals() context['leader'] = vote_round.leader context['round_num'] = vote_round.game_round.round_num context['vote_num'] = vote_round.vote_num context['team_size'] = vote_round.game_round.num_players_on_mission() if vote_round.leader == player: return render(request, 'pick.html', context) else: return render(request, 'pick_wait.html', context) elif game.game_phase == Game.GAME_PHASE_VOTE: vote_round = VoteRound.objects.get_current_vote_round(game=game) assert vote_round.vote_status == VoteRound.VOTE_STATUS_VOTING context['chosen'] = vote_round.chosen.order_by('order').all() context['leader'] = vote_round.leader round_num = vote_round.game_round.round_num context['round_num'] = round_num vote_num = vote_round.vote_num context['vote_num'] = vote_num player_vote = vote_round.playervote_set.filter(player=player) if player_vote: if player_vote.get().accept: context['player_vote'] = 'accept' else: context['player_vote'] = 'reject' player_votes = vote_round.playervote_set.count() num_players = context['num_players'] context['missing_votes_count'] = num_players - player_votes if player is not None: seed = "%s-%s-%d-%d" % (game.access_code, player.secret_id, round_num, vote_num) context['swap_buttons'] = deterministic_random_boolean(seed) return render(request, 'vote.html', context) elif game.game_phase == Game.GAME_PHASE_MISSION: vote_round = VoteRound.objects.get_current_vote_round(game=game) assert vote_round.vote_status == VoteRound.VOTE_STATUS_VOTED chosen = vote_round.chosen.order_by('order').all() context['chosen'] = chosen context['leader'] = vote_round.leader round_num = vote_round.game_round.round_num context['round_num'] = round_num vote_num = vote_round.vote_num context['vote_num'] = vote_num context['vote'] = vote_round.vote_totals() if player in chosen: game_round = vote_round.game_round mission_action = game_round.missionaction_set.filter(player=player) if mission_action: if mission_action.get().played_success: context['mission_action'] = 'Pass' else: context['mission_action'] = 'Fail' seed = "%s-%s-%d-%d" % (game.access_code, player.secret_id, round_num, vote_num) context['swap_buttons'] = deterministic_random_boolean(seed) return render(request, 'mission.html', context) else: return render(request, 'mission_wait.html', context) elif game.game_phase == Game.GAME_PHASE_ASSASSIN: if player is not None and player.is_assassin(): context['targets'] = [p for p in context['players'] if p != player and not player.sees_as_spy(p)] return render(request, 'assassinate.html', context) else: return render(request, 'assassinate_wait.html', context) elif game.game_phase == Game.GAME_PHASE_END: context['game_over'] = True res_wins = game.gameround_set.filter(mission_passed=True).count() res_won = res_wins == 3 and not game.player_assassinated.is_merlin() context['resistance_won'] = res_won if game.player_assassinated: context['player_assassinated'] = game.player_assassinated try: context['previous_game'] = game.previous_game except Game.DoesNotExist: pass if game.next_game: if game.next_game.game_phase != Game.GAME_PHASE_END: context['next_game_ongoing'] = True context['next_game'] = game.next_game return render(request, 'end.html', context) @lookup_access_code @lookup_player_secret @require_POST def leave(request, game, player): player.delete() num_players = game.player_set.count() if num_players == 0: game.delete() return redirect('index') @lookup_access_code @require_POST def observe_start(request, game): return _start(request, game, None) @lookup_access_code @lookup_player_secret @require_POST def start(request, game, player): player.save() # update last_accessed return _start(request, game, player) def _start(request, game, player): if game.game_phase != Game.GAME_PHASE_LOBBY: if player is None: return redirect('observe', access_code=game.access_code) else: return redirect('game', access_code=game.access_code, player_secret=player.secret_id) players = game.player_set.all() num_players = players.count() form = StartGameForm(request.POST) if num_players < 5: form.add_error(None, "You must have at least 5 players to play!") elif num_players > 10: form.add_error(None, "You can't have more than 10 players to play!") if form.is_valid(): num_spies = int(math.ceil(num_players / 3.0)) spy_roles = [] if form.cleaned_data.get('assassin'): spy_roles.append(Player.ROLE_ASSASSIN) if form.cleaned_data.get('morgana'): spy_roles.append(Player.ROLE_MORGANA) if form.cleaned_data.get('mordred'): spy_roles.append(Player.ROLE_MORDRED) if form.cleaned_data.get('oberon'): spy_roles.append(Player.ROLE_OBERON) if len(spy_roles) > num_spies: form.add_error(None, "There will only be %d spies. Select no more than that many special roles for spies." % num_spies) else: game.display_history = form.cleaned_data.get('display_history') game.private_voting = form.cleaned_data.get('private_voting') game.game_phase = Game.GAME_PHASE_ROLE game.times_started += 1 resistance_roles = [] if form.cleaned_data.get('merlin'): resistance_roles.append(Player.ROLE_MERLIN) if form.cleaned_data.get('percival'): resistance_roles.append(Player.ROLE_PERCIVAL) num_resistance = num_players - num_spies roles = spy_roles + resistance_roles +\ [Player.ROLE_SPY]*(num_spies - len(spy_roles)) +\ [Player.ROLE_GOOD]*(num_resistance - len(resistance_roles)) assert len(roles) == num_players play_order = list(range(num_players)) random.shuffle(play_order) random.shuffle(roles) for p, role, order in zip(players, roles, play_order): p.role = role p.order = order p.save() game.save() if player is None: return redirect('observe', access_code=game.access_code) else: return redirect('game', access_code=game.access_code, player_secret=player.secret_id) context = game_base_context(game, player) context['form'] = form return render(request, 'lobby.html', context) @lookup_access_code @require_POST def observe_cancel_game(request, game): if game.game_phase == Game.GAME_PHASE_ROLE: game.game_phase = Game.GAME_PHASE_LOBBY game.save() return redirect('observe', access_code=game.access_code) @lookup_access_code @lookup_player_secret @require_POST def cancel_game(request, game, player): if game.game_phase == Game.GAME_PHASE_ROLE: player.ready = False player.save() game.game_phase = Game.GAME_PHASE_LOBBY game.save() return redirect('game', access_code=game.access_code, player_secret=player.secret_id) @lookup_access_code @lookup_player_secret @require_POST def ready(request, game, player): if game.game_phase == Game.GAME_PHASE_ROLE: player.ready = True player.save() if not game.player_set.filter(ready=False): game.game_phase = Game.GAME_PHASE_PICK game.save() game_round = GameRound.objects.create(game=game, round_num=1) first_leader = Player.objects.get(game=game, order=0) vote_round = VoteRound.objects.create(game_round=game_round, vote_num=1, leader=first_leader) return redirect('game', access_code=game.access_code, player_secret=player.secret_id) @lookup_access_code @lookup_player_secret @nums_to_int @require_POST def choose(request, game, player, round_num, vote_num, who): player.save() if game.game_phase == Game.GAME_PHASE_PICK: vote_round = VoteRound.objects.get_current_vote_round(game=game) if vote_round.vote_status == VoteRound.VOTE_STATUS_WAITING\ and vote_round.game_round.round_num == round_num\ and vote_round.vote_num == vote_num\ and vote_round.leader == player: chosen_player = game.player_set.get(order=who) vote_round.chosen.add(chosen_player) vote_round.save() return redirect('game', access_code=game.access_code, player_secret=player.secret_id) @lookup_access_code @lookup_player_secret @nums_to_int @require_POST def unchoose(request, game, player, round_num, vote_num, who): player.save() if game.game_phase == Game.GAME_PHASE_PICK: vote_round = VoteRound.objects.get_current_vote_round(game=game) if vote_round.vote_status == VoteRound.VOTE_STATUS_WAITING\ and vote_round.game_round.round_num == round_num\ and vote_round.vote_num == vote_num\ and vote_round.leader == player: chosen_player = game.player_set.get(order=who) vote_round.chosen.remove(chosen_player) vote_round.save() return redirect('game', access_code=game.access_code, player_secret=player.secret_id) @lookup_access_code @lookup_player_secret @nums_to_int @require_POST def finalize_team(request, game, player, round_num, vote_num): player.save() if game.game_phase == Game.GAME_PHASE_PICK: vote_round = VoteRound.objects.get_current_vote_round(game=game) if vote_round.vote_status == VoteRound.VOTE_STATUS_WAITING\ and vote_round.game_round.round_num == round_num\ and vote_round.vote_num == vote_num\ and vote_round.leader == player\ and vote_round.is_chosen_correct_size(): vote_round.vote_status = VoteRound.VOTE_STATUS_VOTING vote_round.save() game.game_phase = Game.GAME_PHASE_VOTE game.save() return redirect('game', access_code=game.access_code, player_secret=player.secret_id) @lookup_access_code @lookup_player_secret @nums_to_int @require_POST def retract_team(request, game, player, round_num, vote_num): player.save() if game.game_phase == Game.GAME_PHASE_VOTE: vote_round = VoteRound.objects.get_current_vote_round(game=game) if vote_round.vote_status == VoteRound.VOTE_STATUS_VOTING\ and vote_round.game_round.round_num == round_num\ and vote_round.vote_num == vote_num\ and vote_round.leader == player: vote_round.vote_status = VoteRound.VOTE_STATUS_WAITING vote_round.save() game.game_phase = Game.GAME_PHASE_PICK game.save() vote_round.playervote_set.all().delete() return redirect('game', access_code=game.access_code, player_secret=player.secret_id) @lookup_access_code @lookup_player_secret @nums_to_int @require_POST def vote(request, game, player, round_num, vote_num, vote): player.save() if game.game_phase == Game.GAME_PHASE_VOTE: vote_round = VoteRound.objects.get_current_vote_round(game=game) if vote_round.vote_status == VoteRound.VOTE_STATUS_VOTING\ and vote_round.game_round.round_num == round_num\ and vote_round.vote_num == vote_num: if vote == "cancel": try: vote_round.playervote_set.get(player=player).delete() except PlayerVote.DoesNotExist: pass else: accept = vote == "approve" vote_round.playervote_set\ .update_or_create(defaults={'accept': accept}, player=player) team_approved = vote_round.team_approved() if team_approved is not None: # All players voted, voting round is over. vote_round.vote_status = VoteRound.VOTE_STATUS_VOTED vote_round.save() if team_approved: # Team was approved game.game_phase = Game.GAME_PHASE_MISSION else: # Team was rejected if vote_round.is_final_vote(): game.game_phase = Game.GAME_PHASE_END else: game.game_phase = Game.GAME_PHASE_PICK VoteRound.objects\ .create(game_round=vote_round.game_round, vote_num=vote_round.vote_num+1, leader=vote_round.next_leader()) game.save() return redirect('game', access_code=game.access_code, player_secret=player.secret_id) @lookup_access_code @lookup_player_secret @require_POST def mission(request, game, player, round_num, mission_action): round_num = int(round_num) player.save() if game.game_phase == Game.GAME_PHASE_MISSION: vote_round = VoteRound.objects.get_current_vote_round(game=game) game_round = vote_round.game_round if vote_round.vote_status == VoteRound.VOTE_STATUS_VOTED\ and game_round.round_num == round_num\ and player in vote_round.chosen.all(): passed = mission_action == "success" or not player.is_spy() game_round.missionaction_set\ .update_or_create(defaults={'played_success': passed}, player=player) num_on_mission = game_round.num_players_on_mission() if game_round.missionaction_set.count() == num_on_mission: num_fails_required = game_round.num_fails_required() fails = game_round.missionaction_set\ .filter(played_success=False).count() game_round.mission_passed = fails < num_fails_required game_round.save() res_wins = game.gameround_set.filter(mission_passed=True)\ .count() spy_wins = game.gameround_set.filter(mission_passed=False)\ .count() if res_wins == 3: # resistance wins... except for assassin game.game_phase = Game.GAME_PHASE_ASSASSIN game.save() elif spy_wins == 3: # spies win game.game_phase = Game.GAME_PHASE_END game.save() else: # another round game.game_phase = Game.GAME_PHASE_PICK game.save() next_round_num = game_round.round_num+1 game_round = GameRound.objects\ .create(game=game, round_num=next_round_num) next_leader = vote_round.next_leader() vote_round = VoteRound.objects\ .create(game_round=game_round, vote_num=1, leader=next_leader) return redirect('game', access_code=game.access_code, player_secret=player.secret_id) @lookup_access_code @lookup_player_secret @require_POST def assassinate(request, game, player, target): player.save() if game.game_phase == Game.GAME_PHASE_ASSASSIN\ and player.is_assassin(): target_player = game.player_set.get(order=target) game.player_assassinated = target_player game.game_phase = Game.GAME_PHASE_END game.save() return redirect('game', access_code=game.access_code, player_secret=player.secret_id) @lookup_access_code @lookup_player_secret @require_safe def next_game(request, game, player): if game.game_phase != Game.GAME_PHASE_END: raise Http404() next_game = game.create_or_get_next_game() if next_game.game_phase == Game.GAME_PHASE_LOBBY: try: next_player = Player.objects.get(game=next_game, name=player.name) if not next_player.is_expired(): redirect('join_game', access_code=next_game.access_code) except Player.DoesNotExist: next_player = Player.objects.create(game=next_game, name=player.name) return redirect('game', access_code=next_game.access_code, player_secret=next_player.secret_id) else: raise Http404() @lookup_access_code @require_safe def observe_next_game(request, game): if game.game_phase != Game.GAME_PHASE_END: raise Http404() next_game = game.create_or_get_next_game() redirect('observe_game', access_code=next_game.access_code)