Kaynağa Gözat

Initial models and forms done; working on views.

feature/js
Daniel Perelman 1 yıl önce
ebeveyn
işleme
b8b686d4ed
14 değiştirilmiş dosya ile 663 ekleme ve 13 silme
  1. +0
    -3
      fear_tracker/admin.py
  2. +0
    -5
      fear_tracker/apps.py
  3. +55
    -0
      fear_tracker/forms.py
  4. +250
    -1
      fear_tracker/models.py
  5. +67
    -0
      fear_tracker/static/css/styles.css
  6. +32
    -0
      fear_tracker/templates/base.html
  7. +19
    -0
      fear_tracker/templates/enter_code.html
  8. +40
    -0
      fear_tracker/templates/game.html
  9. +8
    -0
      fear_tracker/templates/index.html
  10. +27
    -0
      fear_tracker/templates/new_game.html
  11. +30
    -0
      fear_tracker/urls.py
  12. +132
    -2
      fear_tracker/views.py
  13. +1
    -0
      fear_tracker_site/settings.py
  14. +2
    -2
      fear_tracker_site/urls.py

+ 0
- 3
fear_tracker/admin.py Dosyayı Görüntüle

@@ -1,3 +0,0 @@
from django.contrib import admin

# Register your models here.

+ 0
- 5
fear_tracker/apps.py Dosyayı Görüntüle

@@ -1,5 +0,0 @@
from django.apps import AppConfig


class FearTrackerConfig(AppConfig):
name = 'fear_tracker'

+ 55
- 0
fear_tracker/forms.py Dosyayı Görüntüle

@@ -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.")

+ 250
- 1
fear_tracker/models.py Dosyayı Görüntüle

@@ -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()

+ 67
- 0
fear_tracker/static/css/styles.css Dosyayı Görüntüle

@@ -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;
}

+ 32
- 0
fear_tracker/templates/base.html Dosyayı Görüntüle

@@ -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>

+ 19
- 0
fear_tracker/templates/enter_code.html Dosyayı Görüntüle

@@ -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 %}

+ 40
- 0
fear_tracker/templates/game.html Dosyayı Görüntüle

@@ -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 %}

+ 8
- 0
fear_tracker/templates/index.html Dosyayı Görüntüle

@@ -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 %}

+ 27
- 0
fear_tracker/templates/new_game.html Dosyayı Görüntüle

@@ -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 %}

+ 30
- 0
fear_tracker/urls.py Dosyayı Görüntüle

@@ -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'),
])),
]

+ 132
- 2
fear_tracker/views.py Dosyayı Görüntüle

@@ -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

+ 1
- 0
fear_tracker_site/settings.py Dosyayı Görüntüle

@@ -31,6 +31,7 @@ ALLOWED_HOSTS = []
# Application definition

INSTALLED_APPS = [
'fear_tracker',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',


+ 2
- 2
fear_tracker_site/urls.py Dosyayı Görüntüle

@@ -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')),
]

Yükleniyor…
İptal
Kaydet