Initial models and forms done; working on views.

feature/js
Daniel Perelman 4 年之前
父節點 984b80abf7
當前提交 b8b686d4ed

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

@ -1,5 +0,0 @@
from django.apps import AppConfig
class FearTrackerConfig(AppConfig):
name = 'fear_tracker'

@ -0,0 +1,55 @@
from django import forms
from .models import Game, Player
class PlayerForm(forms.Form):
name = forms.CharField(max_length=80, label="Name", required=False)
spirit = forms.TypedChoiceField(label="Spirit",
choices=enumerate(Player.SPIRIT_NAMES),
empty_value=None, coerce=int)
class BasePlayerFormSet(forms.BaseFormSet):
def clean(self):
if any(self.errors):
return
names = set()
for form in self.forms:
if self.can_delete and self._should_delete_form(form):
continue
name = form.cleaned_data.get('name')
if name:
if name in names:
raise forms.ValidationError(
"Players must have distinct names.")
names.add(name)
if not names:
raise forms.ValidationError("Must have at least one player.")
PlayerFormSet = forms.formset_factory(PlayerForm, formset=BasePlayerFormSet)
class NewGameForm(forms.Form):
combined_growth_spirit = forms.BooleanField(
required=False, initial=True,
label="Combine Growth and Spirit phases into a single phase")
england_build = forms.BooleanField(
required=False, initial=False,
label="High Immigration (extra build phase for England level 3+)")
fear_per_player = forms.IntegerField(
label="Fear per player", initial=4, min_value=1, max_value=99)
class JoinGameForm(forms.Form):
game = forms.CharField(label='Access code',
max_length=Game.ACCESS_CODE_LENGTH)
def clean_game(self):
data = self.cleaned_data['game']
try:
return Game.objects.get(access_code=data.lower())
except Game.DoesNotExist:
raise forms.ValidationError("Invalid access code.")

@ -1,3 +1,252 @@
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_FEAR = 6
GAME_PHASE_FEAR_CARDS = [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
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=False)
england_build = 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:
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_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.initial_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()
initial_fear = current_phase.starting_fear +\
current_phase.fear_this_phase()
else:
initial_fear = 0
(next_turn, next_phase) = self.next_turn_and_phase()
self.game_turn = next_turn
self.game_phase = next_phase
self.save()
next_phase = self.get_current_phase()
if next_phase is None:
Phase.objects.create(
game=self,
game_turn=next_turn,
game_phase=next_phase,
initial_fear=initial_fear,
started=current_time)
else:
next_phase.started = current_time
next_phase.initial_fear = initial_fear
next_phase.ended = None
next_phase.save()
@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_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 = [
"Lightning's Swift Strike",
"River Surges in Sunlight",
"Vital Strength of the Earth",
"Shadows Flicker Like Flame",
"Thunderspeaker",
"A Spread of Rampant Green",
"Ocean's Hungry Grasp",
"Bringer of Dreams and Nightmares",
"Keeper of the Forbidden Wilds",
"Sharp Fangs Behind the Leaves",
"Serpent Slumbering Beneath the Island",
"Heart of the Wildfire",
]
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]
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
# Create your models here.
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()
towns_destroyed = models.IntegerField()
cities_destroyed = models.IntegerField()

@ -0,0 +1,67 @@
footer hr {
border-width: 0;
border-top: 1px solid lightgray;
}
footer p {
text-align: center;
font-size: 0.7em;
margin: 0.4em;
color: darkgrey;
}
footer a, footer a:visited {
color: darkgray;
}
.players-form p {
margin: 0;
}
.player-1 {
background: #f0ba57;
}
.player-2 {
background: #73fcf6;
}
.player-3 {
background: #bc9c8b;
}
.player-4 {
background: #77a0a6;
}
.player-5 {
background: #fff172;
}
.player-6 {
background: #3adb85;
}
.player-7 {
background: #99c8e1;
}
.player-8 {
background: #f73347;
}
.player-9 {
background: #cac186;
}
.player-10 {
background: #8ca684;
}
.player-11 {
background: #f068a9;
}
.player-12 {
background: #ed822b;
}

@ -0,0 +1,32 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="{% static 'css/styles.css' %}" type="text/css">
<title>Spirit Island Fear Tracker</title>
</head>
<body>
{% block content %}{% endblock %}
<footer>
<hr>
<p>
<a href="https://boardgamegeek.com/boardgame/162886/spirit-island">Spirit
Island</a>
was designed by
<a href="https://boardgamegeek.com/boardgamedesigner/16615/r-eric-reuss">Eric Reuss</a>
and published by
<a href="https://www.greaterthangames.com/">Greater Than Games</a>
</p>
<p>
Spirit Island Fear Tracker was built by
<a href="https://aweirdimagination.net/~perelman/">Daniel Perelman</a>
|
<a href="https://git.aweirdimagination.net/perelman/fear-tracker">source
code</a> licensed under
<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPLv3+</a>
</p>
</footer>
</body>
</html>

@ -0,0 +1,19 @@
{% 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>
</div>
<div class="button-container">
<a href="{% url 'index' %}" class="button button-main-menu">Back</a>
</div>
</form>
{% endblock %}

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block content %}
<header>
<div class="phase">{{ phase }} phase of turn #{{ turn }}</div>
<div class="fear_summary">
<span class="available_fear_cards">{{ available_fear_cards }}</span>
fear cards available;
<span class="fear_to_next_card">{{ fear_to_next_card }}</span>
fear to next fear card;
players have generated
<span class="fear_this_phase">{{ fear_this_phase }}</span>
fear this phase
</div>
<div class="access-code">
<span class="header">Access Code:</span> <a href="{% url 'qr_code' access_code=access_code %}" target="_blank">{{ access_code }}</a>
</div>
</header>
<div class="players">
{% for order, player in players.items %}
<div class="player player-{{ player.order }}">
<div class="player-summary">
{% if player.ready %}(ready){% else %}(waiting...){% endif %}
(<span class="player-total-fear player-{{ player.order }}-total-fear">{{ player.total_fear }}</span> fear)
{{ player.name }} ({{ player.get_spirit_name }})
</div>
<div class="player-effects">
{% for effect_num, effect in player.fear.items %}
<div class="effect effect-{{ effect_num }}">
Effect #{{ effect_num|add:1 }}
{{ effect.pure_fear }} fear
{{ effect.towns }} towns
{{ effect.cities }} cities
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endblock %}

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<div class="button-container">
<a href="{% url 'new_game' %}" class="button button-new-game">New Game</a>
<a href="{% url 'enter_code' %}" class="button button-join-game">Join Game</a>
</div>
{% endblock %}

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block content %}
<h2>New Game</h2>
<form class="new-game-form" action="#" method="post">
{% csrf_token %}
{{ formset.non_form_errors }}
<table class="new-game-form">
{{ form.as_table }}
</table>
{{ formset.management_form }}
<table class="players-form">
{% for form in formset %}
<tr class="player-{{ form.player_id }}">
<th>Player #{{ form.player_id }}</th>
<td>{{ form.as_p }}</td>
</tr>
{% endfor %}
</table>
<div class="button-container">
<input type="submit" class="button button-create" value="Create Game"></input>
</div>
<div class="button-container">
<a href="{% url 'index' %}" class="button button-main-menu">Back</a>
</div>
</form>
{% endblock %}

@ -0,0 +1,30 @@
"""fear_tracker URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf.urls import include, url
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('join/', views.enter_code, name='enter_code'),
path('new/', views.new_game, name='new_game'),
url(r'^(?P<access_code>[a-zA-Z]{6})/', include([
path('', views.game, name='game'),
path('qr/', views.qr_code, name='qr_code'),
path('status/', views.status, name='status'),
])),
]

@ -1,3 +1,133 @@
from django.shortcuts import render
from collections import OrderedDict
from io import BytesIO
import qrcode
# Create your views here.
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_safe, require_http_methods
from django.urls import reverse
from .forms import NewGameForm, JoinGameForm, PlayerFormSet
from .models import Game, Player
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
@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['game']
return redirect('game', access_code=game.access_code)
else:
form = JoinGameForm()
return render(request, 'enter_code.html', {'form': form})
@require_http_methods(["HEAD", "GET", "POST"])
def new_game(request):
if request.method == 'POST':
form = NewGameForm(request.POST)
formset = PlayerFormSet(request.POST)
if form.is_valid() and formset.is_valid():
player_forms = list(filter(lambda p: p.cleaned_data.get('name'),
formset.forms))
num_players = len(player_forms)
game = Game()
game.combined_growth_spirit =\
form.cleaned_data.get('combined_growth_spirit')
game.england_build = form.cleaned_data.get('england_build')
game.fear_per_card =\
form.cleaned_data.get('fear_per_player') * num_players
game.save()
for player_form in player_forms:
player = Player()
player.order = int(player_form.prefix[5:]) + 1
player.spirit = player_form.cleaned_data.get('spirit')
player.name = player_form.cleaned_data.get('name')
player.game = game
player.save()
game.advance_phase()
return redirect('game', access_code=game.access_code)
else:
form = NewGameForm()
initial_player_data = [{'spirit': i}
for i in enumerate(Player.SPIRIT_NAMES)]
formset = PlayerFormSet(initial=initial_player_data)
for player_form in formset:
player_form.player_id = int(player_form.prefix[5:]) + 1
return render(request, 'new_game.html',
{'form': form, 'formset': formset})
@require_safe
def qr_code(request, access_code):
join_url = reverse('game', kwargs={'access_code': access_code.lower()})
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 game(request, game):
players = OrderedDict()
for player in game.player_set.order_by('order').all():
player.total_fear = 0
player.fear = OrderedDict()
players[player.order] = player
for fear in game.get_current_phase().fear_set.order_by('effect').all():
player[fear.player.order].fear[fear.effect] = {
'pure_fear': fear.pure_fear,
'towns': fear.towns_destroyed,
'cities': fear.cities_destroyed,
}
players[fear.player.order].total_fear +=\
fear.pure_fear + fear.towns_destroyed + 2*fear.cities_destroyed
for player in players.values():
info = player.fear
if not info:
new_effect = 0
else:
new_effect = max(info.keys()) + 1
info[new_effect] = {
'pure_fear': 0,
'towns': 0,
'cities': 0,
}
return render(request, 'game.html', {
'access_code': game.access_code,
'turn': game.game_turn,
'phase': game.get_current_phase_name(),
'available_fear_cards': game.num_available_fear_cards(),
'fear_to_next_card': game.get_fear_to_next_card(),
'fear_this_phase': game.get_current_phase().fear_this_phase(),
'players': players,
})
@lookup_access_code
@require_safe
def status(request, game):
# TODO status json
pass

@ -31,6 +31,7 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'fear_tracker',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',

@ -13,9 +13,9 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.conf.urls import include
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('fear_tracker.urls')),
]

Loading…
取消
儲存