Rework sync procedure and add a loading indicator

The server will now send a sync message when the client connects via WebSocket. This removes the need to send another GET request to sync game data.
This commit is contained in:
Andrio Celos 2022-10-25 12:08:18 +11:00
parent 3ad1877a4c
commit d4e8d86571
14 changed files with 552 additions and 384 deletions

View File

@ -29,7 +29,7 @@
<form id="preGameForm">
<p><label for="nameBox">Choose a nickname: <input type="text" id="nameBox" required minlength="1" maxlength="20"/></label></p>
<div id="preGameDefaultSection">
<input type="submit" id="preGameImplicitSubmitButton" tabindex="-1"/>
<button type="submit" id="preGameImplicitSubmitButton" tabindex="-1"></button>
<p>
<button type="submit" id="newGameButton">Create a room</button>
<label for="maxPlayersBox">
@ -48,6 +48,9 @@
<p><button type="submit" id="joinGameButton2">Join game</button></p>
<a id="preGameBackButton" href="../..">Create or join a different room</a>
</div>
<div id="preGameLoadingSection" class="loadingContainer" hidden>
<div class="lds-ripple"><div></div><div></div></div> <span id="preGameLoadingLabel"></span>
</div>
<p>
<a id="preGameDeckEditorButton" href="deckeditor">Edit decks</a>
</p>

View File

@ -1,31 +1,36 @@
const cardDatabase = {
cards: null as Card[] | null,
loadAsync() {
return new Promise<Card[]>((resolve, reject) => {
if (cardDatabase.cards != null) {
resolve(cardDatabase.cards);
return;
}
const cardListRequest = new XMLHttpRequest();
cardListRequest.open('GET', `${config.apiBaseUrl}/cards`);
cardListRequest.addEventListener('load', e => {
const cards = [ ];
if (cardListRequest.status == 200) {
const s = cardListRequest.responseText;
const response = JSON.parse(s) as object[];
for (const o of response) {
cards.push(Card.fromJson(o));
}
cardDatabase.cards = cards;
resolve(cards);
} else {
reject(new Error(`Error downloading card database: response was ${cardListRequest.status}`));
}
});
cardListRequest.addEventListener('error', e => {
reject(new Error('Error downloading card database: no further information.'))
});
cardListRequest.send();
});
}
}
const cardDatabase = {
cards: null as Card[] | null,
get(number: number) {
if (cardDatabase.cards == null) throw new Error('Card database not loaded');
if (number <= 0 || number > cardDatabase.cards.length) throw new RangeError(`No card with number ${number}`);
return cardDatabase.cards[number - 1];
},
loadAsync() {
return new Promise<Card[]>((resolve, reject) => {
if (cardDatabase.cards != null) {
resolve(cardDatabase.cards);
return;
}
const cardListRequest = new XMLHttpRequest();
cardListRequest.open('GET', `${config.apiBaseUrl}/cards`);
cardListRequest.addEventListener('load', e => {
const cards = [ ];
if (cardListRequest.status == 200) {
const s = cardListRequest.responseText;
const response = JSON.parse(s) as object[];
for (const o of response) {
cards.push(Card.fromJson(o));
}
cardDatabase.cards = cards;
resolve(cards);
} else {
reject(new Error(`Error downloading card database: response was ${cardListRequest.status}`));
}
});
cardListRequest.addEventListener('error', e => {
reject(new Error('Error downloading card database: no further information.'))
});
cardListRequest.send();
});
}
}

View File

@ -13,6 +13,8 @@ let currentGame: {
webSocket: WebSocket
} | null;
let enterGameTimeout: number | null = null;
const playerList = document.getElementById('playerList')!;
const playerListItems: HTMLElement[] = [ ];

View File

@ -1,12 +1,23 @@
const preGameForm = document.getElementById('preGameForm') as HTMLFormElement;
const newGameButton = document.getElementById('newGameButton')!;
const joinGameButton = document.getElementById('joinGameButton')!;
const nameBox = document.getElementById('nameBox') as HTMLInputElement;
const gameIDBox = document.getElementById('gameIDBox') as HTMLInputElement;
const maxPlayersBox = document.getElementById('maxPlayersBox') as HTMLSelectElement;
const preGameDeckEditorButton = document.getElementById('preGameDeckEditorButton') as HTMLLinkElement;
const preGameLoadingSection = document.getElementById('preGameLoadingSection')!;
const preGameLoadingLabel = document.getElementById('preGameLoadingLabel')!;
let shownMaxPlayersWarning = false;
function setLoadingMessage(message: string | null) {
if (message)
preGameLoadingLabel.innerText = message;
preGameLoadingSection.hidden = message == null;
for (const button of preGameForm.getElementsByTagName('button'))
button.disabled = message != null;
}
maxPlayersBox.addEventListener('change', () => {
if (!shownMaxPlayersWarning && maxPlayersBox.value != '2') {
if (confirm('Tableturf Battle is designed for two players and may not be well-balanced for more. Do you want to continue?'))
@ -16,7 +27,7 @@ maxPlayersBox.addEventListener('change', () => {
}
});
(document.getElementById('preGameForm') as HTMLFormElement).addEventListener('submit', e => {
preGameForm.addEventListener('submit', e => {
e.preventDefault();
if (e.submitter?.id == 'newGameButton' || (e.submitter?.id == 'preGameImplicitSubmitButton' && !gameIDBox.value)) {
const name = nameBox.value;
@ -41,6 +52,7 @@ maxPlayersBox.addEventListener('change', () => {
data.append('clientToken', clientToken);
data.append('maxPlayers', maxPlayersBox.value);
request.send(data.toString());
setLoadingMessage('Creating a room...');
} else {
const name = nameBox.value;
window.localStorage.setItem('name', name);
@ -48,24 +60,6 @@ maxPlayersBox.addEventListener('change', () => {
}
});
function setUrl(path: string) {
settingUrl = true;
try {
if (canPushState) {
try {
history.pushState(null, '', path);
} catch {
canPushState = false;
location.hash = `#${path}`;
}
} else
location.hash = `#${path}`;
} finally {
settingUrl = false;
}
}
function setGameUrl(gameID: string) { setUrl(`game/${gameID}`); }
function tryJoinGame(name: string, idOrUrl: string, fromInitialLoad: boolean) {
const m = /(?:^|[#/])([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.exec(idOrUrl);
if (!m) {
@ -98,6 +92,8 @@ function tryJoinGame(name: string, idOrUrl: string, fromInitialLoad: boolean) {
alert('The game has already started.');
else
alert('Unable to join the room.');
clearUrlFromGame();
setLoadingMessage(null);
if (fromInitialLoad)
clearPreGameForm(true);
else {
@ -106,10 +102,14 @@ function tryJoinGame(name: string, idOrUrl: string, fromInitialLoad: boolean) {
}
}
});
request.addEventListener('error', () => {
communicationError();
});
let data = new URLSearchParams();
data.append('name', name);
data.append('clientToken', clientToken);
request.send(data.toString());
setLoadingMessage('Joining the game...');
}
function getGameInfo(gameID: string, myPlayerIndex: number | null) {
@ -124,15 +124,7 @@ function backPreGameForm(updateUrl: boolean) {
document.getElementById('preGameJoinSection')!.hidden = true;
if (updateUrl) {
if (canPushState) {
try {
history.pushState(null, '', '../..');
} catch {
canPushState = false;
}
}
if (location.hash)
location.hash = '';
clearUrlFromGame();
}
}
function clearPreGameForm(updateUrl: boolean) {

View File

@ -1,31 +1,31 @@
const stageDatabase = {
stages: null as Stage[] | null,
loadAsync() {
return new Promise<Stage[]>((resolve, reject) => {
if (stageDatabase.stages != null) {
resolve(stageDatabase.stages);
return;
}
const stageListRequest = new XMLHttpRequest();
stageListRequest.open('GET', `${config.apiBaseUrl}/stages`);
stageListRequest.addEventListener('load', e => {
const stages = [ ];
if (stageListRequest.status == 200) {
const s = stageListRequest.responseText;
const response = JSON.parse(s) as object[];
for (const o of response) {
stages.push(Stage.fromJson(o));
}
stageDatabase.stages = stages;
resolve(stages);
} else {
reject(new Error(`Error downloading stage database: response was ${stageListRequest.status}`));
}
});
stageListRequest.addEventListener('error', e => {
reject(new Error('Error downloading stage database: no further information.'))
});
stageListRequest.send();
});
}
}
const stageDatabase = {
stages: null as Stage[] | null,
loadAsync() {
return new Promise<Stage[]>((resolve, reject) => {
if (stageDatabase.stages != null) {
resolve(stageDatabase.stages);
return;
}
const stageListRequest = new XMLHttpRequest();
stageListRequest.open('GET', `${config.apiBaseUrl}/stages`);
stageListRequest.addEventListener('load', e => {
const stages = [ ];
if (stageListRequest.status == 200) {
const s = stageListRequest.responseText;
const response = JSON.parse(s) as object[];
for (const o of response) {
stages.push(Stage.fromJson(o));
}
stageDatabase.stages = stages;
resolve(stages);
} else {
reject(new Error(`Error downloading stage database: response was ${stageListRequest.status}`));
}
});
stageListRequest.addEventListener('error', e => {
reject(new Error('Error downloading stage database: no further information.'))
});
stageListRequest.send();
});
}
}

View File

@ -24,6 +24,35 @@ function setClientToken(token: string) {
clientToken = token;
}
function setUrl(path: string) {
settingUrl = true;
try {
if (canPushState) {
try {
history.pushState(null, '', path);
} catch {
canPushState = false;
location.hash = `#${path}`;
}
} else
location.hash = `#${path}`;
} finally {
settingUrl = false;
}
}
function setGameUrl(gameID: string) { setUrl(`game/${gameID}`); }
function clearUrlFromGame() {
if (canPushState) {
try {
history.pushState(null, '', '../..');
} catch {
canPushState = false;
}
}
if (location.hash)
location.hash = '';
}
function onGameStateChange(game: any, playerData: any) {
if (currentGame == null)
throw new Error('currentGame is null');
@ -97,142 +126,166 @@ function communicationError() {
document.getElementById('errorModal')!.hidden = false;
}
function playerDataReviver(key: string, value: any) {
return !value ? value
: key == 'hand' || key == 'deck'
? (value as (Card | number)[]).map(v => typeof v == 'number' ? cardDatabase.get(v) : Card.fromJson(v))
: key == 'card'
? typeof value == 'number' ? cardDatabase.get(value) : Card.fromJson(value)
: value;
}
function setupWebSocket(gameID: string, myPlayerIndex: number | null) {
const webSocket = new WebSocket(`${config.apiBaseUrl.replace(/(http)(s)?\:\/\//, 'ws$2://')}/websocket?gameID=${gameID}&clientToken=${clientToken}`);
webSocket.addEventListener('open', e => {
const request2 = new XMLHttpRequest();
request2.open('GET', `${config.apiBaseUrl}/games/${gameID}/playerData?clientToken=${clientToken}`);
request2.addEventListener('load', e => {
if (request2.status == 200) {
const response = JSON.parse(request2.responseText);
const playerData = response.playerData as PlayerData | null;
currentGame = {
id: gameID,
me: playerData != null ? { playerIndex: playerData.playerIndex, hand: playerData.hand?.map(Card.fromJson) || null, deck: playerData.deck?.map(Card.fromJson) || null, cardsUsed: playerData.cardsUsed, move: playerData.move } : null,
players: response.game.players,
maxPlayers: response.game.maxPlayers,
webSocket: webSocket
};
for (const li of playerListItems)
playerList.removeChild(li);
playerListItems.splice(0);
for (let i = 0; i < currentGame.maxPlayers; i++) {
var el = document.createElement('li');
playerListItems.push(el);
playerList.appendChild(el);
updatePlayerListItem(i);
}
for (let i = 0; i < playerBars.length; i++) {
playerBars[i].visible = i < currentGame.maxPlayers;
}
for (const button of stageButtons)
button.setStartSpaces(currentGame.maxPlayers);
onGameStateChange(response.game, playerData);
for (let i = 0; i < response.game.players.length; i++) {
if (response.game.players[i].isReady)
showReady(i);
}
if (playerData) {
myPlayerIndex = playerData.playerIndex;
if (playerData.move) {
canPlay = false;
board.autoHighlight = false;
if (!playerData.move.isPass) {
const move = playerData.move as PlayMove;
board.cardPlaying = Card.fromJson(move.card);
board.highlightX = move.x;
board.highlightY = move.y;
board.cardRotation = move.rotation;
board.specialAttack = move.isSpecialAttack;
board.refreshHighlight();
}
}
}
}
});
request2.send();
enterGameTimeout = setTimeout(() => {
webSocket.close(1002, 'Timeout waiting for a sync message');
enterGameTimeout = null;
communicationError();
}, 30000);
});
webSocket.addEventListener('message', e => {
if (currentGame == null)
return;
let s = e.data as string;
console.log(`>> ${s}`);
if (s) {
let payload = JSON.parse(s);
if (payload.event == 'join') {
if (payload.data.playerIndex == currentGame.players.length) {
currentGame.players.push(payload.data.player);
playerListItems[payload.data.playerIndex].innerText = payload.data.player.name;
updatePlayerListItem(payload.data.playerIndex);
let payload = JSON.parse(s, playerDataReviver);
if (payload.event == 'sync') {
if (enterGameTimeout != null) {
clearTimeout(enterGameTimeout);
enterGameTimeout = null;
}
else
communicationError();
} else if (payload.event == 'playerReady') {
currentGame.players[payload.data.playerIndex].isReady = true;
updatePlayerListItem(payload.data.playerIndex);
setLoadingMessage(null);
if (!e.data) {
webSocket.close();
alert('The game was not found.');
} else {
currentGame = {
id: gameID,
me: payload.playerData,
players: payload.data.players,
maxPlayers: payload.data.maxPlayers,
webSocket: webSocket
};
if (payload.data.playerIndex == currentGame.me?.playerIndex) {
lobbyStageSection.hidden = true;
lobbyDeckSection.hidden = true;
}
for (const li of playerListItems)
playerList.removeChild(li);
playerListItems.splice(0);
for (let i = 0; i < currentGame.maxPlayers; i++) {
var el = document.createElement('li');
playerListItems.push(el);
playerList.appendChild(el);
updatePlayerListItem(i);
}
if (playContainers[payload.data.playerIndex].getElementsByTagName('div').length == 0) {
showReady(payload.data.playerIndex);
}
} else if (payload.event == 'stateChange') {
clearReady();
onGameStateChange(payload.data, payload.playerData);
} else if (payload.event == 'turn' || payload.event == 'gameEnd') {
clearReady();
board.autoHighlight = false;
showPage('game');
for (let i = 0; i < playerBars.length; i++) {
playerBars[i].visible = i < currentGame.maxPlayers;
}
for (const button of stageButtons)
button.setStartSpaces(currentGame.maxPlayers);
onGameStateChange(payload.data, payload.playerData);
(async () => {
let anySpecialAttacks = false;
// Show the cards that were played.
clearPlayContainers();
for (let i = 0; i < currentGame.players.length; i++) {
const player = currentGame.players[i];
player.specialPoints = payload.data.game.players[i].specialPoints;
player.totalSpecialPoints = payload.data.game.players[i].totalSpecialPoints;
player.passes = payload.data.game.players[i].passes;
if (currentGame.players[i].isReady)
showReady(i);
}
const move = payload.data.moves[i];
const button = new CardButton('checkbox', move.card);
if (move.isSpecialAttack) {
anySpecialAttacks = true;
button.element.classList.add('specialAttack');
} else if (move.isPass) {
const el = document.createElement('div');
el.className = 'passLabel';
el.innerText = 'Pass';
button.element.appendChild(el);
if (currentGame.me) {
if (currentGame.me.move) {
canPlay = false;
board.autoHighlight = false;
if (!currentGame.me.move.isPass) {
const move = currentGame.me.move as PlayMove;
board.cardPlaying = move.card;
board.highlightX = move.x;
board.highlightY = move.y;
board.cardRotation = move.rotation;
board.specialAttack = move.isSpecialAttack;
board.refreshHighlight();
}
}
button.inputElement.hidden = true;
playContainers[i].append(button.element);
}
}
} else {
if (currentGame == null) {
communicationError();
return;
}
switch (payload.event) {
case 'join':
if (payload.data.playerIndex == currentGame.players.length) {
currentGame.players.push(payload.data.player);
playerListItems[payload.data.playerIndex].innerText = payload.data.player.name;
updatePlayerListItem(payload.data.playerIndex);
}
else
communicationError();
break;
case 'playerReady':
currentGame.players[payload.data.playerIndex].isReady = true;
updatePlayerListItem(payload.data.playerIndex);
if (payload.data.playerIndex == currentGame.me?.playerIndex) {
lobbyStageSection.hidden = true;
lobbyDeckSection.hidden = true;
}
if (playContainers[payload.data.playerIndex].getElementsByTagName('div').length == 0) {
showReady(payload.data.playerIndex);
}
break;
case 'stateChange':
clearReady();
onGameStateChange(payload.data, payload.playerData);
break;
case 'turn':
case 'gameEnd':
clearReady();
board.autoHighlight = false;
showPage('game');
let anySpecialAttacks = false;
// Show the cards that were played.
clearPlayContainers();
for (let i = 0; i < currentGame.players.length; i++) {
const player = currentGame.players[i];
player.specialPoints = payload.data.game.players[i].specialPoints;
player.totalSpecialPoints = payload.data.game.players[i].totalSpecialPoints;
player.passes = payload.data.game.players[i].passes;
await playInkAnimations(payload.data, anySpecialAttacks);
updateHand(payload.playerData.hand);
turnNumberLabel.setTurnNumber(payload.data.game.turnNumber);
clearPlayContainers();
if (payload.event == 'gameEnd') {
gameButtonsContainer.hidden = true;
gamePage.classList.add('gameEnded');
showResult();
} else {
canPlay = myPlayerIndex != null;
board.autoHighlight = canPlay;
setupControlsForPlay();
}
})();
const move = payload.data.moves[i];
const button = new CardButton('checkbox', move.card);
if (move.isSpecialAttack) {
anySpecialAttacks = true;
button.element.classList.add('specialAttack');
} else if (move.isPass) {
const el = document.createElement('div');
el.className = 'passLabel';
el.innerText = 'Pass';
button.element.appendChild(el);
}
button.inputElement.hidden = true;
playContainers[i].append(button.element);
}
(async () => {
await playInkAnimations(payload.data, anySpecialAttacks);
updateHand(payload.playerData.hand);
turnNumberLabel.setTurnNumber(payload.data.game.turnNumber);
clearPlayContainers();
if (payload.event == 'gameEnd') {
gameButtonsContainer.hidden = true;
gamePage.classList.add('gameEnded');
showResult();
} else {
canPlay = myPlayerIndex != null;
board.autoHighlight = canPlay;
setupControlsForPlay();
}
})();
break;
}
}
}
});
@ -253,6 +306,7 @@ function processUrl() {
if (location.pathname.endsWith('/deckeditor') || location.hash == '#deckeditor')
showDeckList();
else {
showPage('preGame');
const m = /^(.*)\/game\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/.exec(location.toString());
if (m)
presetGameID(m[2]);
@ -261,7 +315,6 @@ function processUrl() {
presetGameID(location.hash);
} else {
clearPreGameForm(false);
showPage('preGame');
}
}
return true;

View File

@ -1099,3 +1099,59 @@ dialog::backdrop {
height: 13em;
}
}
/* Loading spinner - based on loading.io */
.loadingContainer:not([hidden]) {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5em;
}
.lds-ripple {
--border-width: 0.15em;
position: relative;
width: 1.5em;
height: 1.5em;
display: inline-block;
}
.lds-ripple div {
position: absolute;
border: var(--border-width) solid #fff;
opacity: 1;
border-radius: 50%;
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.lds-ripple div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes lds-ripple {
0% {
top: calc(50% - var(--border-width));
left: calc(50% - var(--border-width));
right: calc(50% - var(--border-width));
bottom: calc(50% - var(--border-width));
opacity: 0;
}
4.9% {
top: calc(50% - var(--border-width));
left: calc(50% - var(--border-width));
right: calc(50% - var(--border-width));
bottom: calc(50% - var(--border-width));
opacity: 0;
}
5% {
top: calc(50% - var(--border-width));
left: calc(50% - var(--border-width));
right: calc(50% - var(--border-width));
bottom: calc(50% - var(--border-width));
opacity: 1;
}
100% {
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
}
}

View File

@ -0,0 +1,44 @@
using Newtonsoft.Json;
namespace TableturfBattleServer.DTO;
internal class WebSocketPayload<T> {
[JsonProperty("event")]
public string EventName;
[JsonProperty("data")]
public T Payload;
public WebSocketPayload(string eventName, T payload) {
this.EventName = eventName ?? throw new ArgumentNullException(nameof(eventName));
this.Payload = payload;
}
}
internal class WebSocketPayloadWithPlayerData<T> : WebSocketPayload<T> {
[JsonProperty("playerData")]
public PlayerData? PlayerData;
public WebSocketPayloadWithPlayerData(string eventName, T payload, PlayerData? playerData) : base(eventName, payload)
=> this.PlayerData = playerData;
}
public class PlayerData {
[JsonProperty("playerIndex")]
public int PlayerIndex;
[JsonProperty("hand")]
public Card[]? Hand;
[JsonProperty("deck")]
public Card[]? Deck;
[JsonProperty("move")]
public Move? Move;
[JsonProperty("cardsUsed")]
public List<int>? CardsUsed;
public PlayerData(int playerIndex, Card[]? hand, Card[]? deck, Move? move, List<int>? cardsUsed) {
this.PlayerIndex = playerIndex;
this.Hand = hand;
this.Deck = deck;
this.Move = move;
this.CardsUsed = cardsUsed;
}
public PlayerData(int playerIndex, Player player) : this(playerIndex, player.Hand, player.Deck, player.Move, player.CardsUsed) { }
}

View File

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json;
namespace TableturfBattleServer;
public struct Error {

View File

@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using Newtonsoft.Json;
@ -177,6 +176,8 @@ public class Game {
var placements = new List<Placement>();
var specialSpacesActivated = new List<Point>();
if (this.Board == null) throw new InvalidOperationException("No board?!");
// Place the ink.
(Placement placement, int cardSize)? placementData = null;
foreach (var i in Enumerable.Range(0, this.Players.Count).Where(i => this.Players[i] != null).OrderByDescending(i => this.Players[i]!.Move!.Card.Size)) {
@ -284,21 +285,21 @@ public class Game {
internal void SendPlayerReadyEvent(int playerIndex) => this.SendEvent("playerReady", new { playerIndex }, false);
internal void SendEvent(string eventType, object? data, bool includePlayerData) {
foreach (var session in Program.httpServer.WebSocketServices.Hosts.First().Sessions.Sessions) {
internal void SendEvent<T>(string eventType, T data, bool includePlayerData) {
foreach (var session in Program.httpServer!.WebSocketServices.Hosts.First().Sessions.Sessions) {
if (session is TableturfWebSocketBehaviour behaviour && behaviour.GameID == this.ID) {
if (includePlayerData) {
object? playerData = null;
DTO.PlayerData? playerData = null;
for (int i = 0; i < this.Players.Count; i++) {
var player = this.Players[i];
if (player.Token == behaviour.ClientToken) {
playerData = new { playerIndex = i, hand = player.Hand, deck = player.Deck, move = player.Move, cardsUsed = player.CardsUsed };
playerData = new(i, player);
break;
}
}
behaviour.SendInternal(JsonConvert.SerializeObject(new { @event = eventType, data, playerData }));
behaviour.SendInternal(JsonConvert.SerializeObject(new DTO.WebSocketPayloadWithPlayerData<T>(eventType, data, playerData)));
} else {
behaviour.SendInternal(JsonConvert.SerializeObject(new { @event = eventType, data }));
behaviour.SendInternal(JsonConvert.SerializeObject(new DTO.WebSocketPayload<T>(eventType, data)));
}
}
}

View File

@ -8,6 +8,8 @@ using System.Web;
using Newtonsoft.Json;
using TableturfBattleServer.DTO;
using WebSocketSharp.Server;
using HttpListenerRequest = WebSocketSharp.Net.HttpListenerRequest;
@ -154,7 +156,7 @@ internal class Program {
SetResponse(e.Response, (int) HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(new {
game,
playerData = game.GetPlayer(clientToken, out var playerIndex, out var player)
? new { playerIndex, hand = player.Hand, deck = player.Deck, cardsUsed = player.CardsUsed, move = player.Move }
? new PlayerData(playerIndex, player)
: null
}));
break;

View File

@ -1,24 +1,24 @@
using Newtonsoft.Json;
namespace TableturfBattleServer;
public class Stage {
[JsonProperty("name")]
public string Name { get; }
[JsonProperty("grid")]
internal readonly Space[,] grid;
/// <summary>
/// The lists of starting spaces on this stage.
/// </summary>
/// <remarks>
/// The smallest list with at least as many spaces as players in the game will be used.
/// For example, if there is a list of 3 and a list of 4, the list of 3 will be used for 2 or 3 players, and the list of 4 will be used for 4 players.
/// </remarks>
[JsonProperty("startSpaces")]
internal readonly Point[][] startSpaces;
public Stage(string name, Space[,] grid, Point[][] startSpaces) {
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.grid = grid ?? throw new ArgumentNullException(nameof(grid));
this.startSpaces = startSpaces ?? throw new ArgumentNullException(nameof(startSpaces));
}
}
using Newtonsoft.Json;
namespace TableturfBattleServer;
public class Stage {
[JsonProperty("name")]
public string Name { get; }
[JsonProperty("grid")]
internal readonly Space[,] grid;
/// <summary>
/// The lists of starting spaces on this stage.
/// </summary>
/// <remarks>
/// The smallest list with at least as many spaces as players in the game will be used.
/// For example, if there is a list of 3 and a list of 4, the list of 3 will be used for 2 or 3 players, and the list of 4 will be used for 4 players.
/// </remarks>
[JsonProperty("startSpaces")]
internal readonly Point[][] startSpaces;
public Stage(string name, Space[,] grid, Point[][] startSpaces) {
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.grid = grid ?? throw new ArgumentNullException(nameof(grid));
this.startSpaces = startSpaces ?? throw new ArgumentNullException(nameof(startSpaces));
}
}

View File

@ -1,132 +1,132 @@
using System.Collections.ObjectModel;
using Newtonsoft.Json;
namespace TableturfBattleServer;
internal class StageDatabase {
private const Space E = Space.Empty;
private const Space o = Space.OutOfBounds;
private static readonly Stage[] stages = new Stage[] {
new("Main Street", new Space[9, 26], new[] {
new Point[] { new(4, 21), new(4, 4), new(4, 13) },
new Point[] { new(2, 21), new(6, 4), new(2, 4), new(6, 21) }
}),
new("Thunder Point", new Space[,] {
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
}, new[] {
new Point[] { new(3, 18), new(12, 3), new(7, 11) },
new Point[] { new(3, 18), new(12, 3), new(3, 10), new(12, 11) },
}),
new("X Marks the Garden", new Space[,] {
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
}, new[] { new Point[] { new(9, 19), new(x: 9, 3), new(3, 11), new(15, 11) } }),
new("Square Squared", new Space[15, 15], new[] { new Point[] { new(3, 11), new(11, 3), new(3, 3), new(11, 11) }}),
new("Lakefront Property", new Space[,] {
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, o, o, o, o, E, E, E, E, E, E, },
{ E, E, E, E, E, E, o, o, o, o, E, E, E, E, E, E, },
{ E, E, E, E, E, E, o, o, o, o, E, E, E, E, E, E, },
{ E, E, E, E, E, E, o, o, o, o, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
}, new[] { new Point[] { new(3, 12), new(12, 3), new(3, 3), new(12, 12) }}),
new("Double Gemini", new Space[,] {
{ o, o, o, o, o, o, o, o, E, o, o, o, o, o, o, o, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, E, E, E, o, o, o, o, o, E, E, E, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, E, E, E, E, E, o, o, o, E, E, E, E, E, o, o, o, o, o, o },
{ o, o, o, o, o, E, E, E, E, E, E, E, o, E, E, E, E, E, E, E, o, o, o, o, o },
{ o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o },
{ o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o },
{ o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o },
{ o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o },
{ o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o },
{ o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o },
{ o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o },
{ o, o, o, o, o, E, E, E, E, E, E, E, o, E, E, E, E, E, E, E, o, o, o, o, o },
{ o, o, o, o, o, o, E, E, E, E, E, o, o, o, E, E, E, E, E, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, E, E, E, o, o, o, o, o, E, E, E, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, o, o, o, o, o, o, o, E, o, o, o, o, o, o, o, o },
}, new[] {
new Point[] { new(8, 19), new(8, 5), new(8, 12) },
new Point[] { new(5, 16), new(11, 8), new(5, 8), new(11, 16) }
}),
new("River Drift", new Space[,] {
{ o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o },
}, new[] {
new Point[] { new(3, 21), new(13, 3), new(8, 12) },
new Point[] { new(3, 21), new(13, 3), new(8, 16), new(8, 8) }
}),
new("Box Seats", new Space[10, 10], new[] { new Point[] { new(2, 7), new(7, 2), new(2, 2), new(7, 7) }}),
};
public static Version Version { get; } = new(1, 1, 0, 0);
public static DateTime LastModified { get; } = new(2022, 10, 6, 6, 0, 0, DateTimeKind.Utc);
public static string JSON { get; }
public static ReadOnlyCollection<Stage> Stages { get; }
static StageDatabase() {
Stages = Array.AsReadOnly(stages);
JSON = JsonConvert.SerializeObject(stages);
}
}
using System.Collections.ObjectModel;
using Newtonsoft.Json;
namespace TableturfBattleServer;
internal class StageDatabase {
private const Space E = Space.Empty;
private const Space o = Space.OutOfBounds;
private static readonly Stage[] stages = new Stage[] {
new("Main Street", new Space[9, 26], new[] {
new Point[] { new(4, 21), new(4, 4), new(4, 13) },
new Point[] { new(2, 21), new(6, 4), new(2, 4), new(6, 21) }
}),
new("Thunder Point", new Space[,] {
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o, o, o, o },
}, new[] {
new Point[] { new(3, 18), new(12, 3), new(7, 11) },
new Point[] { new(3, 18), new(12, 3), new(3, 10), new(12, 11) },
}),
new("X Marks the Garden", new Space[,] {
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, E, E, E, E, E, E, o, o, o, o, o, o, o, o },
}, new[] { new Point[] { new(9, 19), new(x: 9, 3), new(3, 11), new(15, 11) } }),
new("Square Squared", new Space[15, 15], new[] { new Point[] { new(3, 11), new(11, 3), new(3, 3), new(11, 11) }}),
new("Lakefront Property", new Space[,] {
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, o, o, o, o, E, E, E, E, E, E, },
{ E, E, E, E, E, E, o, o, o, o, E, E, E, E, E, E, },
{ E, E, E, E, E, E, o, o, o, o, E, E, E, E, E, E, },
{ E, E, E, E, E, E, o, o, o, o, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, },
}, new[] { new Point[] { new(3, 12), new(12, 3), new(3, 3), new(12, 12) }}),
new("Double Gemini", new Space[,] {
{ o, o, o, o, o, o, o, o, E, o, o, o, o, o, o, o, E, o, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, E, E, E, o, o, o, o, o, E, E, E, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, E, E, E, E, E, o, o, o, E, E, E, E, E, o, o, o, o, o, o },
{ o, o, o, o, o, E, E, E, E, E, E, E, o, E, E, E, E, E, E, E, o, o, o, o, o },
{ o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o },
{ o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o },
{ o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o },
{ o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o },
{ o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o },
{ o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o },
{ o, o, o, o, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, o, o, o, o },
{ o, o, o, o, o, E, E, E, E, E, E, E, o, E, E, E, E, E, E, E, o, o, o, o, o },
{ o, o, o, o, o, o, E, E, E, E, E, o, o, o, E, E, E, E, E, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, E, E, E, o, o, o, o, o, E, E, E, o, o, o, o, o, o, o },
{ o, o, o, o, o, o, o, o, E, o, o, o, o, o, o, o, E, o, o, o, o, o, o, o, o },
}, new[] {
new Point[] { new(8, 19), new(8, 5), new(8, 12) },
new Point[] { new(5, 16), new(11, 8), new(5, 8), new(11, 16) }
}),
new("River Drift", new Space[,] {
{ o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, E, E, E, E, E, E, E },
{ o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, E },
{ E, E, E, E, E, E, E, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o },
{ E, E, E, E, E, E, E, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o },
}, new[] {
new Point[] { new(3, 21), new(13, 3), new(8, 12) },
new Point[] { new(3, 21), new(13, 3), new(8, 16), new(8, 8) }
}),
new("Box Seats", new Space[10, 10], new[] { new Point[] { new(2, 7), new(7, 2), new(2, 2), new(7, 7) }}),
};
public static Version Version { get; } = new(1, 1, 0, 0);
public static DateTime LastModified { get; } = new(2022, 10, 6, 6, 0, 0, DateTimeKind.Utc);
public static string JSON { get; }
public static ReadOnlyCollection<Stage> Stages { get; }
static StageDatabase() {
Stages = Array.AsReadOnly(stages);
JSON = JsonConvert.SerializeObject(stages);
}
}

View File

@ -1,7 +1,9 @@
using System.Web;
using Newtonsoft.Json;
using WebSocketSharp.Server;
namespace TableturfBattleServer;
internal class TableturfWebSocketBehaviour : WebSocketBehavior {
public Guid GameID { get; set; }
public Guid ClientToken { get; set; }
@ -13,7 +15,21 @@ internal class TableturfWebSocketBehaviour : WebSocketBehavior {
this.GameID = gameID;
if (args.TryGetValue("clientToken", out var clientTokenString) && Guid.TryParse(clientTokenString, out var clientToken))
this.ClientToken = clientToken;
// Send an initial state payload.
if (Program.games.TryGetValue(this.GameID, out var game)) {
DTO.PlayerData? playerData = null;
for (int i = 0; i < game.Players.Count; i++) {
var player = game.Players[i];
if (player.Token == this.ClientToken) {
playerData = new(i, player);
break;
}
}
this.Send(JsonConvert.SerializeObject(new DTO.WebSocketPayloadWithPlayerData<Game?>("sync", game, playerData)));
} else
this.Send(JsonConvert.SerializeObject(new DTO.WebSocketPayloadWithPlayerData<Game?>("sync", null, null)));
}
internal void SendInternal(string data) => this.Send(data);
}
}