Compare commits
	
		
			No commits in common. "9cd3803b61547dc0e269ba3ae4eead176d5e273d" and "49cebb9f0ae5ce18f85988f2c77d6adab4bc3265" have entirely different histories.
		
	
	
		
			9cd3803b61
			...
			49cebb9f0a
		
	
		
| @ -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 hypercorn | ||||
| COPY --from=frontend /glebby-client/static /glebby-server/glebby/static | ||||
| RUN pip install gunicorn | ||||
| COPY --from=frontend /glebby-client/static /glebby-server/static | ||||
| 
 | ||||
| CMD ["hypercorn", "--bind", "0.0.0.0:5000", "glebby:app"] | ||||
| CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--threads", "100", "glebby:app"] | ||||
|  | ||||
							
								
								
									
										3
									
								
								TODO.md
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								TODO.md
									
									
									
									
									
								
							| @ -1,6 +1,5 @@ | ||||
| - [ ] composition api | ||||
| - [x] quart | ||||
|     - [x] fix dockerfile | ||||
| - [ ] quart | ||||
| - [ ] lobby | ||||
| - [ ] actual game logic | ||||
| - [ ] websocket url config | ||||
|  | ||||
| @ -27,7 +27,7 @@ export default defineComponent({ | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       chatMessages: [] as { from: Player, message: string }[], | ||||
|       chatMessages: [] as { from: number, message: string }[], | ||||
| 
 | ||||
|       playerName: '', | ||||
|       chatMessage: '' | ||||
| @ -55,7 +55,7 @@ export default defineComponent({ | ||||
|           break | ||||
|         case 'chat': | ||||
|           this.chatMessages.push({ | ||||
|             from: getPlayer(this.model, message.from)!, | ||||
|             from: 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="chatMessage in chatMessages" class="chatbox-message"> | ||||
|             <code>{{ chatMessage.from.name }}#{{ chatMessage.from.id }}:</code> {{ chatMessage.message }} | ||||
|           <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> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
							
								
								
									
										171
									
								
								glebby-server/glebby.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								glebby-server/glebby.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,171 @@ | ||||
| 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) | ||||
| @ -1,40 +0,0 @@ | ||||
| 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 | ||||
| @ -1,28 +0,0 @@ | ||||
| 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)] | ||||
| @ -1,86 +0,0 @@ | ||||
| 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,18 +1,12 @@ | ||||
| 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.2 | ||||
| priority==2.0.0 | ||||
| quart==0.18.3 | ||||
| toml==0.10.2 | ||||
| MarkupSafe==2.1.1 | ||||
| simple-websocket==0.9.0 | ||||
| Werkzeug==2.2.2 | ||||
| wsproto==1.2.0 | ||||
| zipp==3.11.0 | ||||
|  | ||||
| @ -1,4 +0,0 @@ | ||||
| import glebby | ||||
| 
 | ||||
| # For development use only | ||||
| glebby.app.run() | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user