| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
7432b6ad07 | Add sample app and i/o | 4 years ago |
|
|
f346df1d49 | Prepping for websocket connection | 4 years ago |
| @@ -0,0 +1,2 @@ | |||||
| __pycache__ | |||||
| venv | |||||
| @@ -1 +1,14 @@ | |||||
| # IF Console Template | # IF Console Template | ||||
| A header bar. | |||||
| A darker colored background. A center column on top. | |||||
| A text prompt inline, on the bottom row, with no border. The text prompt should start at the top of the page, and as text is added, it should crawl down. Once it comes within a few rows of the bottom, set the margin there. | |||||
| All text above prompt should then crawl up the screen. | |||||
| --- | |||||
| Maybe make top bar a floating bar, and let the whole page scroll down? | |||||
| @@ -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}); | |||||