Compare commits

..

3 Commits

Author SHA1 Message Date
9cd3803b61 Fix up dockerfile
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-19 02:43:34 +01:00
09d3d8684c Rewrite server using quart instead of flask 2023-01-19 02:03:44 +01:00
d9eb2d7b05 Save player object for each chat message 2023-01-18 21:55:57 +01:00
9 changed files with 177 additions and 183 deletions

View File

@ -14,7 +14,7 @@ FROM python:3.11-alpine
COPY glebby-server /glebby-server
WORKDIR /glebby-server
RUN pip install -r requirements.txt
RUN pip install gunicorn
COPY --from=frontend /glebby-client/static /glebby-server/static
RUN pip install hypercorn
COPY --from=frontend /glebby-client/static /glebby-server/glebby/static
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--threads", "100", "glebby:app"]
CMD ["hypercorn", "--bind", "0.0.0.0:5000", "glebby:app"]

View File

@ -1,5 +1,6 @@
- [ ] composition api
- [ ] quart
- [x] quart
- [x] fix dockerfile
- [ ] lobby
- [ ] actual game logic
- [ ] websocket url config

View File

@ -27,7 +27,7 @@ export default defineComponent({
},
data() {
return {
chatMessages: [] as { from: number, message: string }[],
chatMessages: [] as { from: Player, message: string }[],
playerName: '',
chatMessage: ''
@ -55,7 +55,7 @@ export default defineComponent({
break
case 'chat':
this.chatMessages.push({
from: message.from,
from: getPlayer(this.model, message.from)!,
message: payload.message
})
break
@ -127,8 +127,8 @@ export default defineComponent({
<h2>chat</h2>
<div class="chatbox-scroller">
<div class="chatbox-scroller-content">
<div v-for="message in chatMessages" class="chatbox-message">
<code v-if="model.players.get(message.from)">{{ model.players.get(message.from)!.name }}#{{ message.from }}:</code> {{ message.message }}
<div v-for="chatMessage in chatMessages" class="chatbox-message">
<code>{{ chatMessage.from.name }}#{{ chatMessage.from.id }}:</code> {{ chatMessage.message }}
</div>
</div>
</div>

View File

@ -1,171 +0,0 @@
import json
import simple_websocket
from collections import OrderedDict
from flask import Flask
from flask_sock import Sock
from queue import Queue
from random import Random
from threading import Thread, Lock
class BoardGenerator:
def __init__(self, seed):
self.dice = [
'aeaneg',
'wngeeh',
'ahspco',
'lnhnrz',
'aspffk',
'tstiyd',
'objoab',
'owtoat',
'iotmuc',
'erttyl',
'ryvdel',
'toessi',
'lreixd',
'terwhv',
'eiunes',
'nuihmq'
]
self.rng = Random(seed)
def generate_board(self):
shuffled_dice = self.rng.sample(self.dice, k=len(self.dice))
roll_results = [self.rng.choice(die) for die in shuffled_dice]
return [roll_results[4 * i : 4 * (i+1)] for i in range(0, 4)]
class Client:
def __init__(self, sock, client_id):
self.sock = sock
self.data = { 'id': client_id, 'name': '' }
class GlebbyState:
def __init__(self):
self.clients_lock = Lock()
# We want to preserve the order that clients arrived in,
# e.g. for whose turn it is
self.clients = OrderedDict()
self.next_client_id_lock = Lock()
self.next_client_id = 0
self.incoming_messages = Queue()
self.board_generator = BoardGenerator(42)
self.board = None
# domain stuff
def handle_message_from(self, client_id, payload):
print(f"{client_id}: {payload}")
if payload['type'] == 'set-name':
self.clients[client_id].data['name'] = payload['name']
self.broadcast(client_id, {
'type': 'set-name',
'name': payload['name']
})
elif payload['type'] == 'chat':
self.broadcast(client_id, {
'type': 'chat',
'message': payload['message']
})
elif payload['type'] == 'roll':
self.board = self.board_generator.generate_board()
self.broadcast(client_id, {
'type': 'roll',
'board': self.board
})
else:
print("Unhandled!")
def get_state_dto(self, client_id):
return {
'yourId': client_id,
'players': [
self.clients[other_client_id].data
for other_client_id in self.clients
],
'board': self.board
}
# message receiving and sending
def put_incoming_message(self, client_id, payload):
self.incoming_messages.put({
'from': client_id,
'payload': json.loads(payload)
})
def send_to(self, client_id_from, client_id_to, payload):
self.clients[client_id_to].sock.send(json.dumps({
'from': client_id_from,
'payload': payload
}))
def broadcast(self, client_id_from, payload):
with self.clients_lock:
for client_id_to in self.clients:
self.send_to(client_id_from, client_id_to, payload)
# client management
def _get_next_client_id(self):
with self.next_client_id_lock:
client_id = self.next_client_id
self.next_client_id += 1
return client_id
# TODO: Instead of using clients_lock, synchronize clients throught incoming_messages
# Make add_client and remove_client simply push events into the queue
def add_client(self, sock):
client_id = self._get_next_client_id()
self.broadcast(client_id, {'type': 'join'})
with self.clients_lock:
self.clients[client_id] = Client(sock, client_id)
self.send_to(None, client_id, {
'type': 'init',
'state': self.get_state_dto(client_id)
})
return client_id
def remove_client(self, client_id):
with self.clients_lock:
del self.clients[client_id]
self.broadcast(client_id, {'type': 'leave'})
# event thread stuff
def start_event_thread(self):
event_thread = Thread(target=lambda: self._event_thread())
event_thread.daemon = True
event_thread.start()
def _event_thread(self):
while True:
msg = self.incoming_messages.get()
self.handle_message_from(msg['from'], msg['payload'])
# Initialization
state = GlebbyState()
state.start_event_thread()
# TODO: Examine Quart (basically an asyncio version of flask)
app = Flask(__name__, static_url_path='')
sock = Sock(app)
@app.route('/')
def index():
return app.send_static_file('index.html')
@sock.route('/glebby')
def echo(sock):
client_id = state.add_client(sock)
try:
while True:
data = sock.receive()
state.put_incoming_message(client_id, data)
except simple_websocket.ConnectionClosed:
state.remove_client(client_id)

View File

@ -0,0 +1,40 @@
import asyncio
from quart import Quart, websocket
from glebby.model import Model
model = Model()
app = Quart(__name__, static_url_path='')
async def send_outgoing(queue):
while True:
data = await queue.get()
await websocket.send(data)
@app.route('/')
async def root():
return await app.send_static_file('index.html')
@app.websocket('/glebby')
async def ws():
global model
# For each connected client, we create a queue to store its outgoing messages.
# We pass this queue to the model so that it can communicate with the clients.
outgoing_queue = asyncio.Queue()
client_id = await model.add_client(outgoing_queue)
try:
# In order to get the queued messages through the websocket, we need to create a task
# since the current one will be busy receiving incoming messages below.
outgoing_task = asyncio.create_task(send_outgoing(outgoing_queue))
while True:
data = await websocket.receive()
await model.handle_incoming(client_id, data)
# When a client disconnects, we shut down the message pumping task and wait for it to finish
except asyncio.CancelledError:
await model.remove_client(client_id)
outgoing_task.cancel()
await outgoing_task

View File

@ -0,0 +1,28 @@
from random import Random
class BoardGenerator:
def __init__(self, seed):
self.dice = [
'aeaneg',
'wngeeh',
'ahspco',
'lnhnrz',
'aspffk',
'tstiyd',
'objoab',
'owtoat',
'iotmuc',
'erttyl',
'ryvdel',
'toessi',
'lreixd',
'terwhv',
'eiunes',
'nuihmq'
]
self.rng = Random(seed)
def generate_board(self):
shuffled_dice = self.rng.sample(self.dice, k=len(self.dice))
roll_results = [self.rng.choice(die) for die in shuffled_dice]
return [roll_results[4 * i : 4 * (i+1)] for i in range(0, 4)]

View File

@ -0,0 +1,86 @@
import itertools
import json
from collections import OrderedDict
from glebby.board import BoardGenerator
class Client:
def __init__(self, client_id, outgoing_queue):
self.outgoing_queue = outgoing_queue
self.data = { 'id': client_id, 'name': '' }
async def put_outgoing_message(self, message):
await self.outgoing_queue.put(message)
class Model:
def __init__(self):
self.clients = OrderedDict()
self.id_generator = itertools.count(start=0, step=1)
self.board_generator = BoardGenerator(None)
self.board = None
async def add_client(self, outgoing_queue):
client_id = next(self.id_generator)
await self.broadcast(client_id, { 'type': 'join' })
self.clients[client_id] = Client(client_id, outgoing_queue)
print(f'<{client_id}|join>')
await self.send_to(None, client_id, {
'type': 'init',
'state': self.get_state_dto(client_id)
})
return client_id
async def remove_client(self, client_id):
del self.clients[client_id]
await self.broadcast(client_id, { 'type': 'leave' })
print(f'<{client_id}|leave>')
async def send_to(self, client_id_from, client_id_to, payload):
await self.clients[client_id_to].put_outgoing_message(json.dumps({
'from': client_id_from,
'payload': payload
}))
async def broadcast(self, client_id_from, payload):
for client_id_to in self.clients:
await self.send_to(client_id_from, client_id_to, payload)
async def handle_incoming(self, client_id, message):
print(f'<{client_id}|data> {message}')
payload = json.loads(message)
if payload['type'] == 'set-name':
self.clients[client_id].data['name'] = payload['name']
await self.broadcast(client_id, {
'type': 'set-name',
'name': payload['name']
})
elif payload['type'] == 'chat':
await self.broadcast(client_id, {
'type': 'chat',
'message': payload['message']
})
elif payload['type'] == 'roll':
self.board = self.board_generator.generate_board()
await self.broadcast(client_id, {
'type': 'roll',
'board': self.board
})
else:
print("Unhandled!")
def get_state_dto(self, client_id):
return {
'yourId': client_id,
'players': [
client.data
for client in self.clients.values()
],
'board': self.board
}

View File

@ -1,12 +1,18 @@
aiofiles==22.1.0
blinker==1.5
click==8.1.3
Flask==2.2.2
flask-sock==0.6.0
h11==0.14.0
h2==4.1.0
hpack==4.0.0
hypercorn==0.14.3
hyperframe==6.0.1
importlib-metadata==6.0.0
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.1
simple-websocket==0.9.0
MarkupSafe==2.1.2
priority==2.0.0
quart==0.18.3
toml==0.10.2
Werkzeug==2.2.2
wsproto==1.2.0
zipp==3.11.0

4
glebby-server/run.py Normal file
View File

@ -0,0 +1,4 @@
import glebby
# For development use only
glebby.app.run()