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