Compare commits

...

2 Commits

Author SHA1 Message Date
Daniel Perelman 99d15b4f40 Add skeleton of Django Channels project. 2020-07-25 14:53:44 -07:00
Daniel Perelman 17b781d182 More detailed design notes. 2020-07-25 14:53:03 -07:00
15 changed files with 424 additions and 1 deletions

102
design
View File

@ -1 +1,101 @@
[Design plans will go here.]
Game setup involves a collection of Cards selected to be included in the game
and Players playing the game. Each Player connects to the game via a
web browser and choses a name.
Each Card included in the Game is randomly assigned to Player or "buried".
The buried Card is omitted from the game; some Cards have rules which
forbid them to be buried.
Each Card has a Color, Allegiance (differs from Color for e.g. spies),
RoleName, human-readable Description, and collection of Powers and
Conditions it gives to the Player that gets it at the start of the game.
Code-wise: In the database, Cards, Powers, and Conditions are all
represented in the database by strings identifying them that correspond
to the names of Python classes which implement callbacks for every way
they could impact gameplay.
To support private table-talk (for the online version), have a concept
of chat messages being "whispers" (to a sub-room with additional
commands for inviting people to a sub-room or disbanding it (or
kicking?)) or "yells" (to entire room, even those in whisper groups).
# Database Tables
## Game
game_id INT PRIMARY KEY,
## Round
# Rounds are created at the start of the game, so round number and length
# can be set. Must be numbered consecutively 1 through max_round.
round_id INT PRIMARY KEY,
game_id INT FOREIGN KEY (Game),
round_num INT, # 1-5
round_length INT, # round length in sections
## Event - master table for all events
event_id INT PRIMARY KEY,
game_id INT FOREIGN KEY (Game),
timestamp DATETIME,
round,
room,
event_kind,
actor FOREIGN KEY (Player),
target_player FOREIGN KEY (Player) NULL,
### Event kinds
* round_start / round_end ?
* color_share_offer / color_share / color_share_reject:
(card_share_offer / card_share / card_share_reject)
* Accepting causes an immediate color share, so there's no
color_share_accept event.
* color_share_offer may not be rescinded (by game rules)...
* ... but is automatically considered rejected at the end of a round.
* color_private_reveal / color_public_reveal / color_permanent_reveal
(card_private_reveal / card_public_reveal / card_permanent_reveal)
* Unilateral actions, so no offer/accept.
* private_reveal has a player target; public_reveal is to entire room.
* Permanent reveals may be caused by powers.
* claim_leader
* no target, requires no current leader
* usurp
* initiate a vote, requires not being current leader
* should votes be tracked by a separate table?
* vote
* Must have an active usurp
* ... a votes table would make this easier to track.
* Targets a player.
* change_leader
* Targets a player, announcing they are now leader
(due to claim_leader or vote event)
* select_hostage / deselect_hostage
* Only leader can take this action.
* Hostages should probably be tracked by a separate table or
flag in the Players table?
* use_power? Or maybe mark events caused by a power to track usages?
## Player
player_id INT PRIMARY KEY,
name CHAR(20),
## PlayerCondition
player FOREIGN KEY (Player),
condition CHAR,
relatedPlayer FOREIGN KEY (Player) NULL, # e.g. for the "in love" condition
gained DATETIME,
lost DATETIME NULL,

21
manage.py Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'two_rooms_site.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

0
two_rooms/__init__.py Normal file
View File

5
two_rooms/forms.py Normal file
View File

@ -0,0 +1,5 @@
# from django import forms
# from .models import Game, Player
# TODO ...

44
two_rooms/models.py Normal file
View File

@ -0,0 +1,44 @@
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 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_ACTIVE = 1
GAME_PHASE_PARLEY = 2
GAME_PHASE_END = 3
game_phase = models.IntegerField(default=GAME_PHASE_LOBBY)
created = models.DateTimeField()
ended = models.DateTimeField(null=True, default=None)
# 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)
class Player(models.Model):
game = models.ForeignKey(Game, on_delete=models.CASCADE, db_index=True)
name = models.CharField(max_length=80)
unique_together = (("game", "name"),)

16
two_rooms/routing.py Normal file
View File

@ -0,0 +1,16 @@
from channels.http import AsgiHandler
from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf.urls import url
from . import views
application = ProtocolTypeRouter({
"http": URLRouter([
url(r'^(?P<access_code>[a-zA-Z]{6})/', URLRouter([
url(r"status/(?P<hashcode>[a-z0-9]{64})/",
views.StatusLongPollConsumer, name='status'),
])),
url(r"", AsgiHandler),
]),
})

3
two_rooms/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

31
two_rooms/urls.py Normal file
View File

@ -0,0 +1,31 @@
"""two_rooms 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('update/', views.update_game, name='update_game'),
path('qr/', views.qr_code, name='qr_code'),
path('status/', views.status, name='status'),
])),
]

22
two_rooms/views.py Normal file
View File

@ -0,0 +1,22 @@
# from collections import OrderedDict
# from http import HTTPStatus
# from io import BytesIO
# import hashlib
# import json
# import qrcode
# from asgiref.sync import async_to_sync
# from channels.generic.http import AsyncHttpConsumer
# from channels.layers import get_channel_layer
# from django.db import transaction
# 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,\
# require_POST
# from django.urls import reverse
# from .models import Game, Player
# TODO ...

View File

12
two_rooms_site/asgi.py Normal file
View File

@ -0,0 +1,12 @@
"""
ASGI entrypoint. Configures Django and then runs the application
defined in the ASGI_APPLICATION setting.
"""
import os
import django
from channels.routing import get_default_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "two_rooms_site.settings")
django.setup()
application = get_default_application()

View File

@ -0,0 +1,3 @@
import two_rooms.routing
application = two_rooms.routing.application

129
two_rooms_site/settings.py Normal file
View File

@ -0,0 +1,129 @@
"""
Django settings for two_rooms_site project.
Generated by 'django-admin startproject' using Django 2.2.11.
For more information on this file, see
https://docs.djangoproject.com/en/2.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.2/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = r"GXAf)EfZ]T&L?_^4Hk~'>01flT55C;tT/\G{x$;Ni4{.}^nt8h"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'channels',
'two_rooms',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'two_rooms_site.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'two_rooms_site.wsgi.application'
ASGI_APPLICATION = 'two_rooms_site.routing.application'
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = '/static/'

21
two_rooms_site/urls.py Normal file
View File

@ -0,0 +1,21 @@
"""two_rooms_site 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
from django.urls import path
urlpatterns = [
path('', include('two_rooms.urls')),
]

16
two_rooms_site/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for two_rooms_site project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'two_rooms_site.settings')
application = get_wsgi_application()