Compare commits
3 Commits
49cebb9f0a
...
9cd3803b61
Author | SHA1 | Date | |
---|---|---|---|
9cd3803b61 | |||
09d3d8684c | |||
d9eb2d7b05 |
@ -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"]
|
||||
|
3
TODO.md
3
TODO.md
@ -1,5 +1,6 @@
|
||||
- [ ] composition api
|
||||
- [ ] quart
|
||||
- [x] quart
|
||||
- [x] fix dockerfile
|
||||
- [ ] lobby
|
||||
- [ ] actual game logic
|
||||
- [ ] websocket url config
|
||||
|
@ -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>
|
||||
|
@ -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)
|
40
glebby-server/glebby/__init__.py
Normal file
40
glebby-server/glebby/__init__.py
Normal 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
|
28
glebby-server/glebby/board.py
Normal file
28
glebby-server/glebby/board.py
Normal 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)]
|
86
glebby-server/glebby/model.py
Normal file
86
glebby-server/glebby/model.py
Normal 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
|
||||
}
|
||||
|
@ -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
4
glebby-server/run.py
Normal file
@ -0,0 +1,4 @@
|
||||
import glebby
|
||||
|
||||
# For development use only
|
||||
glebby.app.run()
|
Loading…
x
Reference in New Issue
Block a user