Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { analysisView, embedView } from './analysis';
import { puzzleView } from './puzzle';
import { profileView } from './profile';
import { tournamentView } from './tournament';
import { simulView } from './simul/simul';
import { calendarView } from './calendar';
import { pasteView } from './paste';
import { statsView } from './stats';
Expand Down Expand Up @@ -69,6 +70,7 @@ function initModel(el: HTMLElement) {
gameId : el.getAttribute("data-gameid") ?? "",
tournamentId : el.getAttribute("data-tournamentid") ?? "",
tournamentname : el.getAttribute("data-tournamentname") ?? "",
simulId : el.getAttribute("data-simulid") ?? "",
inviter : el.getAttribute("data-inviter") ?? "",
ply : parseInt(""+el.getAttribute("data-ply")),
initialFen : el.getAttribute("data-initialfen") ?? "",
Expand Down Expand Up @@ -151,6 +153,8 @@ export function view(el: HTMLElement, model: PyChessModel): VNode {
return h('div#main-wrap', editorView(model));
case 'tournament':
return h('div#main-wrap', [h('main.tour', tournamentView(model))]);
case 'simul':
return h('div#main-wrap', [h('main.simul', simulView(model))]);
case 'calendar':
return h('div#calendar', calendarView());
case 'games':
Expand Down
227 changes: 227 additions & 0 deletions client/simul/simul.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { h, VNode } from 'snabbdom';
import { Chessground } from 'chessgroundx';
import { Api } from "chessgroundx/api";
import { Variant, VARIANTS } from '../variants';

Check failure on line 4 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

'Variant' is declared but its value is never read.

Check failure on line 4 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

'Variant' is declared but its value is never read.

import { PyChessModel } from '../types';
import { _ } from '../i18n';
import { patch } from '../document';
import { chatMessage, chatView, ChatController } from '../chat';

Check failure on line 9 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

'chatMessage' is declared but its value is never read.

Check failure on line 9 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

'chatMessage' is declared but its value is never read.
import { newWebsocket } from "@/socket/webSocketUtils";

interface SimulPlayer {
name: string;
rating: number;
title: string;
}

interface SimulGame {
gameId: string;
wplayer: string;
bplayer: string;
variant: string;
fen: string;
rated: boolean;
base: number;
inc: number;
byo: number;
}

interface MsgSimulUserConnected {
type: string;
simulId: string;
players: SimulPlayer[];
pendingPlayers: SimulPlayer[];
createdBy: string;
}

export class SimulController implements ChatController {

Check failure on line 38 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Class 'SimulController' incorrectly implements interface 'ChatController'.

Check failure on line 38 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Class 'SimulController' incorrectly implements interface 'ChatController'.
sock;
simulId: string;
players: SimulPlayer[] = [];
pendingPlayers: SimulPlayer[] = [];
createdBy: string;
model: PyChessModel;
games: SimulGame[] = [];
activeGameId: string | null = null;
chessgrounds: { [gameId: string]: Api } = {};

constructor(el: HTMLElement, model: PyChessModel) {
console.log("SimulController constructor", el, model);
this.simulId = model["simulId"];

Check failure on line 51 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Element implicitly has an 'any' type because expression of type '"simulId"' can't be used to index type 'PyChessModel'.

Check failure on line 51 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Element implicitly has an 'any' type because expression of type '"simulId"' can't be used to index type 'PyChessModel'.
this.model = model;
this.players = model["players"] || [];

Check failure on line 53 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Property 'players' does not exist on type 'PyChessModel'. Did you mean 'wplayer'?

Check failure on line 53 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Property 'players' does not exist on type 'PyChessModel'. Did you mean 'wplayer'?
this.pendingPlayers = model["pendingPlayers"] || [];

Check failure on line 54 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Element implicitly has an 'any' type because expression of type '"pendingPlayers"' can't be used to index type 'PyChessModel'.

Check failure on line 54 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Element implicitly has an 'any' type because expression of type '"pendingPlayers"' can't be used to index type 'PyChessModel'.
this.createdBy = model["createdBy"] || "";

Check failure on line 55 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Element implicitly has an 'any' type because expression of type '"createdBy"' can't be used to index type 'PyChessModel'.

Check failure on line 55 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Element implicitly has an 'any' type because expression of type '"createdBy"' can't be used to index type 'PyChessModel'.

const onOpen = () => {
this.doSend({ type: "simul_user_connected", username: model["username"], simulId: this.simulId });
}

this.sock = newWebsocket('wss');
this.sock.onopen = () => onOpen();
this.sock.onmessage = (e: MessageEvent) => this.onMessage(e);

this.redraw();
patch(document.getElementById('lobbychat') as HTMLElement, chatView(this, "lobbychat"));

Check failure on line 66 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Argument of type 'this' is not assignable to parameter of type 'ChatController'.

Check failure on line 66 in client/simul/simul.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Argument of type 'this' is not assignable to parameter of type 'ChatController'.
}

doSend(message: object) {
this.sock.send(JSON.stringify(message));
}

onMessage(evt: MessageEvent) {
console.log("<+++ simul onMessage():", evt.data);
if (evt.data === '/n') return;
const msg = JSON.parse(evt.data);
switch (msg.type) {
case "simul_user_connected":
this.onMsgSimulUserConnected(msg);
break;
case "new_game":
this.onMsgNewGame(msg);
break;
case "player_joined":
this.onMsgPlayerJoined(msg);
break;
case "player_approved":
this.onMsgPlayerApproved(msg);
break;
case "player_denied":
this.onMsgPlayerDenied(msg);
break;
}
}

onMsgSimulUserConnected(msg: MsgSimulUserConnected) {
this.players = msg.players;
this.pendingPlayers = msg.pendingPlayers;
this.createdBy = msg.createdBy;
this.redraw();
}

onMsgNewGame(msg: SimulGame) {
this.games.push(msg);
if (this.activeGameId === null) {
this.activeGameId = msg.gameId;
}
this.redraw();
}

onMsgPlayerJoined(msg: { player: SimulPlayer }) {
this.pendingPlayers.push(msg.player);
this.redraw();
}

onMsgPlayerApproved(msg: { username: string }) {
const player = this.pendingPlayers.find(p => p.name === msg.username);
if (player) {
this.pendingPlayers = this.pendingPlayers.filter(p => p.name !== msg.username);
this.players.push(player);
this.redraw();
}
}

onMsgPlayerDenied(msg: { username: string }) {
this.pendingPlayers = this.pendingPlayers.filter(p => p.name !== msg.username);
this.players = this.players.filter(p => p.name !== msg.username);
this.redraw();
}

setActiveGame(gameId: string) {
this.activeGameId = gameId;
this.redraw();
}

redraw() {
patch(document.getElementById('simul-view') as HTMLElement, this.render());
}

approve(username: string) {
this.doSend({ type: "approve_player", simulId: this.simulId, username: username });
}

deny(username: string) {
this.doSend({ type: "deny_player", simulId: this.simulId, username: username });
}

startSimul() {
this.doSend({ type: "start_simul", simulId: this.simulId });
}

joinSimul() {
this.doSend({ type: "join", simulId: this.simulId });
}

render() {
const isHost = this.model.username === this.createdBy;

const startButton = isHost
? h('button', { on: { click: () => this.startSimul() } }, 'Start Simul')
: h('div');

const joinButton = (!isHost && !this.players.find(p => p.name === this.model.username) && !this.pendingPlayers.find(p => p.name === this.model.username))
? h('button', { on: { click: () => this.joinSimul() } }, 'Join Simul')
: h('div');

return h('div#simul-view', [
h('div.simul-sidebar', [
h('h1', 'Simul Lobby'),
startButton,
joinButton,
h('div.players-grid', [
h('div.pending-players', [
h('h2', 'Pending Players'),
h('ul', this.pendingPlayers.map(p => h('li', [
`${p.title} ${p.name} (${p.rating})`,
isHost ? h('button', { on: { click: () => this.approve(p.name) } }, '✓') : null,
isHost ? h('button', { on: { click: () => this.deny(p.name) } }, 'X') : null,
]))),
]),
h('div.approved-players', [
h('h2', 'Approved Players'),
h('ul', this.players.map(p => h('li', [
`${p.title} ${p.name} (${p.rating})`,
(isHost && p.name !== this.createdBy) ? h('button', { on: { click: () => this.deny(p.name) } }, 'X') : null,
]))),
]),
]),
]),
h('div.simul-main', [
this.renderMiniBoards(),
h('div#lobbychat')
])
]);
}

renderMiniBoards() {
return h('div.mini-boards', this.games.map(game => {
const variant = VARIANTS[game.variant];
return h(`div.mini-board`, {
on: { click: () => this.setActiveGame(game.gameId) },
class: { active: game.gameId === this.activeGameId }
}, [
h(`div.cg-wrap.${variant.board.cg}.mini`, {
hook: {
insert: vnode => {
const cg = Chessground(vnode.elm as HTMLElement, {
fen: game.fen,
viewOnly: true,
coordinates: false,
});
this.chessgrounds[game.gameId] = cg;
}
}
}),
]);
}));
}
}

export function simulView(model: PyChessModel): VNode[] {
return [
h('div#simul-view', { hook: { insert: vnode => new SimulController(vnode.elm as HTMLElement, model) } }, [
// initial content, will be replaced by redraw
])
];
}
2 changes: 2 additions & 0 deletions server/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class TPairing(IntEnum):
ARENA = 0
RR = 1
SWISS = 2
SIMUL = 3


# translations
Expand Down Expand Up @@ -274,6 +275,7 @@ def _(message):
0: _("Arena"),
1: _("Round-Robin"),
2: _("Swiss"),
3: _("Simul"),
}

TRANSLATED_FREQUENCY_NAMES = {
Expand Down
4 changes: 4 additions & 0 deletions server/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def __init__(
corr=False,
create=True,
tournamentId=None,
simulId=None,
new_960_fen_needed_for_rematch=False,
):
self.app_state = app_state
Expand All @@ -94,6 +95,7 @@ def __init__(
self.inc = inc
self.level = level if level is not None else 0
self.tournamentId = tournamentId
self.simulId = simulId
self.chess960 = chess960
self.corr = corr
self.create = create
Expand Down Expand Up @@ -404,6 +406,8 @@ async def play_move(self, move, clocks=None, ply=None):
await self.save_game()
if self.corr:
await opp_player.notify_game_end(self)
if self.simulId is not None:
await self.app_state.simuls[self.simulId].game_update(self)
else:
await self.save_move(move)

Expand Down
2 changes: 2 additions & 0 deletions server/pychess_global_app_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
DEV,
static_url,
)
from simul.simul import Simul
from tournament.tournament import Tournament
from tournament.tournaments import (
translated_tournament_name,
Expand Down Expand Up @@ -101,6 +102,7 @@ def __init__(self, app: web.Application):
self.tourneynames: dict[str, dict] = {lang: {} for lang in LANGUAGES}

self.tournaments: dict[str, Tournament] = {}
self.simuls: dict[str, Simul] = {}

self.tourney_calendar = None

Expand Down
7 changes: 7 additions & 0 deletions server/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from wsl import lobby_socket_handler
from wsr import round_socket_handler
from tournament.wst import tournament_socket_handler
from ws_simul import simul_socket_handler
from tournament.tournament_calendar import tournament_calendar
from twitch import twitch_request_handler
from puzzle import puzzle_complete, puzzle_vote
Expand Down Expand Up @@ -85,6 +86,7 @@
videos,
video,
winners,
simul as simul_view,
)


Expand Down Expand Up @@ -132,6 +134,9 @@
(r"/tournament/{tournamentId:\w{8}}", tournament.tournament),
(r"/tournament/{tournamentId:\w{8}}/pause", tournament.tournament),
(r"/tournament/{tournamentId:\w{8}}/cancel", tournament.tournament),
("/simuls", simul_view.simuls),
("/simul/new", simul_view.simul_new),
(r"/simul/{simulId:\w{8}}", simul_view.simul),
("/@/{profileId}", profile.profile),
("/@/{profileId}/tv", tv.tv),
("/@/{profileId}/challenge", lobby.lobby),
Expand All @@ -156,6 +161,7 @@
("/wsl", lobby_socket_handler),
("/wsr/{gameId}", round_socket_handler),
("/wst", tournament_socket_handler),
("/wss", simul_socket_handler),
("/api/account", account),
("/api/account/playing", playing),
("/api/stream/event", event_stream),
Expand Down Expand Up @@ -214,6 +220,7 @@
("/import_bpgn", import_game_bpgn),
("/tournaments/arena", tournaments.tournaments),
(r"/tournament/{tournamentId:\w{8}}/edit", arena_new.arena_new), # TODO: implement
("/simuls/simul", simul_view.simuls),
("/twitch", twitch_request_handler),
(r"/puzzle/complete/{puzzleId:\w{5}}", puzzle_complete),
(r"/puzzle/vote/{puzzleId:\w{5}}", puzzle_vote),
Expand Down
Empty file added server/simul/__init__.py
Empty file.
Loading
Loading