Implemented setting up a game up until actually playing.

pull/11/head
Daniel Perelman 8 years ago
parent 61eab85a28
commit fc492aa18d

@ -1,3 +1,5 @@
jQuery is licensed according to https://jquery.org/license/
The HTML and CSS are based on https://github.com/athtran/avalon
All modifications to them and all other files in this repository are licensed
under the below license.

@ -79,6 +79,7 @@ DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
'ATOMIC_REQUESTS': True,
}
}

@ -0,0 +1,75 @@
from django import forms
from .models import Game, Player
class NewGameForm(forms.Form):
name = forms.CharField(label='Name', max_length=80)
class JoinGameForm(forms.Form):
game = forms.CharField(label='Access code',
max_length=Game.ACCESS_CODE_LENGTH)
player = forms.CharField(label='Name', max_length=80)
def clean_game(self):
data = self.cleaned_data['game']
try:
return Game.objects.get(access_code=data)
except Game.DoesNotExist:
raise forms.ValidationError("Invalid access code.")
def clean(self):
cleaned_data = super(JoinGameForm, self).clean()
game = cleaned_data.get("game")
name = cleaned_data.get("player")
if game is None or name is None:
return
try:
player = Player.objects.get(game=game, name=name)
if player.is_expired():
player.change_secret_id();
player.save()
cleaned_data["player"] = player
else:
self.add_error('player', "Please choose a different name; there is already a player using that name.")
self.add_error('player', "Please try again in a few seconds if you are trying to rejoin.")
except Player.DoesNotExist:
if game.game_phase == Game.GAME_PHASE_LOBBY:
player = Player.objects.create(game=game, name=name)
cleaned_data["player"] = player
else:
self.add_error('player', "That game has already started. If you want to rejoin, please enter your name exactly as you did before.")
class StartGameForm(forms.Form):
display_history = forms.BooleanField(required=False, initial=True,
label="show history table")
merlin = forms.BooleanField(required=False, initial=True, label="Merlin")
percival = forms.BooleanField(required=False, initial=True,
label="Percival")
assassin = forms.BooleanField(required=False, initial=True,
label="Assassin")
morgana = forms.BooleanField(required=False, initial=True, label="Morgana")
mordred = forms.BooleanField(required=False, initial=False,
label="Mordred")
oberon = forms.BooleanField(required=False, initial=False, label="Oberon")
def clean(self):
cleaned_data = super(StartGameForm, self).clean()
merlin = cleaned_data.get("merlin")
percival = cleaned_data.get("percival")
assassin = cleaned_data.get("assassin")
morgana = cleaned_data.get("morgana")
mordred = cleaned_data.get("mordred")
if assassin and not merlin:
self.add_error('assassin', "The assassin requires Merlin to be in the game.")
if percival and not merlin:
self.add_error('percival', "Percival requires Merlin to be in the game.")
if morgana and (not merlin or not percival):
self.add_error('morgana', "Morgana requies Merlin and Percival to be in the game.")
if mordred and not merlin:
self.add_error('mordred', "Mordred requires Merlin to be in the game.")

@ -0,0 +1,54 @@
def mission_size(num_players, round_num):
if num_players == 5:
if round_num == 1:
return (2, 1)
elif round_num == 2:
return (3, 1)
elif round_num == 3:
return (2, 1)
elif round_num == 4:
return (3, 1)
elif round_num == 5:
return (3, 1)
elif num_players == 6:
if round_num == 1:
return (2, 1)
elif round_num == 2:
return (3, 1)
elif round_num == 3:
return (4, 1)
elif round_num == 4:
return (3, 1)
elif round_num == 5:
return (4, 1)
elif num_players == 7:
if round_num == 1:
return (2, 1)
elif round_num == 2:
return (3, 1)
elif round_num == 3:
return (3, 1)
elif round_num == 4:
return (4, 2)
elif round_num == 5:
return (4, 1)
elif num_players > 7 and num_players < 11:
if round_num == 1:
return (3, 1)
elif round_num == 2:
return (4, 1)
elif round_num == 3:
return (5, 1)
elif round_num == 4:
return (5, 2)
elif round_num == 5:
return (5, 1)
else:
raise ValueError("Invalid # of players %d or round number %d" %\
(num_players, round_num))
def mission_size_string(mission_size):
if mission_size[1] == 2:
return "%d*" % mission_size[0]
else:
return "%d" % mission_size[0]

@ -1,22 +1,47 @@
from __future__ import unicode_literals
from datetime import datetime, timedelta
import random
import string
from django.db import models
from django.utils import timezone
def generate_code(length):
return "".join([random.choice(string.ascii_lowercase)
for i in xrange(length)])
class Game(models.Model):
access_key = models.CharField(db_index=True, unique=True, max_length=6)
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_VOTING = 3
GAME_PHASE_VOTE = 3
GAME_PHASE_MISSION = 4
GAME_PHASE_ASSASSIN = 5
GAME_PHASE_END = 6
game_phase = models.IntegerField(default=GAME_PHASE_LOBBY)
player_assassinated = models.ForeignKey('Player', related_name='+')
display_history = models.NullBooleanField()
player_assassinated = models.ForeignKey('Player', null=True, default=None,
related_name='+')
# 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
super(Game, self).save(*args, **kwargs)
class Player(models.Model):
game = models.ForeignKey(Game, on_delete=models.CASCADE, db_index=True)
secret_id = models.CharField(db_index=True, max_length=8)
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
@ -26,17 +51,112 @@ class Player(models.Model):
ROLE_GOOD = 1
ROLE_MERLIN = 2
ROLE_PERCIVAL = 3
role = models.IntegerField()
role = models.IntegerField(null=True, default=None)
order = models.IntegerField(null=True, default=None)
ready = models.BooleanField(default=False)
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.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 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()
def winner_string(self):
if self.mission_passed is None:
return ''
elif self.mission_passed:
return 'resistance'
else:
return 'spy'
class MissionAction(models.Model):
game_round = models.ForeignKey(GameRound, on_delete=models.CASCADE, db_index=True)
player = models.ForeignKey(Player, on_delete=models.CASCADE)

@ -218,7 +218,7 @@ li.voter {
.game-options li {
text-align: left;
margin: 0;
padding: 10px;
padding: 0;
border-bottom: 0;
}
@ -231,7 +231,6 @@ label {
display: inline;
}
.role-options,
#role-info {
display: none;
}

File diff suppressed because one or more lines are too long

@ -5,7 +5,8 @@
<link rel="stylesheet" href="{% static 'css/normalize.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/skeleton.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/styles.css' %}" type="text/css">
<title>Resistance</title>
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<title>Resistance</title>
</head>
<body>
@ -17,11 +18,15 @@
{% endblock %}
{% block score %}
<div class="score">
{% for round_num, round in round_scores.items %}
<div class="score-box {{ round.winner }}"><p>{{ round.mission_size }}</p></div>
{% empty %}
<div class="score-box"><p></p></div>
<div class="score-box"><p></p></div>
<div class="score-box"><p></p></div>
<div class="score-box"><p></p></div>
<div class="score-box"><p></p></div>
{% endfor %}
</div>
{% endblock %}
<br>

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block content %}
{% block game_header %}{% endblock %}
{% block game_content %}{% endblock %}
<div class="button-container">
<a href="{% url 'game' access_code=access_code player_secret=player_secret %}" class="button">Refresh</a>
</div>
{% endblock %}

@ -0,0 +1,57 @@
{% extends "game.html" %}
{% block game_header %}
<div class="role-info">
<hr class="small-hr">
<div id="role-info">
<p>Team: <b class="{{ player.team }}">{{ player.team|title }}</b> | Role: {{ player.role_string }}</p>
{% if player.is_spy and not player.is_oberon %}
<p>Other spies:
{% for spy in visible_spies %}
<b class="spy">{{ spy.name }} </b>
{% endfor %}
</p>
{% endif %}
{% if player.is_merlin %}
<p>The spies {% if game_has_mordred %}(except Mordred){% endif %} are:
{% for spy in visible_spies %}
<b class="spy">{{ spy.name }} </b>
{% endfor %}
</p>
{% endif %}
{% if player.is_percival %}
<p>Merlin is {{ possible_merlins }}.</p>
{% endif %}
{% if player.is_assassin %}
<p>Try to see if you can spot who the Merlin is. You'll have a chance to identify him at the end of the game to win it.</p>
{% endif %}
{% if player.is_morgana %}
<p>Percival sees you as Merlin.</p>
{% endif %}
{% if player.is_mordred %}
<p>Merlin does not know you are a spy.</p>
{% endif %}
</div>
<p id="role-info-hidden">
Tap to view role info
</p>
</div>
<script>
$(document).ready(function () {
$('.role-info').click(function () {
$('#role-info').toggle();
$('#role-info-hidden').toggle();
})
})
</script>
{% endblock %}
{% block history %}
{% endblock %}

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block content %}
<h2>Join Game</h2>
<form class="join-game-form" action="{% url 'enter_code' %}" method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<div class="button-container">
<input type="submit" class="button button-join" value="Join"></input>
<a href="{% url 'index' %}" class="button button-main-menu">Back</a>
</div>
</form>
{% endblock %}

@ -0,0 +1,31 @@
{% extends "game.html" %}
{% block game_content %}
<div class="lobby">
<h2>Lobby</h2>
<form method="post">
{% csrf_token %}
<div class="lobby-info">
<div class="access-code">
<p>Access Code: {{ access_code }}</p>
</div>
<div class="game-options">
<p class="options-header">Options</p>
<div class="role-options">
{{ form.as_ul }}
</div>
</div>
</div>
<ul>
{% for player in players %}
<li>{{ player.name }}</li>
{% endfor %}
</ul>
<div class="button-container">
<button type="submit" formaction="{% url 'start' access_code=access_code player_secret=player_secret %}" class="button-start">Start</button>
<button type="submit" formaction="{% url 'leave' access_code=access_code player_secret=player_secret %}" class="button-leave">Leave</button>
</div>
</form>
</div>
{% endblock %}

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block content %}
<h2>New Game</h2>
<form class="new-game-form" action="#" method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<div class="button-container">
<input type="submit" class="button button-create" value="Create"></input>
<a href="{% url 'index' %}" class="button button-main-menu">Back</a>
</div>
</form>
{% endblock %}

@ -0,0 +1,20 @@
{% extends "in_game.html" %}
{% block game_content %}
<div class="role-phase">
<h2>Role Phase</h2>
<p><b>Ready up!</b></p>
<ul>
{% for p in players %}
<li class="{% if p.ready %}ready{% endif %}">{{ p.name }}</li>
{% endfor %}
</ul>
<form method="post" action="{% url 'ready' access_code=access_code player_secret=player_secret %}">
{% csrf_token %}
<div class="button-container">
<button type="submit">Ready</button>
</div>
</form>
</div>
{% endblock %}

@ -7,13 +7,15 @@ urlpatterns = [
url(r'^join/$', views.enter_code, name='enter_code'),
url(r'^new/$', views.new_game, name='new_game'),
url(r'^(?P<access_code>[a-z]{6})/', include([
url(r'$', views.join_game, name='join_game'),
url(r'^$', views.join_game, name='join_game'),
url(r'(?P<player_secret>[a-z]{8})/', include([
url(r'^$', views.game, name='game'),
url(r'^start/$', views.start, name='start'),
url(r'^leave/$', views.leave, name='leave'),
url(r'^ready/$', views.ready, name='ready'),
url(r'^vote/(?P<round_num>[1-5])/(?P<vote_num>[1-5])/(?P<vote>(approve|reject))/$', views.vote, name='vote'),
url(r'^choose/(?P<round_num>[1-5])/(?P<vote_num>[1-5])/(?P<who>[0-9]+)/$', views.choose, name='choose'),
url(r'^remove/(?P<round_num>[1-5])/(?P<vote_num>[1-5])/(?P<who>[0-9]+)/$', views.remove, name='remove'),
url(r'^remove/(?P<round_num>[1-5])/(?P<vote_num>[1-5])/(?P<who>[0-9]+)/$', views.unchoose, name='unchoose'),
url(r'^mission/(?P<round_num>[1-5])/(?P<mission_action>(success|fail))/$', views.mission, name='mission'),
url(r'^assassinate/(?P<target>[0-9]+)/$', views.choose, name='choose'),
])),

@ -1,10 +1,15 @@
import math
import random
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_safe,\
require_POST,\
require_http_methods
from .models import Game, Player
from .forms import NewGameForm, JoinGameForm, StartGameForm
from .helpers import mission_size, mission_size_string
from .models import Game, GameRound, Player
# helpers to interpret arguments
def lookup_access_code(func):
@ -30,53 +35,215 @@ def index(request):
@require_http_methods(["HEAD", "GET", "POST"])
def enter_code(request):
if request.method == 'GET':
pass
elif request.method == 'POST':
pass
if request.method == 'POST':
form = JoinGameForm(request.POST)
if form.is_valid():
game = form.cleaned_data.get('game')
player = form.cleaned_data.get('player')
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 == 'GET':
pass
elif request.method == 'POST':
pass
if request.method == 'POST':
form = NewGameForm(request.POST)
if form.is_valid():
game = Game.objects.create()
name = form.cleaned_data.get('name')
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):
pass
form = JoinGameForm(initial={'game': game.access_code})
return render(request, 'join_game.html', {'access_code': game.access_code,
'form': form})
def game_base_context(game, player):
players = Player.objects.filter(game=game).order_by('order')
num_players = players.count()
context = {}
context['access_code'] = game.access_code
context['player_secret'] = player.secret_id
context['players'] = players
context['player'] = player
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)), 'winner': ''}
for game_round in GameRound.objects.filter(game=game):
round_scores[game_round.round_num]['winner'] = game_round.winner_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()
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
@lookup_player_secret
@lookup_access_code
@lookup_player_secret
@require_safe
def game(request, game, player):
pass
player.save() # update last_accessed
context = game_base_context(game, player)
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:
return render(request, 'role_phase.html', context)
elif game.game_phase == Game.GAME_PHASE_PICK:
pass
elif game.game_phase == Game.GAME_PHASE_VOTE:
pass
elif game.game_phase == Game.GAME_PHASE_MISSION:
pass
elif game.game_phase == Game.GAME_PHASE_ASSASSIN:
pass
elif game.game_phase == Game.GAME_PHASE_END:
pass
else:
pass
return render(request, 'in_game.html', context)
@lookup_access_code
@lookup_player_secret
@require_POST
def leave(request, game, player):
player.delete()
num_players = Player.objects.filter(game=game).count()
if num_players == 0:
game.delete()
return redirect('index')
@lookup_access_code
def ready(request, game, player):
pass
@lookup_player_secret
@require_POST
def start(request, game, player):
player.save() # update last_accessed
if game.game_phase != Game.GAME_PHASE_LOBBY:
return redirect('game', access_code=game.access_code,
player_secret=player.secret_id)
players = Player.objects.filter(game=game)
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.game_phase = Game.GAME_PHASE_ROLE
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 = 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()
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
@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 Player.objects.filter(game=game, ready=False):
game.game_phase = Game.GAME_PHASE_PICK
game.save()
GameRound.objects.create(game=game, round_num=1)
return redirect('game', access_code=game.access_code,
player_secret=player.secret_id)
@lookup_access_code
@lookup_player_secret
def choose(request, game, player, round_num, vote_num, who):
pass
@lookup_player_secret
@lookup_access_code
def remove(request, game, player, round_num, vote_num, who):
@lookup_player_secret
def unchoose(request, game, player, round_num, vote_num, who):
pass
@lookup_player_secret
@lookup_access_code
@lookup_player_secret
def vote(request, game, player, round_num, vote_num, vote):
pass
@lookup_player_secret
@lookup_access_code
@lookup_player_secret
def mission(request, game, player, round_num, mission_action):
pass
@lookup_player_secret
@lookup_access_code
@lookup_player_secret
def assassinate(request, game, player, target):
pass

Loading…
Cancel
Save