Set up simple chat server
This commit is contained in:
parent
573fac40cc
commit
28e93b1fa3
@ -1,7 +1,39 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts">
|
||||||
import HelloWorld from './components/HelloWorld.vue'
|
import Game from './components/Game.vue'
|
||||||
|
|
||||||
|
/*
|
||||||
|
Basically just opens the websocket connection to the server,
|
||||||
|
waits for the 'init' message and passes the initial state to the Game
|
||||||
|
component. Once the initial state has been received, no more event are processed.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Game
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ws: new WebSocket('ws://localhost:5000/glebby'),
|
||||||
|
model: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
const messageListener = (ev: MessageEvent) => {
|
||||||
|
const message = JSON.parse(ev.data)
|
||||||
|
const payload = message.payload
|
||||||
|
|
||||||
|
if (payload.type === 'init') {
|
||||||
|
console.log('Received initial game state:')
|
||||||
|
console.log(payload.state)
|
||||||
|
this.model = payload.state
|
||||||
|
this.ws.removeEventListener('message', messageListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.addEventListener('message', messageListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<HelloWorld msg="Vite + Vue" />
|
<Game v-if="model" :ws="ws" :model="model" />
|
||||||
</template>
|
</template>
|
||||||
|
102
glebby-client/src/components/Game.vue
Normal file
102
glebby-client/src/components/Game.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
|
||||||
|
interface Player {
|
||||||
|
id: number,
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Model {
|
||||||
|
yourId: number,
|
||||||
|
players: Player[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlayer(model: Model, id: number): Player | undefined {
|
||||||
|
return model.players.find(player => player.id === id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Handles all game business. Sends (local) player input to the server for distribution.
|
||||||
|
Receives player input from the server and manages state accordingly.
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
ws: { type: WebSocket, required: true },
|
||||||
|
model: { type: Object as PropType<Model>, required: true }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
chatMessages: [] as string[],
|
||||||
|
|
||||||
|
playerName: '',
|
||||||
|
chatMessage: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.ws.addEventListener('message', (ev) => {
|
||||||
|
const message = JSON.parse(ev.data)
|
||||||
|
const payload = message.payload
|
||||||
|
console.log(`Message from player ${message.from}:`)
|
||||||
|
console.log(payload)
|
||||||
|
|
||||||
|
switch (payload.type) {
|
||||||
|
case 'join':
|
||||||
|
this.model.players.push({
|
||||||
|
id: message.from,
|
||||||
|
name: ''
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'leave':
|
||||||
|
this.model.players = this.model.players.filter(player => player.id !== message.from)
|
||||||
|
break
|
||||||
|
case 'chat':
|
||||||
|
this.chatMessages.push(payload.message)
|
||||||
|
break
|
||||||
|
case 'set-name':
|
||||||
|
getPlayer(this.model, message.from)!.name = payload.name
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setName() {
|
||||||
|
this.ws.send(JSON.stringify({
|
||||||
|
type: 'set-name',
|
||||||
|
name: this.playerName
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
sendChat() {
|
||||||
|
if (this.chatMessage === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.send(JSON.stringify({
|
||||||
|
type: 'chat',
|
||||||
|
message: this.chatMessage
|
||||||
|
}))
|
||||||
|
this.chatMessage = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<pre>{{ JSON.stringify(model, null, 2) }}</pre>
|
||||||
|
<h1>I am #{{ model.yourId }}</h1>
|
||||||
|
<form @submit.prevent="setName">
|
||||||
|
<input v-model="playerName" placeholder="enter your name...">
|
||||||
|
<button >set name</button>
|
||||||
|
</form>
|
||||||
|
<form @submit.prevent="sendChat">
|
||||||
|
<input v-model="chatMessage">
|
||||||
|
<button>Post</button>
|
||||||
|
</form>
|
||||||
|
<h2>players</h2>
|
||||||
|
<ul v-if="model">
|
||||||
|
<li v-for="player in model.players" :key="player.id">{{ player.name }}#{{ player.id }}</li>
|
||||||
|
</ul>
|
||||||
|
<h2>chat</h2>
|
||||||
|
<ul v-if="model">
|
||||||
|
<li v-for="message in chatMessages">{{ message }}</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
@ -1,15 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
defineProps<{ msg: string }>()
|
|
||||||
|
|
||||||
const count = ref(0)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<button type="button" @click="count++">count is {{ count }}</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@ -1,16 +1,123 @@
|
|||||||
from flask import Flask, render_template
|
import json
|
||||||
from flask_sock import Sock
|
|
||||||
import simple_websocket
|
import simple_websocket
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_sock import Sock
|
||||||
|
from queue import Queue
|
||||||
|
from threading import Thread, Lock
|
||||||
|
|
||||||
|
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()
|
||||||
|
self.clients = dict()
|
||||||
|
|
||||||
|
self.next_client_id_lock = Lock()
|
||||||
|
self.next_client_id = 0
|
||||||
|
|
||||||
|
self.incoming_messages = Queue()
|
||||||
|
|
||||||
|
# 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']
|
||||||
|
})
|
||||||
|
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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
sock = Sock(app)
|
sock = Sock(app)
|
||||||
|
|
||||||
@sock.route('/echo')
|
@sock.route('/glebby')
|
||||||
def echo(sock):
|
def echo(sock):
|
||||||
print('New echo client')
|
client_id = state.add_client(sock)
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
data = sock.receive()
|
data = sock.receive()
|
||||||
sock.send(data)
|
state.put_incoming_message(client_id, data)
|
||||||
except simple_websocket.ConnectionClosed:
|
except simple_websocket.ConnectionClosed:
|
||||||
print("Client closed the connection")
|
state.remove_client(client_id)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user