glebby/glebby-server/glebby.py

172 lines
4.9 KiB
Python

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)