| @@ -0,0 +1,2 @@ | |||
| __pycache__ | |||
| venv | |||
| @@ -1,113 +0,0 @@ | |||
| <html> | |||
| <head> | |||
| <meta charset="utf-8"> | |||
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||
| <meta name="viewport" content="width=device-width,initial-scale=1.0"> | |||
| <link rel="icon" href="favicon.ico"> | |||
| <link rel="stylesheet" href="/static/normalize.css"> | |||
| <title>IF Game</title> | |||
| <!-- currently 1.1.0 --> | |||
| <script src="/static/htmx.min.js"></script> | |||
| <script scr="/static/alpine.2.8.2.min.js"></script> | |||
| <style> | |||
| #app { | |||
| background-color: gray; | |||
| height: 100vh; | |||
| display: flex; | |||
| flex-direction: column; | |||
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |||
| } | |||
| #header { | |||
| background-color: black; | |||
| color: white; | |||
| position: sticky; | |||
| display: flex; | |||
| flex-direction: row; | |||
| padding: 0.7em; | |||
| } | |||
| #headerContent { | |||
| margin: auto auto; | |||
| } | |||
| #narrativeContainerWrapper { | |||
| overflow: auto; | |||
| display: flex; | |||
| flex-direction:column-reverse; | |||
| background-color: white; | |||
| width: 80ch; | |||
| max-width: 80ch; | |||
| height: calc(90% - 4em); | |||
| /*max-height: calc(90% - 2em);*/ | |||
| margin: 0 auto; | |||
| margin-top: 1em; | |||
| scrollbar-width: none; | |||
| } | |||
| #narrativeContainerWrapper:hover { | |||
| scrollbar-width: unset; | |||
| } | |||
| .narrativeItem { | |||
| padding: 1em; | |||
| } | |||
| #playerEntry { | |||
| background-color: white; | |||
| width: 80ch; | |||
| margin: auto auto; | |||
| font-size: 1.2em; | |||
| display: flex; | |||
| align-items: center; | |||
| padding: 0 1em; | |||
| } | |||
| #playerEntry input { | |||
| border: 0; | |||
| outline: none; | |||
| width: 100%; | |||
| padding: 1rem; | |||
| font-weight: bold | |||
| } | |||
| </style> | |||
| <link rel="stylesheet" href="/static/custom.css"> | |||
| </head> | |||
| <body> | |||
| <div id="app"> | |||
| <div id="header"> | |||
| <div id="headerContent"> | |||
| I'm a header! | |||
| </div> | |||
| </div> | |||
| <div id="narrativeContainerWrapper"> | |||
| <div id="narrativeContainer"> | |||
| <script> | |||
| var x = 0; | |||
| setInterval(function () { | |||
| document.getElementById('lastNarrativeItem').insertAdjacentHTML('beforebegin', `<div style="height:100px" class="narrativeItem">Bogus content ${x}!</div>`) | |||
| x++; | |||
| }, 1000); | |||
| </script> | |||
| <div id="lastNarrativeItem"> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div id="playerEntry"> | |||
| > <input type="text" spellcheck="false"> | |||
| </div> | |||
| </div> | |||
| </body> | |||
| </html> | |||
| @@ -1 +0,0 @@ | |||
| hrs | |||
| @@ -0,0 +1,2 @@ | |||
| call ../venv/scripts/Activate.bat | |||
| uvicorn blockingSampleApp:app --reload --no-access-log --log-level warning --port 5777 --host 0.0.0.0 | |||
| @@ -0,0 +1,53 @@ | |||
| #standard | |||
| import asyncio | |||
| import html | |||
| import traceback | |||
| #custom | |||
| import dataset | |||
| from fastapi import FastAPI, WebSocket, Form, File, UploadFile, Response, Request, WebSocketDisconnect, websockets | |||
| from fastapi.responses import HTMLResponse | |||
| from fastapi.staticfiles import StaticFiles | |||
| from fastapi.templating import Jinja2Templates | |||
| import starlette.status as status | |||
| from starlette.responses import RedirectResponse | |||
| app = FastAPI() | |||
| app.mount('/static', StaticFiles(directory="./static"), name="static") | |||
| templates = Jinja2Templates(directory="templates") | |||
| @app.get('/', response_class=HTMLResponse) | |||
| async def getRoot(request: Request): | |||
| return RedirectResponse('/static/index.html', status_code=status.HTTP_302_FOUND) | |||
| db = dataset.connect('sqlite:///../state.db') | |||
| @app.websocket('/websocket') | |||
| async def runGameSession(websocket: WebSocket): | |||
| await websocket.accept() | |||
| try: | |||
| await websocket.send_json({ | |||
| 'type': 'header', | |||
| 'rawHTML': 'Testing Header Text' | |||
| }) | |||
| while True: | |||
| await websocket.send_json({ | |||
| 'type': 'narrative', | |||
| 'rawHTML': 'Something <i>interesting</i> happened.' | |||
| }) | |||
| while True: | |||
| try: | |||
| resp = await asyncio.wait_for(websocket.receive_json(), timeout=5) | |||
| await websocket.send_json({ | |||
| 'type': 'playerEcho', | |||
| 'rawHTML': '> ' + html.escape(resp['playerInput']) | |||
| }) | |||
| break | |||
| except asyncio.exceptions.TimeoutError: | |||
| # timeout | |||
| await websocket.send_json({ | |||
| 'type': 'narrativeUpdate', | |||
| 'rawHTML': 'Something <i>EVEN MORE interesting</i> happened.' | |||
| }) | |||
| except Exception as e: | |||
| traceback.print_exc() | |||
| @@ -0,0 +1,9 @@ | |||
| dataset | |||
| click | |||
| tracery | |||
| stringcase | |||
| fastapi | |||
| aiofiles | |||
| python-multipart | |||
| uvicorn[standard] | |||
| jinja2 | |||
| @@ -0,0 +1,174 @@ | |||
| <html> | |||
| <head> | |||
| <meta charset="utf-8"> | |||
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||
| <meta name="viewport" content="width=device-width,initial-scale=1.0"> | |||
| <link rel="icon" href="favicon.ico"> | |||
| <link rel="stylesheet" href="/static/normalize.css"> | |||
| <title>IF Game</title> | |||
| <!-- currently htmx 1.1.0 --> | |||
| <script src="/static/htmx.min.js"></script> | |||
| <script src="/static/alpine.2.8.2.min.js"></script> | |||
| <!-- https://github.com/joewalnes/reconnecting-websocket/blob/master/reconnecting-websocket.min.js --> | |||
| <script src="/static/reconnecting-websocket.js"></script> | |||
| <style> | |||
| #app { | |||
| background-color: gray; | |||
| height: 100vh; | |||
| display: flex; | |||
| flex-direction: column; | |||
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |||
| } | |||
| #header { | |||
| background-color: black; | |||
| color: white; | |||
| position: sticky; | |||
| display: flex; | |||
| flex-direction: row; | |||
| padding: 0.7em; | |||
| } | |||
| #headerContent { | |||
| margin: auto auto; | |||
| } | |||
| #narrativeContainerWrapper { | |||
| overflow: auto; | |||
| display: flex; | |||
| flex-direction:column-reverse; | |||
| background-color: white; | |||
| width: 80ch; | |||
| max-width: 80ch; | |||
| height: calc(90% - 6em); | |||
| /*max-height: calc(90% - 2em);*/ | |||
| margin: 0 auto; | |||
| margin-top: 1em; | |||
| margin-bottom: 1em; | |||
| padding-bottom: 1em; | |||
| scrollbar-width: none; | |||
| } | |||
| #narrativeContainerWrapper:hover { | |||
| scrollbar-width: unset; | |||
| } | |||
| .narrativeItem { | |||
| padding: 0.2em 1em; | |||
| } | |||
| .playerEchoItem { | |||
| padding: 0.2em 1em; | |||
| font-weight: bold; | |||
| } | |||
| #playerEntry { | |||
| background-color: white; | |||
| width: 80ch; | |||
| margin: auto auto; | |||
| font-size: 1.2em; | |||
| display: flex; | |||
| align-items: center; | |||
| padding: 0 1em; | |||
| } | |||
| #playerEntryInput { | |||
| border: 0; | |||
| outline: none; | |||
| width: 100%; | |||
| padding: 1rem; | |||
| font-weight: bold | |||
| } | |||
| </style> | |||
| <link rel="stylesheet" href="/static/custom.css"> | |||
| <script> | |||
| var currNarrativeElement = 0; | |||
| function addNarrativeHTML(rawHTML) { | |||
| document.getElementById('lastNarrativeItem').insertAdjacentHTML('beforebegin', `<div class="narrativeItem" id="narrativeItem-${currNarrativeElement}">${rawHTML}</div>`) | |||
| currNarrativeElement++; | |||
| } | |||
| function replaceNarrativeHTML(rawHTML) { | |||
| document.getElementById(`narrativeItem-${currNarrativeElement-1}`).innerHTML = rawHTML; | |||
| } | |||
| var currPlayerEcho = 0; | |||
| function addPlayerEcho(rawHTML) { | |||
| document.getElementById('lastNarrativeItem').insertAdjacentHTML('beforebegin', `<div class="playerEchoItem" id="playerEchoItem-${currPlayerEcho}">${rawHTML}</div>`) | |||
| currPlayerEcho++; | |||
| } | |||
| function resetNarrative() { | |||
| // Clear narrative items and reset counter | |||
| currNarrativeElement = 0; | |||
| document.querySelectorAll('.narrativeItem').forEach(e => e.remove()); | |||
| } | |||
| var ws = new ReconnectingWebSocket(`ws://${window.location.host}/websocket`); | |||
| ws.addEventListener('open', function(event) { | |||
| resetNarrative(); | |||
| }); | |||
| ws.addEventListener('message', function(event) { | |||
| console.log(event.data); | |||
| let message = JSON.parse(event.data); | |||
| if (message.type === 'narrative') { | |||
| addNarrativeHTML(message.rawHTML); | |||
| } else if (message.type === 'narrativeUpdate') { | |||
| replaceNarrativeHTML(message.rawHTML); | |||
| } else if (message.type === 'playerEcho') { | |||
| addPlayerEcho(message.rawHTML); | |||
| } else if (message.type === 'header') { | |||
| document.getElementById('headerContent').innerHTML = message.rawHTML; | |||
| } | |||
| }); | |||
| </script> | |||
| </head> | |||
| <body> | |||
| <div id="app"> | |||
| <div id="header"> | |||
| <div id="headerContent"> | |||
| I'm a header! | |||
| </div> | |||
| </div> | |||
| <div id="narrativeContainerWrapper"> | |||
| <div id="narrativeContainer"> | |||
| <div id="lastNarrativeItem"></div> | |||
| </div> | |||
| </div> | |||
| <div id="playerEntry"> | |||
| > <input id="playerEntryInput" type="text" spellcheck="false"> | |||
| <script> | |||
| document.getElementById('playerEntryInput').addEventListener('keyup', function(event){ | |||
| if (event.key === 'Enter') { | |||
| let val = document.getElementById('playerEntryInput').value; | |||
| if (val === '') { | |||
| val = document.getElementById('playerEntryInput').placeholder; | |||
| } | |||
| ws.send(JSON.stringify({ | |||
| 'action': 'playerInputPrompt', | |||
| 'playerInput': val | |||
| })); | |||
| document.getElementById('playerEntryInput').value = ''; | |||
| document.getElementById('playerEntryInput').placeholder = val; | |||
| } | |||
| if (event.key === 'ArrowUp') { | |||
| document.getElementById('playerEntryInput').value = document.getElementById('playerEntryInput').placeholder; | |||
| } | |||
| }); | |||
| </script> | |||
| </div> | |||
| </div> | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1 @@ | |||
| !function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.ReconnectingWebSocket=b()}(this,function(){function a(b,c,d){function l(a,b){var c=document.createEvent("CustomEvent");return c.initCustomEvent(a,!1,!1,b),c}var e={debug:!1,automaticOpen:!0,reconnectInterval:1e3,maxReconnectInterval:3e4,reconnectDecay:1.5,timeoutInterval:2e3};d||(d={});for(var f in e)this[f]="undefined"!=typeof d[f]?d[f]:e[f];this.url=b,this.reconnectAttempts=0,this.readyState=WebSocket.CONNECTING,this.protocol=null;var h,g=this,i=!1,j=!1,k=document.createElement("div");k.addEventListener("open",function(a){g.onopen(a)}),k.addEventListener("close",function(a){g.onclose(a)}),k.addEventListener("connecting",function(a){g.onconnecting(a)}),k.addEventListener("message",function(a){g.onmessage(a)}),k.addEventListener("error",function(a){g.onerror(a)}),this.addEventListener=k.addEventListener.bind(k),this.removeEventListener=k.removeEventListener.bind(k),this.dispatchEvent=k.dispatchEvent.bind(k),this.open=function(b){h=new WebSocket(g.url,c||[]),b||k.dispatchEvent(l("connecting")),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","attempt-connect",g.url);var d=h,e=setTimeout(function(){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","connection-timeout",g.url),j=!0,d.close(),j=!1},g.timeoutInterval);h.onopen=function(){clearTimeout(e),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onopen",g.url),g.protocol=h.protocol,g.readyState=WebSocket.OPEN,g.reconnectAttempts=0;var d=l("open");d.isReconnect=b,b=!1,k.dispatchEvent(d)},h.onclose=function(c){if(clearTimeout(e),h=null,i)g.readyState=WebSocket.CLOSED,k.dispatchEvent(l("close"));else{g.readyState=WebSocket.CONNECTING;var d=l("connecting");d.code=c.code,d.reason=c.reason,d.wasClean=c.wasClean,k.dispatchEvent(d),b||j||((g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onclose",g.url),k.dispatchEvent(l("close")));var e=g.reconnectInterval*Math.pow(g.reconnectDecay,g.reconnectAttempts);setTimeout(function(){g.reconnectAttempts++,g.open(!0)},e>g.maxReconnectInterval?g.maxReconnectInterval:e)}},h.onmessage=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onmessage",g.url,b.data);var c=l("message");c.data=b.data,k.dispatchEvent(c)},h.onerror=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onerror",g.url,b),k.dispatchEvent(l("error"))}},1==this.automaticOpen&&this.open(!1),this.send=function(b){if(h)return(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","send",g.url,b),h.send(b);throw"INVALID_STATE_ERR : Pausing to reconnect websocket"},this.close=function(a,b){"undefined"==typeof a&&(a=1e3),i=!0,h&&h.close(a,b)},this.refresh=function(){h&&h.close()}}return a.prototype.onopen=function(){},a.prototype.onclose=function(){},a.prototype.onconnecting=function(){},a.prototype.onmessage=function(){},a.prototype.onerror=function(){},a.debugAll=!1,a.CONNECTING=WebSocket.CONNECTING,a.OPEN=WebSocket.OPEN,a.CLOSING=WebSocket.CLOSING,a.CLOSED=WebSocket.CLOSED,a}); | |||