Add React frontend and clean up legacy HTTP backend
This commit is contained in:
+28
-21
@@ -160,6 +160,19 @@ async def send_error(sid: str, message: str):
|
||||
await sio.emit("error", {"error": message}, to=sid)
|
||||
|
||||
|
||||
async def _mark_player_offline(game: "Game", player: "Player"):
|
||||
"""Mark player disconnected, delete the game if everyone left, else notify the room."""
|
||||
player.connected = False
|
||||
if not any(p.connected for p in game.players):
|
||||
del games[game.gid]
|
||||
else:
|
||||
await sio.emit(
|
||||
"player_connection",
|
||||
{"order": player.order, "connected": False},
|
||||
room=game.gid,
|
||||
)
|
||||
|
||||
|
||||
def _active_game(sid: str) -> "tuple[Game, dict] | None":
|
||||
"""Resolve the started game and seat for a connection, or None."""
|
||||
sess = sessions.get(sid)
|
||||
@@ -181,20 +194,12 @@ async def connect(sid, environ, auth=None):
|
||||
|
||||
@sio.event
|
||||
async def disconnect(sid):
|
||||
sessions.pop(sid, None)
|
||||
game = next((g for g in games.values() if g.player_by_sid(sid)), None)
|
||||
sess = sessions.pop(sid, None)
|
||||
game = games.get(sess["gid"]) if sess else None
|
||||
if game is not None:
|
||||
player = game.player_by_sid(sid)
|
||||
player.connected = False
|
||||
if not any(p.connected for p in game.players):
|
||||
# Everybody left — drop the game so it can't leak memory forever.
|
||||
del games[game.gid]
|
||||
else:
|
||||
await sio.emit(
|
||||
"player_connection",
|
||||
{"order": player.order, "connected": False},
|
||||
room=game.gid,
|
||||
)
|
||||
if player is not None:
|
||||
await _mark_player_offline(game, player)
|
||||
await broadcast_lobby()
|
||||
|
||||
|
||||
@@ -256,14 +261,7 @@ async def leave_game(sid):
|
||||
# Game in progress: keep the seat (reconnect via token still works),
|
||||
# just mark the player offline.
|
||||
if player is not None:
|
||||
player.connected = False
|
||||
await sio.emit(
|
||||
"player_connection",
|
||||
{"order": player.order, "connected": False},
|
||||
room=game.gid,
|
||||
)
|
||||
if not any(p.connected for p in game.players):
|
||||
del games[game.gid]
|
||||
await _mark_player_offline(game, player)
|
||||
else:
|
||||
# Not started yet: free the seat entirely.
|
||||
if player is not None:
|
||||
@@ -275,6 +273,11 @@ async def leave_game(sid):
|
||||
|
||||
@sio.on("start_game")
|
||||
async def start_game(sid, gid):
|
||||
sess = sessions.get(sid)
|
||||
if sess is None or sess["gid"] != gid:
|
||||
return await send_error(sid, "Nie ste v tejto hre.")
|
||||
if sess["order"] != 0:
|
||||
return await send_error(sid, "Iba hostitel moze spustit hru.")
|
||||
game = games.get(gid)
|
||||
if game is None:
|
||||
return await send_error(sid, "Hra neexistuje.")
|
||||
@@ -299,6 +302,9 @@ async def reconnect_to_game(sid, gid, token):
|
||||
if player is None:
|
||||
return await send_error(sid, "Neplatny token pre pripojenie.")
|
||||
|
||||
old_sid = player.sid
|
||||
if old_sid != sid:
|
||||
sessions.pop(old_sid, None)
|
||||
player.sid = sid
|
||||
player.connected = True
|
||||
sessions[sid] = {"gid": gid, "order": player.order}
|
||||
@@ -371,4 +377,5 @@ async def play_card(sid, card_key):
|
||||
except BridzikException as exc:
|
||||
return await send_error(sid, str(exc))
|
||||
await send_game_status(game.gid)
|
||||
await send_player_cards(game.gid, sess["order"], sid)
|
||||
for player in game.players:
|
||||
await send_player_cards(game.gid, player.order, player.sid)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField, IntegerField, RadioField, StringField
|
||||
from wtforms.validators import DataRequired, NumberRange
|
||||
|
||||
class GuessForm(FlaskForm):
|
||||
guess = IntegerField('Tip', validators=[DataRequired(message='Zadaj tip'), NumberRange(min=0, max=8)])
|
||||
submit = SubmitField('Zadaj tip')
|
||||
|
||||
def __init__(self, max_guess: int = 8, *args, **kwargs):
|
||||
super(GuessForm, self).__init__(*args, **kwargs)
|
||||
self.max_guess = max_guess
|
||||
|
||||
class PlayForm(FlaskForm):
|
||||
card = RadioField('Vyber kartu', validators=[DataRequired(message='Musíš vybrať kartu')])
|
||||
submit = SubmitField('Zahraj')
|
||||
|
||||
class AdminForm(FlaskForm):
|
||||
player0 = StringField('0', validators=[DataRequired()])
|
||||
player1 = StringField('1', validators=[DataRequired()])
|
||||
player2 = StringField('2', validators=[DataRequired()])
|
||||
player3 = StringField('3', validators=[DataRequired()])
|
||||
submit = SubmitField()
|
||||
@@ -1,85 +0,0 @@
|
||||
from api import app, bridzikInstance
|
||||
from bridzik import Card, Card_colors, Card_values, BridzikException
|
||||
import json
|
||||
from flask import render_template, url_for, flash, redirect
|
||||
from api.forms import GuessForm, PlayForm, AdminForm
|
||||
from api.utils import get_points_sums, sort_card_list
|
||||
|
||||
players = [
|
||||
'Jakub',
|
||||
'Timo',
|
||||
'Katka',
|
||||
'Ondrej'
|
||||
]
|
||||
|
||||
@app.route('/bridzik_api/get_status/<id>')
|
||||
def get_status(id: int):
|
||||
return json.dumps(bridzikInstance.get_status(int(id)), cls=Card.JSONEncoder)
|
||||
|
||||
@app.route('/bridzik/<player>/status')
|
||||
def status(player):
|
||||
player = int(player)
|
||||
game_status = bridzikInstance.get_status(player)
|
||||
action = None
|
||||
form = None
|
||||
player_cards = sort_card_list(bridzikInstance.series[-1].get_last_round().player_cards[player])
|
||||
game_status['player_cards'] = [str(c) for c in player_cards]
|
||||
points_sums = get_points_sums(game_status['standings'])
|
||||
if bridzikInstance.is_completed() or bridzikInstance.series[-1].get_last_round().get_active_player() != player:
|
||||
pass
|
||||
elif not bridzikInstance.series[-1].get_last_round().is_guessing_completed():
|
||||
form = GuessForm(max_guess= 8 - bridzikInstance.series[-1].get_last_round().round_number)
|
||||
action = 'guess'
|
||||
else:
|
||||
form = PlayForm()
|
||||
form.card.choices = [(str(c), str(c)) for c in player_cards]
|
||||
action = 'play'
|
||||
return render_template(
|
||||
'status.html', status=game_status, player=player, action=action,
|
||||
form=form, players=players, points_sums=points_sums
|
||||
)
|
||||
|
||||
@app.route('/bridzik/<player>/guess', methods=['POST'])
|
||||
def guess(player):
|
||||
player = int(player)
|
||||
form = GuessForm()
|
||||
try:
|
||||
bridzikInstance.add_player_guess(player, int(form.guess.data))
|
||||
except BridzikException:
|
||||
flash('Nie je možné zadať tip.')
|
||||
return redirect(url_for('status', player=player))
|
||||
|
||||
@app.route('/bridzik/<player>/play_card', methods=['POST'])
|
||||
def play_card(player):
|
||||
player = int(player)
|
||||
player_cards = bridzikInstance.series[-1].get_last_round().player_cards[player]
|
||||
form = PlayForm()
|
||||
form.card.choices = [(str(c), str(c)) for c in player_cards]
|
||||
color, value = form.card.data.split('_')
|
||||
try:
|
||||
card = Card(Card_colors[color], Card_values[value])
|
||||
except KeyError:
|
||||
flash('Chyba. Opakuj pokus znovu.')
|
||||
|
||||
try:
|
||||
bridzikInstance.play_card(player, card)
|
||||
except BridzikException:
|
||||
flash('Nie je možné zahrať kartu.')
|
||||
return redirect(url_for('status', player=player))
|
||||
|
||||
|
||||
@app.route('/bridzik/admin', methods=['GET', 'POST'])
|
||||
def admin():
|
||||
form = AdminForm()
|
||||
if form.validate_on_submit():
|
||||
players[0] = form.player0.data
|
||||
players[1] = form.player1.data
|
||||
players[2] = form.player2.data
|
||||
players[3] = form.player3.data
|
||||
return redirect(url_for('admin'))
|
||||
else:
|
||||
form.player0.data = players[0]
|
||||
form.player1.data = players[1]
|
||||
form.player2.data = players[2]
|
||||
form.player3.data = players[3]
|
||||
return render_template('admin.html', form=form)
|
||||
@@ -1,51 +0,0 @@
|
||||
#header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#header>div {
|
||||
padding: 10px;
|
||||
border: 2px solid grey;
|
||||
}
|
||||
|
||||
.state_row_header {
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.points {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 250px;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 10px;
|
||||
}
|
||||
|
||||
.card_display {
|
||||
width: 82px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
#points_summary_row {
|
||||
border-top: 4px double black;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.active_player_highlight {
|
||||
background: grey;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.player_name_highlight {
|
||||
background: grey;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.standings_round_final_row {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<form action="{{ url_for('guess', player=player) }}" method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
{{ form.guess.label }}:
|
||||
{{ form.guess(size=15, autocomplete="off") }}
|
||||
{% for error in form.guess.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
{{ form.submit() }}
|
||||
</p>
|
||||
</form>
|
||||
@@ -1,39 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Bridžik admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="" method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
{{ form.player0.label }}:
|
||||
{{ form.player0(size=15) }}
|
||||
{% for error in form.player0.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.player1.label }}:
|
||||
{{ form.player1(size=15) }}
|
||||
{% for error in form.player1.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.player2.label }}:
|
||||
{{ form.player2(size=15) }}
|
||||
{% for error in form.player2.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.player3.label }}:
|
||||
{{ form.player3(size=15) }}
|
||||
{% for error in form.player3.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,176 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Bridžik</title>
|
||||
<link href="/static/style.css" rel="stylesheet">
|
||||
{% if not action%}
|
||||
<meta http-equiv="refresh" content="2">
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul>
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<hr>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if 'active_player' not in status %}
|
||||
<h1>Hra sa skončila.</h1>
|
||||
{% else %}
|
||||
<div id="wrapper">
|
||||
<div id="table">
|
||||
<div id="header">
|
||||
<div {% if player == status['active_player'] %}class="active_player_highlight"{% endif %}>
|
||||
<p id="active_player" >Na ťahu je: {{ players[status['active_player']] }}</p>
|
||||
</div>
|
||||
|
||||
<div id="active_round_guesses">
|
||||
<p></p>
|
||||
<table style="width: auto;">
|
||||
<thead>
|
||||
<th></th>
|
||||
<th class="state_column_header">{{ players[0] }}</th>
|
||||
<th class="state_column_header">{{ players[1] }}</th>
|
||||
<th class="state_column_header">{{ players[2] }}</th>
|
||||
<th class="state_column_header">{{ players[3] }}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="state_row_header">Tipy v tomto kole:</th>
|
||||
{% for player in range(4) %}
|
||||
<td class="points">{{ status['active_round_guesses'][player] }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
|
||||
{% if status['active_round_stashes'] %}
|
||||
<tr>
|
||||
<th class="state_row_header">Kôpky v tomto kole:</th>
|
||||
{% for player in status['active_round_stashes'] %}
|
||||
<td class="points">{{ player }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
{% if action == 'guess' %}
|
||||
<h1>Zadaj tip:</h1>
|
||||
{% include "_guess_form.html" %}
|
||||
{% endif %}
|
||||
|
||||
{% if status['active_stash'] %}
|
||||
<h1>Aktuálna kôpka:</h1>
|
||||
<table style="width: auto;">
|
||||
{% for player in range(status['active_stash']['first_player'], status['active_stash']['first_player'] + 4) %}
|
||||
<th>{{ players[player % 4] }}</th>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
{% for player in range(status['active_stash']['first_player'], status['active_stash']['first_player'] + 4) %}
|
||||
<td class="card_display">
|
||||
{% if status['active_stash']['cards'][player % 4] %}
|
||||
<img src="/static/cards/{{ status['active_stash']['cards'][player % 4] }}.png" width="80" height="auto">
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
{% if action != 'play' %}
|
||||
<h1>Moje karty:</h1>
|
||||
<table id="player_hand">
|
||||
{% for card in status['player_cards'] %}
|
||||
<td>
|
||||
<img src="/static/cards/{{ card }}.png" width="80" height="auto">
|
||||
</td>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% else %}
|
||||
<h1>{{ form.card.label }}:</h1>
|
||||
<form action="{{ url_for('play_card', player=player) }}" method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
<table style="width: auto">
|
||||
{% for card in status['player_cards'] %}
|
||||
<td>
|
||||
<img src="/static/cards/{{ card }}.png" width="80" height="auto">
|
||||
</td>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
{% for card in status['player_cards'] %}
|
||||
<td>
|
||||
<input id="card-{{ loop.index }}" name="card" type="radio" value="{{ card }}">
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</table>
|
||||
{% for error in form.card.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
{{ form.submit() }}
|
||||
</p>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if status['previous_stash'] %}
|
||||
<h1>Predchádzajúca kôpka:</h1>
|
||||
<table style="width: auto;">
|
||||
{% for player in range(status['previous_stash']['first_player'], status['previous_stash']['first_player'] + 4) %}
|
||||
<th>{{ players[player % 4] }}</th>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
{% for player in range(status['previous_stash']['first_player'], status['previous_stash']['first_player'] + 4) %}
|
||||
<td class="card_display">
|
||||
{% if status['previous_stash']['cards'][player % 4] %}
|
||||
<img src="/static/cards/{{ status['previous_stash']['cards'][player % 4] }}.png" width="80" height="auto">
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<div id="standings">
|
||||
<h1>Výsledky</h1>
|
||||
<table style="width: auto;">
|
||||
<tr>
|
||||
<th>{{ players[0] }}</th>
|
||||
<th>{{ players[1] }}</th>
|
||||
<th>{{ players[2] }}</th>
|
||||
<th>{{ players[3] }}</th>
|
||||
</tr>
|
||||
{% for series in status['standings'] %}
|
||||
{% if series %}
|
||||
{% for round in series %}
|
||||
{% if round %}
|
||||
<tr {% if loop.index == 7 %}class="standings_round_final_row"{% endif %} >
|
||||
{% for player in round %}
|
||||
<td class="points">{{ player }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<tr id="points_summary_row">
|
||||
{% for player in points_sums %}
|
||||
<td class="points">{{ player }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
from bridzik import Card_colors
|
||||
|
||||
def get_points_sums(standings: []):
|
||||
sums = [0] * 4
|
||||
for series in standings:
|
||||
for round in series:
|
||||
for player, points in enumerate(round):
|
||||
sums[player] += points
|
||||
return sums
|
||||
|
||||
def sort_card_list(input_card_set: []) -> []:
|
||||
color_paritions = [
|
||||
[c for c in input_card_set if c.color == Card_colors['HEARTS']],
|
||||
[c for c in input_card_set if c.color == Card_colors['LEAVES']],
|
||||
[c for c in input_card_set if c.color == Card_colors['ACORNS']],
|
||||
[c for c in input_card_set if c.color == Card_colors['BELLS']]
|
||||
|
||||
]
|
||||
output_list = []
|
||||
for color_list in color_paritions:
|
||||
color_list.sort(key=lambda a : a.value)
|
||||
output_list.extend(color_list)
|
||||
return output_list
|
||||
Reference in New Issue
Block a user