Compare commits
No commits in common. "main" and "history/short-polling-only" have entirely different histories.
main
...
history/sh
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -60,4 +60,3 @@ target/
|
||||||
# sqlite database
|
# sqlite database
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
|
||||||
fear_tracker/migrations
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from .models import Game, Player
|
||||||
class PlayerForm(forms.Form):
|
class PlayerForm(forms.Form):
|
||||||
name = forms.CharField(max_length=80, label="Name", required=False)
|
name = forms.CharField(max_length=80, label="Name", required=False)
|
||||||
spirit = forms.TypedChoiceField(label="Spirit",
|
spirit = forms.TypedChoiceField(label="Spirit",
|
||||||
choices=Player.enumerate_spirit_names(),
|
choices=enumerate(Player.SPIRIT_NAMES),
|
||||||
empty_value=None, coerce=int)
|
empty_value=None, coerce=int)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -247,42 +247,18 @@ class Player(models.Model):
|
||||||
game = models.ForeignKey(Game, on_delete=models.CASCADE, db_index=True)
|
game = models.ForeignKey(Game, on_delete=models.CASCADE, db_index=True)
|
||||||
name = models.CharField(max_length=80)
|
name = models.CharField(max_length=80)
|
||||||
SPIRIT_NAMES = [
|
SPIRIT_NAMES = [
|
||||||
# From https://spiritislandwiki.com/index.php?title=List_of_Spirits
|
"Lightning's Swift Strike",
|
||||||
# Base game
|
"River Surges in Sunlight",
|
||||||
("Lightning's Swift Strike", "SI", "L"),
|
"Vital Strength of the Earth",
|
||||||
("River Surges in Sunlight", "SI", "L"),
|
"Shadows Flicker Like Flame",
|
||||||
("Vital Strength of the Earth", "SI", "L"),
|
"Thunderspeaker",
|
||||||
("Shadows Flicker Like Flame", "SI", "L"),
|
"A Spread of Rampant Green",
|
||||||
("Thunderspeaker", "SI", "M"),
|
"Ocean's Hungry Grasp",
|
||||||
("A Spread of Rampant Green", "SI", "M"),
|
"Bringer of Dreams and Nightmares",
|
||||||
("Ocean's Hungry Grasp", "SI", "H"),
|
"Keeper of the Forbidden Wilds",
|
||||||
("Bringer of Dreams and Nightmares", "SI", "H"),
|
"Sharp Fangs Behind the Leaves",
|
||||||
# Branch and Claw
|
"Serpent Slumbering Beneath the Island",
|
||||||
("Keeper of the Forbidden Wilds", "BC", "M"),
|
"Heart of the Wildfire",
|
||||||
("Sharp Fangs Behind the Leaves", "BC", "M"),
|
|
||||||
# Promo Pack 1
|
|
||||||
("Serpent Slumbering Beneath the Island", "P1", "H"),
|
|
||||||
("Heart of the Wildfire", "P1", "H"),
|
|
||||||
# Jagged Earth
|
|
||||||
("Stone's Unyielding Defiance", "JE", "M"),
|
|
||||||
("Shifting Memory of Ages", "JE", "M"),
|
|
||||||
("Grinning Trickster Stirs Up Trouble", "JE", "M"),
|
|
||||||
("Lure of the Deep Wilderness", "JE", "M"),
|
|
||||||
("Many Minds Move as One", "JE", "M"),
|
|
||||||
("Volcano Looming High", "JE", "M"),
|
|
||||||
("Shroud of Silent Mist", "JE", "H"),
|
|
||||||
("Vengeance as a Burning Plague", "JE", "H"),
|
|
||||||
("Starlight Seeks Its Form", "JE", "V"),
|
|
||||||
("Fractured Days Split the Sky", "JE", "V"),
|
|
||||||
# Promo Pack 2
|
|
||||||
("Downpour Drenches the World", "P2", "H"),
|
|
||||||
("Finder of Paths Unseen", "P2", "V"),
|
|
||||||
# Horizons of Spirit Island
|
|
||||||
("Devouring Teeth Lurk Underfoot", "HS", "L"),
|
|
||||||
("Eyes Watch From the Trees", "HS", "L"),
|
|
||||||
("Fathomless Mud of the Swamp", "HS", "L"),
|
|
||||||
("Rising Heat of Stone and Sand", "HS", "L"),
|
|
||||||
("Sun-Bright Whirlwind", "HS", "L"),
|
|
||||||
]
|
]
|
||||||
spirit = models.IntegerField()
|
spirit = models.IntegerField()
|
||||||
order = models.IntegerField()
|
order = models.IntegerField()
|
||||||
|
@ -291,12 +267,7 @@ class Player(models.Model):
|
||||||
unique_together = (("game", "name"), ("game", "order"))
|
unique_together = (("game", "name"), ("game", "order"))
|
||||||
|
|
||||||
def get_spirit_name(self):
|
def get_spirit_name(self):
|
||||||
return Player.SPIRIT_NAMES[self.spirit][0]
|
return Player.SPIRIT_NAMES[self.spirit]
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def enumerate_spirit_names():
|
|
||||||
return [(i, f"[{spirit[1]},{spirit[2]}] {spirit[0]}") for (i, spirit)
|
|
||||||
in enumerate(Player.SPIRIT_NAMES)]
|
|
||||||
|
|
||||||
|
|
||||||
class Phase(models.Model):
|
class Phase(models.Model):
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
|
||||||
|
|
||||||
import django
|
|
||||||
from django.conf.urls import url
|
|
||||||
from django.core.asgi import get_asgi_application
|
|
||||||
|
|
||||||
# TODO Importing views breaks if django.setup() isn't called first...
|
|
||||||
# ... but this isn't the way this is supposed to work.
|
|
||||||
django.setup()
|
|
||||||
|
|
||||||
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.as_asgi(), name='status'),
|
|
||||||
])),
|
|
||||||
url(r"", get_asgi_application()),
|
|
||||||
]),
|
|
||||||
})
|
|
|
@ -360,23 +360,13 @@
|
||||||
el.addEventListener("change", formElementChanged);
|
el.addEventListener("change", formElementChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkStatus() {
|
setInterval(function() {
|
||||||
if(activeRequests.size != 0) return;
|
if(activeRequests.size != 0) return;
|
||||||
// From https://stackoverflow.com/a/50101022
|
|
||||||
|
|
||||||
const abort = new AbortController();
|
|
||||||
const signal = abort.signal;
|
|
||||||
|
|
||||||
// 50 second timeout:
|
|
||||||
const timeoutId = setTimeout(() => abort.abort(), 50000);
|
|
||||||
|
|
||||||
fetch(new Request("{% url 'status' access_code=access_code %}"
|
fetch(new Request("{% url 'status' access_code=access_code %}"
|
||||||
+ (statusObj.hash != ""
|
+ (statusObj.hash != ""
|
||||||
? statusObj.hash + "/"
|
? statusObj.hash + "/"
|
||||||
: "")),
|
: "")))
|
||||||
{signal})
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
clearTimeout(timeoutId);
|
|
||||||
if(response.status === 304) {
|
if(response.status === 304) {
|
||||||
// TODO Just skip the next step?
|
// TODO Just skip the next step?
|
||||||
return statusObj;
|
return statusObj;
|
||||||
|
@ -391,14 +381,8 @@
|
||||||
} else {
|
} else {
|
||||||
statusObj = data;
|
statusObj = data;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.then(checkStatus)
|
|
||||||
.catch(() => {
|
|
||||||
// If something went wrong, wait a few seconds before retrying.
|
|
||||||
setTimeout(checkStatus, 5000);
|
|
||||||
});
|
});
|
||||||
}
|
}, 5000);
|
||||||
checkStatus();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -27,5 +27,7 @@ urlpatterns = [
|
||||||
path('update/', views.update_game, name='update_game'),
|
path('update/', views.update_game, name='update_game'),
|
||||||
path('qr/', views.qr_code, name='qr_code'),
|
path('qr/', views.qr_code, name='qr_code'),
|
||||||
path('status/', views.status, name='status'),
|
path('status/', views.status, name='status'),
|
||||||
|
url('^status/(?P<hashcode>[a-z0-9]{64})/',
|
||||||
|
views.status, name='status'),
|
||||||
])),
|
])),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,11 +5,6 @@ import hashlib
|
||||||
import json
|
import json
|
||||||
import qrcode
|
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.db import transaction
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
@ -81,7 +76,7 @@ def new_game(request):
|
||||||
form = NewGameForm()
|
form = NewGameForm()
|
||||||
|
|
||||||
initial_player_data = [{'spirit': i}
|
initial_player_data = [{'spirit': i}
|
||||||
for (i, _) in Player.enumerate_spirit_names()]
|
for i in enumerate(Player.SPIRIT_NAMES)]
|
||||||
formset = PlayerFormSet(initial=initial_player_data)
|
formset = PlayerFormSet(initial=initial_player_data)
|
||||||
for player_form in formset:
|
for player_form in formset:
|
||||||
player_form.player_id = int(player_form.prefix[5:]) + 1
|
player_form.player_id = int(player_form.prefix[5:]) + 1
|
||||||
|
@ -262,7 +257,7 @@ def handle_game_request(request, game, update):
|
||||||
f"#{effect_num} {effect_kind} " +
|
f"#{effect_num} {effect_kind} " +
|
||||||
"was changed by another user to " +
|
"was changed by another user to " +
|
||||||
f"{current_value}, so " +
|
f"{current_value}, so " +
|
||||||
"your attempt to change it from " +
|
f"your attempt to change it from " +
|
||||||
f"{orig} to {amount} " +
|
f"{orig} to {amount} " +
|
||||||
"was not processed.")
|
"was not processed.")
|
||||||
else:
|
else:
|
||||||
|
@ -293,22 +288,12 @@ def handle_game_request(request, game, update):
|
||||||
res['value'] = current_value
|
res['value'] = current_value
|
||||||
else:
|
else:
|
||||||
res = {'success': True}
|
res = {'success': True}
|
||||||
async_to_sync(get_channel_layer().group_send)(
|
|
||||||
"%s_status" % game.access_code,
|
|
||||||
{"type": "fear_tracker.invalidate_status"})
|
|
||||||
return HttpResponse(json.dumps(res))
|
return HttpResponse(json.dumps(res))
|
||||||
|
|
||||||
players = get_players_with_fear(game, current_phase, players)
|
players = get_players_with_fear(game, current_phase, players)
|
||||||
status_obj = game_status_object(game, current_phase, players)
|
status_obj = game_status_object(game, current_phase, players)
|
||||||
status_string = json.dumps(status_obj)
|
status_string = json.dumps(status_obj)
|
||||||
|
|
||||||
async_to_sync(get_channel_layer().group_send)(
|
|
||||||
"%s_status" % game.access_code, {
|
|
||||||
"type": "fear_tracker.hashcode_seen",
|
|
||||||
"hashcode": status_obj['hash'],
|
|
||||||
"status_string": status_string,
|
|
||||||
})
|
|
||||||
|
|
||||||
for player in players.values():
|
for player in players.values():
|
||||||
info = player.fear
|
info = player.fear
|
||||||
if not info:
|
if not info:
|
||||||
|
@ -339,57 +324,4 @@ def status(request, game, hashcode=None):
|
||||||
return HttpResponse(status=HTTPStatus.NOT_MODIFIED)
|
return HttpResponse(status=HTTPStatus.NOT_MODIFIED)
|
||||||
else:
|
else:
|
||||||
status_string = json.dumps(status_obj)
|
status_string = json.dumps(status_obj)
|
||||||
async_to_sync(get_channel_layer().group_send)(
|
|
||||||
"%s_status" % game.access_code, {
|
|
||||||
"type": "fear_tracker.hashcode_seen",
|
|
||||||
"hashcode": status_obj['hash'],
|
|
||||||
"status_string": status_string,
|
|
||||||
})
|
|
||||||
return HttpResponse(status_string)
|
return HttpResponse(status_string)
|
||||||
|
|
||||||
|
|
||||||
class StatusLongPollConsumer(AsyncHttpConsumer):
|
|
||||||
async def handle(self, body):
|
|
||||||
self.access_code = self.scope["url_route"]["kwargs"]["access_code"]
|
|
||||||
self.hashcode = self.scope["url_route"]["kwargs"]["hashcode"]
|
|
||||||
|
|
||||||
await self.channel_layer.group_add("%s_status" % self.access_code,
|
|
||||||
self.channel_name)
|
|
||||||
await self.channel_layer.group_send(
|
|
||||||
"%s_status" % self.access_code, {
|
|
||||||
"type": "fear_tracker.hashcode_seen",
|
|
||||||
"hashcode": self.hashcode,
|
|
||||||
})
|
|
||||||
|
|
||||||
async def http_request(self, message):
|
|
||||||
"""
|
|
||||||
Async entrypoint - concatenates body fragments and hands off control
|
|
||||||
to ``self.handle`` when the body has been completely received.
|
|
||||||
"""
|
|
||||||
if "body" in message:
|
|
||||||
self.body.append(message["body"])
|
|
||||||
if not message.get("more_body"):
|
|
||||||
await self.handle(b"".join(self.body))
|
|
||||||
|
|
||||||
async def disconnect(self):
|
|
||||||
await self.channel_layer.group_discard("%s_status" % self.access_code,
|
|
||||||
self.channel_name)
|
|
||||||
|
|
||||||
async def fear_tracker_hashcode_seen(self, event):
|
|
||||||
if self.hashcode != event["hashcode"]:
|
|
||||||
if "status_string" in event and event["status_string"]:
|
|
||||||
body = event["status_string"].encode('utf-8')
|
|
||||||
await self.send_response(200, body)
|
|
||||||
await self.disconnect()
|
|
||||||
await self.channel_layer.group_send(
|
|
||||||
"%s_status" % self.access_code, {
|
|
||||||
"type": "fear_tracker.invalidate_status",
|
|
||||||
})
|
|
||||||
|
|
||||||
async def fear_tracker_invalidate_status(self, event):
|
|
||||||
no_hash_status = reverse('status',
|
|
||||||
kwargs={'access_code': self.access_code})
|
|
||||||
await self.send_response(302, b'', headers=[
|
|
||||||
(b"Location", no_hash_status.encode('utf-8'))
|
|
||||||
])
|
|
||||||
await self.http_disconnect(None)
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
"""
|
|
||||||
ASGI entrypoint. Configures Django and then runs the application
|
|
||||||
defined in the ASGI_APPLICATION setting.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import django
|
|
||||||
|
|
||||||
import fear_tracker.routing
|
|
||||||
|
|
||||||
django.setup()
|
|
||||||
application = fear_tracker.routing.application
|
|
|
@ -1,3 +0,0 @@
|
||||||
import fear_tracker.routing
|
|
||||||
|
|
||||||
application = fear_tracker.routing.application
|
|
|
@ -31,7 +31,6 @@ ALLOWED_HOSTS = []
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'channels',
|
|
||||||
'fear_tracker',
|
'fear_tracker',
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
|
@ -70,7 +69,6 @@ TEMPLATES = [
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'fear_tracker_site.wsgi.application'
|
WSGI_APPLICATION = 'fear_tracker_site.wsgi.application'
|
||||||
ASGI_APPLICATION = 'fear_tracker_site.routing.application'
|
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
|
@ -83,12 +81,6 @@ DATABASES = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANNEL_LAYERS = {
|
|
||||||
"default": {
|
|
||||||
"BACKEND": "channels.layers.InMemoryChannelLayer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||||
|
|
Loading…
Reference in New Issue
Block a user