avalon-django/avalon/avalon_game/views.py

720 lines
28 KiB
Python

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)