mirror of
https://github.com/AndrioCelos/TableturfBattleApp.git
synced 2026-08-24 11:54:29 -05:00
Add more options for stage selection. Resolves #13
This commit is contained in:
@@ -85,12 +85,14 @@
|
||||
</div>
|
||||
</section>
|
||||
<section id="lobbyStageSection" hidden>
|
||||
<h3>Vote for the stage.</h3>
|
||||
<form id="stageSelectionForm">
|
||||
<div class="submitButtonContainer">
|
||||
<button type="submit" id="submitStageButton">Submit</button>
|
||||
<div class="loadingContainer" hidden>
|
||||
<div class="loadingSpinner"></div>
|
||||
<form id="stageSelectionForm" hidden>
|
||||
<div id="stageSelectionFormHeader">
|
||||
<h3 id="stagePrompt">Vote for the stage.</h3>
|
||||
<div class="submitButtonContainer">
|
||||
<button type="submit" id="submitStageButton" disabled>Submit</button>
|
||||
<div class="loadingContainer" hidden>
|
||||
<div class="loadingSpinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stageList">
|
||||
@@ -98,6 +100,11 @@
|
||||
</div>
|
||||
<div id="stageListLoadingSection"><div class="loadingSpinner"></div> Loading stages...</div>
|
||||
</form>
|
||||
<form id="strikeOrderSelectionForm" hidden>
|
||||
<h3 id="stagePrompt">Do you want to strike first or second?</h3>
|
||||
<button type="submit" data-strike-index="0">Strike first</button>
|
||||
<button type="submit" data-strike-index="1">Strike second</button>
|
||||
</form>
|
||||
</section>
|
||||
<div id="lobbyDeckSection" hidden>
|
||||
<h3>Choose your deck.</h3>
|
||||
@@ -602,6 +609,49 @@
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<p>Stage selection:</p>
|
||||
<table>
|
||||
<tr>
|
||||
<td><label for="stageSelectionRuleFirstBox">First battle:</label></td>
|
||||
<td>
|
||||
<select id="stageSelectionRuleFirstBox">
|
||||
<option value="Vote">Vote</option>
|
||||
<option value="Random">Random/Fixed</option>
|
||||
<option value="Strike">Strike</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="stageSelectionRuleAfterWinBox">After a win:</label></td>
|
||||
<td>
|
||||
<select id="stageSelectionRuleAfterWinBox">
|
||||
<option value="Inherit">Same method as the first battle</option>
|
||||
<option value="Same">Repeat stage of the last battle</option>
|
||||
<option value="Vote">Vote</option>
|
||||
<option value="Random">Random/Fixed</option>
|
||||
<option value="Counterpick">Counterpick</option>
|
||||
<option value="Strike">Strike</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="stageSelectionRuleAfterDrawBox">After a draw:</label></td>
|
||||
<td>
|
||||
<select id="stageSelectionRuleAfterDrawBox">
|
||||
<option value="Inherit">Same method as the first battle</option>
|
||||
<option value="Same">Repeat stage of the last battle</option>
|
||||
<option value="Vote">Vote</option>
|
||||
<option value="Random">Random/Fixed</option>
|
||||
<option value="Strike">Strike</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="gameSetupForceSameDeckAfterDrawBox"><input type="checkbox" id="gameSetupForceSameDeckAfterDrawBox"/> Force same decks</label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>Stage switch:</p>
|
||||
<div id="stageSwitch"></div>
|
||||
<p>
|
||||
<button type="submit" id="gameSetupSubmitButton">Create room</button>
|
||||
<button type="submit">Back</button>
|
||||
|
||||
@@ -3,6 +3,7 @@ class CheckButtonGroup<TValue> {
|
||||
entries: Array<{ button: CheckButton, value: TValue }> = [ ];
|
||||
parentElement: HTMLElement | null;
|
||||
value: TValue | null = null;
|
||||
allowMultipleSelections = false;
|
||||
|
||||
constructor(parentElement?: HTMLElement | null) {
|
||||
this.parentElement = parentElement ?? null;
|
||||
@@ -12,7 +13,10 @@ class CheckButtonGroup<TValue> {
|
||||
|
||||
private setupButton(button: CheckButton, value: TValue) {
|
||||
button.buttonElement.addEventListener('click', () => {
|
||||
if (button.enabled && !button.checked) {
|
||||
if (!button.enabled) return;
|
||||
if (this.allowMultipleSelections)
|
||||
button.checked = !button.checked;
|
||||
else if (!button.checked) {
|
||||
for (const el of this.entries) {
|
||||
if (el.button == button) {
|
||||
el.button.checked = true;
|
||||
|
||||
@@ -154,7 +154,7 @@ function initTest(stage: Stage) {
|
||||
clear();
|
||||
testMode = true;
|
||||
gamePage.classList.add('deckTest');
|
||||
currentGame = { id: 'test', game: { state: GameState.Ongoing, maxPlayers: 2, players: [ ], turnNumber: 1, turnTimeLimit: null, turnTimeLeft: null, goalWinCount: null }, me: { playerIndex: 0, move: null, deck: null, hand: null, cardsUsed: [ ] }, webSocket: null };
|
||||
currentGame = { id: 'test', game: { state: GameState.Ongoing, maxPlayers: 2, players: [ ], turnNumber: 1, turnTimeLimit: null, turnTimeLeft: null, goalWinCount: null }, me: { playerIndex: 0, move: null, deck: null, hand: null, cardsUsed: [ ], stageSelectionPrompt: null }, webSocket: null };
|
||||
board.resize(stage.copyGrid());
|
||||
const startSpaces = stage.getStartSpaces(2);
|
||||
board.startSpaces = startSpaces;
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
const lobbyWinCounters: WinCounter[] = [ ];
|
||||
|
||||
const stageButtons = new CheckButtonGroup<Stage>(document.getElementById('stageList')!);
|
||||
const stageButtons = new CheckButtonGroup<number>(document.getElementById('stageList')!);
|
||||
const shareLinkButton = document.getElementById('shareLinkButton') as HTMLButtonElement;
|
||||
const showQrCodeButton = document.getElementById('showQrCodeButton') as HTMLButtonElement;
|
||||
const stageSelectionForm = document.getElementById('stageSelectionForm') as HTMLFormElement;
|
||||
const stageSelectionFormLoadingSection = stageSelectionForm.getElementsByClassName('loadingContainer')[0] as HTMLElement;
|
||||
const stageSelectionFormSubmitButton = document.getElementById('submitStageButton') as HTMLButtonElement;
|
||||
const stageRandomButton = CheckButton.fromId('stageRandomButton');
|
||||
const strikeOrderSelectionForm = document.getElementById('strikeOrderSelectionForm') as HTMLFormElement;
|
||||
|
||||
const deckSelectionForm = document.getElementById('deckSelectionForm') as HTMLFormElement;
|
||||
const deckSelectionFormLoadingSection = deckSelectionForm.getElementsByClassName('loadingContainer')[0] as HTMLElement;
|
||||
const lobbySelectedStageSection = document.getElementById('lobbySelectedStageSection')!;
|
||||
const lobbyStageSection = document.getElementById('lobbyStageSection')!;
|
||||
const stagePrompt = document.getElementById('stagePrompt')!;
|
||||
const lobbyStageSubmitButton = document.getElementById('submitStageButton') as HTMLButtonElement;
|
||||
const lobbyDeckSection = document.getElementById('lobbyDeckSection')!;
|
||||
const lobbyDeckList = document.getElementById('lobbyDeckList')!;
|
||||
@@ -26,13 +27,17 @@ let qrCode: QRCode | null;
|
||||
let lobbyShareData: ShareData | null;
|
||||
|
||||
let selectedStageIndicator = null as StageButton | null;
|
||||
let stageSelectionPrompt = null as StageSelectionPrompt | null;
|
||||
|
||||
function lobbyInitStageDatabase(stages: Stage[]) {
|
||||
stageButtons.add(stageRandomButton, -1);
|
||||
let i = 0;
|
||||
for (const stage of stages) {
|
||||
const button = new StageButton(stage);
|
||||
stageButtons.add(button, stage);
|
||||
stageButtons.add(button, i++);
|
||||
button.buttonElement.addEventListener('click', () => {
|
||||
stageRandomButton.checked = false;
|
||||
lobbyStageSubmitButton.disabled = !stageSelectionPrompt || stageButtons.buttons.filter(b => b.checked).length != (stageSelectionPrompt.promptType == StageSelectionPromptType.Strike ? stageSelectionPrompt.numberOfStagesToStrike : 1);
|
||||
});
|
||||
button.setStartSpaces(2);
|
||||
}
|
||||
@@ -49,12 +54,72 @@ function initLobbyPage(url: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function showStageSelectionForm() {
|
||||
function showStageSelectionForm(prompt: StageSelectionPrompt | null, isReady: boolean) {
|
||||
stageSelectionPrompt = prompt;
|
||||
if (!prompt) return;
|
||||
|
||||
lobbyStageSection.hidden = false;
|
||||
stageSelectionFormLoadingSection.hidden = true;
|
||||
stageRandomButton.checked = true;
|
||||
stageButtons.deselect();
|
||||
lobbyStageSubmitButton.disabled = false;
|
||||
lobbyStageSubmitButton.disabled = true;
|
||||
|
||||
let i = -1;
|
||||
for (const button of stageButtons.buttons) {
|
||||
const originalClass = i < 0 ? 'stageRandom' : 'stage';
|
||||
if (prompt.bannedStages?.includes(i)) {
|
||||
button.buttonElement.className = `${originalClass} banned`;
|
||||
button.enabled = false;
|
||||
} else if (prompt.struckStages?.includes(i)) {
|
||||
button.buttonElement.className = `${originalClass} struck`;
|
||||
button.enabled = false;
|
||||
} else {
|
||||
button.buttonElement.className = originalClass;
|
||||
button.enabled = prompt.promptType != StageSelectionPromptType.Wait && !isReady;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
switch (prompt.promptType) {
|
||||
case StageSelectionPromptType.Vote:
|
||||
stageSelectionForm.hidden = false;
|
||||
stageRandomButton.buttonElement.hidden = false;
|
||||
strikeOrderSelectionForm.hidden = true;
|
||||
stagePrompt.innerText = isReady ? 'Opponent is choosing...' : 'Vote for the stage.';
|
||||
stageButtons.allowMultipleSelections = false;
|
||||
stageButtons.parentElement!.classList.remove('striking');
|
||||
break;
|
||||
case StageSelectionPromptType.VoteOrder:
|
||||
stageSelectionForm.hidden = true;
|
||||
strikeOrderSelectionForm.hidden = false;
|
||||
for (const button of strikeOrderSelectionForm.getElementsByTagName('button'))
|
||||
(<HTMLButtonElement> button).disabled = isReady;
|
||||
break;
|
||||
case StageSelectionPromptType.Strike:
|
||||
stageSelectionForm.hidden = false;
|
||||
stageRandomButton.buttonElement.hidden = true;
|
||||
strikeOrderSelectionForm.hidden = true;
|
||||
stagePrompt.innerText = prompt.numberOfStagesToStrike == 1 ? 'Choose a stage to strike.' : `Choose ${prompt.numberOfStagesToStrike} stages to strike.`;
|
||||
stageButtons.allowMultipleSelections = prompt.numberOfStagesToStrike != 1;
|
||||
stageButtons.parentElement!.classList.add('striking');
|
||||
break;
|
||||
case StageSelectionPromptType.Choose:
|
||||
stageSelectionForm.hidden = false;
|
||||
stageRandomButton.buttonElement.hidden = true;
|
||||
strikeOrderSelectionForm.hidden = true;
|
||||
stagePrompt.innerText = 'Choose the stage for the next battle.';
|
||||
stageButtons.allowMultipleSelections = false;
|
||||
stageButtons.parentElement!.classList.remove('striking');
|
||||
break;
|
||||
case StageSelectionPromptType.Wait:
|
||||
stageSelectionForm.hidden = false;
|
||||
stageRandomButton.buttonElement.hidden = true;
|
||||
strikeOrderSelectionForm.hidden = true;
|
||||
stagePrompt.innerText = currentGame?.game.state == GameState.ChoosingStage ? 'Opponent is choosing...' : 'Possible stages:';
|
||||
stageButtons.allowMultipleSelections = false;
|
||||
stageButtons.parentElement!.classList.remove('striking');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
shareLinkButton.addEventListener('click', () => {
|
||||
@@ -111,7 +176,7 @@ function lobbyLockSettings(lock: boolean) {
|
||||
|
||||
function clearReady() {
|
||||
if (!currentGame) throw new Error('No current game');
|
||||
stageSelectionFormSubmitButton.disabled = false;
|
||||
lobbyStageSubmitButton.disabled = false;
|
||||
stageSelectionFormLoadingSection.hidden = true;
|
||||
for (var i = 0; i < currentGame.game.players.length; i++) {
|
||||
currentGame.game.players[i].isReady = false;
|
||||
@@ -171,6 +236,7 @@ function initDeckSelection() {
|
||||
}
|
||||
lobbyDeckSubmitButton.disabled = selectedDeck == null;
|
||||
deckSelectionFormLoadingSection.hidden = true;
|
||||
lobbyStageSection.hidden = true;
|
||||
lobbyDeckSection.hidden = false;
|
||||
} else {
|
||||
lobbyDeckSection.hidden = true;
|
||||
@@ -215,6 +281,7 @@ deckSelectionForm.addEventListener('submit', e => {
|
||||
stageRandomButton.buttonElement.addEventListener('click', () => {
|
||||
stageRandomButton.checked = true;
|
||||
stageButtons.deselect();
|
||||
lobbyStageSubmitButton.disabled = false;
|
||||
});
|
||||
|
||||
stageSelectionForm.addEventListener('submit', e => {
|
||||
@@ -222,20 +289,41 @@ stageSelectionForm.addEventListener('submit', e => {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open('POST', `${config.apiBaseUrl}/games/${currentGame!.id}/chooseStage`);
|
||||
req.addEventListener('load', () => {
|
||||
stageSelectionFormLoadingSection.hidden = true;
|
||||
if (req.status != 204) {
|
||||
stageSelectionFormLoadingSection.hidden = true;
|
||||
alert(req.responseText);
|
||||
lobbyStageSubmitButton.disabled = false;
|
||||
}
|
||||
});
|
||||
req.addEventListener('error', () => communicationError());
|
||||
let data = new URLSearchParams();
|
||||
const stageName = stageRandomButton.checked ? 'random' : stageButtons.value!.name;
|
||||
data.append('clientToken', clientToken);
|
||||
data.append('stage', stageName);
|
||||
data.append('stages', stageButtons.entries.filter(e => e.button.checked).map(e => e.value).join(','));
|
||||
req.send(data.toString());
|
||||
|
||||
localStorage.setItem('lastStage', stageName);
|
||||
stageSelectionFormLoadingSection.hidden = false;
|
||||
lobbyStageSubmitButton.disabled = true;
|
||||
});
|
||||
|
||||
strikeOrderSelectionForm.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
for (const button of strikeOrderSelectionForm.getElementsByTagName('button'))
|
||||
(<HTMLButtonElement> button).disabled = true;
|
||||
let req = new XMLHttpRequest();
|
||||
req.open('POST', `${config.apiBaseUrl}/games/${currentGame!.id}/chooseStage`);
|
||||
req.addEventListener('load', () => {
|
||||
if (req.status != 204) {
|
||||
stageSelectionFormLoadingSection.hidden = true;
|
||||
alert(req.responseText);
|
||||
lobbyStageSubmitButton.disabled = false;
|
||||
for (const button of strikeOrderSelectionForm.getElementsByTagName('button'))
|
||||
(<HTMLButtonElement> button).disabled = false;
|
||||
}
|
||||
});
|
||||
req.addEventListener('error', () => communicationError());
|
||||
let data = new URLSearchParams();
|
||||
const number = e.submitter!.dataset.strikeIndex!;
|
||||
data.append('clientToken', clientToken);
|
||||
data.append('stages', number);
|
||||
req.send(data.toString());
|
||||
});
|
||||
|
||||
@@ -18,6 +18,13 @@ const gameSetupForm = document.getElementById('gameSetupForm') as HTMLFormElemen
|
||||
const maxPlayersBox = document.getElementById('maxPlayersBox') as HTMLSelectElement;
|
||||
const turnTimeLimitBox = document.getElementById('turnTimeLimitBox') as HTMLInputElement;
|
||||
const goalWinCountBox = document.getElementById('goalWinCountBox') as HTMLSelectElement;
|
||||
const stageSelectionRuleFirstBox = document.getElementById('stageSelectionRuleFirstBox') as HTMLSelectElement;
|
||||
const stageSelectionRuleAfterWinBox = document.getElementById('stageSelectionRuleAfterWinBox') as HTMLSelectElement;
|
||||
const stageSelectionRuleAfterDrawBox = document.getElementById('stageSelectionRuleAfterDrawBox') as HTMLSelectElement;
|
||||
const stageSwitch = document.getElementById('stageSwitch')!;
|
||||
const stageSwitchButtons: HTMLButtonElement[] = [ ];
|
||||
const gameSetupForceSameDeckAfterDrawBox = document.getElementById('gameSetupForceSameDeckAfterDrawBox') as HTMLInputElement;
|
||||
const gameSetupSubmitButton = document.getElementById('gameSetupSubmitButton') as HTMLButtonElement;
|
||||
|
||||
const optionsColourLock = document.getElementById('optionsColourLock') as HTMLInputElement;
|
||||
const optionsTurnNumberStyle = document.getElementById('optionsTurnNumberStyle') as HTMLSelectElement;
|
||||
@@ -35,6 +42,37 @@ function setLoadingMessage(message: string | null) {
|
||||
}
|
||||
}
|
||||
|
||||
function preGameInitStageDatabase(stages: Stage[]) {
|
||||
for (const stage of stages) {
|
||||
const button = document.createElement('button');
|
||||
|
||||
const div1 = document.createElement('div');
|
||||
div1.className = 'stageName';
|
||||
div1.innerText = stage.name;
|
||||
button.appendChild(div1);
|
||||
|
||||
const div2 = document.createElement('div');
|
||||
div2.className = 'stageStatus';
|
||||
div2.innerText = 'Allowed';
|
||||
button.appendChild(div2);
|
||||
|
||||
button.type = 'button';
|
||||
button.dataset.index = stageSwitchButtons.length.toString();
|
||||
button.dataset.status = '0';
|
||||
stageSwitchButtons.push(button);
|
||||
button.addEventListener('click', stageSwitchButton_click);
|
||||
stageSwitch.appendChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
function stageSwitchButton_click(e: Event) {
|
||||
const button = e.currentTarget as HTMLButtonElement;
|
||||
let status = button.dataset.status == '0' ? 1 : button.dataset.status == '1' ? 2 : 0;
|
||||
button.dataset.status = status.toString();
|
||||
(<HTMLElement>button.getElementsByClassName('stageStatus')[0]).innerText = [ 'Allowed', 'Counterpick only', 'Banned' ][status];
|
||||
gameSetupSubmitButton.disabled = stageSwitchButtons.every(b => b.dataset.status != '0');
|
||||
}
|
||||
|
||||
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?'))
|
||||
@@ -116,6 +154,27 @@ function createRoom(useOptionsForm: boolean) {
|
||||
data.append('turnTimeLimit', turnTimeLimitBox.value);
|
||||
if (goalWinCountBox.value)
|
||||
data.append('goalWinCount', goalWinCountBox.value);
|
||||
|
||||
const stageSelectionRuleFirst = {
|
||||
method: StageSelectionMethod[stageSelectionRuleFirstBox.value as keyof typeof StageSelectionMethod],
|
||||
bannedStages: stageSwitchButtons.map((_, i) => i).filter(i => stageSwitchButtons[i].dataset.status != '0'),
|
||||
strikeCounts: [ 1, 2 ],
|
||||
};
|
||||
const stageSelectionRuleAfterWin = {
|
||||
method: stageSelectionRuleAfterWinBox.value == 'Inherit' ? stageSelectionRuleFirst.method : StageSelectionMethod[stageSelectionRuleAfterWinBox.value as keyof typeof StageSelectionMethod],
|
||||
bannedStages: stageSwitchButtons.map((_, i) => i).filter(i => stageSwitchButtons[i].dataset.status == '2'),
|
||||
strikeCounts: [ 2 ],
|
||||
};
|
||||
const stageSelectionRuleAfterDraw = {
|
||||
method: stageSelectionRuleAfterWinBox.value == 'Inherit' ? stageSelectionRuleFirst.method : StageSelectionMethod[stageSelectionRuleAfterDrawBox.value as keyof typeof StageSelectionMethod],
|
||||
bannedStages: stageSwitchButtons.map((_, i) => i).filter(i => stageSwitchButtons[i].dataset.status == '2'),
|
||||
strikeCounts: [ 1, 2 ],
|
||||
};
|
||||
|
||||
data.append('stageSelectionRuleFirst', JSON.stringify(stageSelectionRuleFirst));
|
||||
data.append('stageSelectionRuleAfterWin', JSON.stringify(stageSelectionRuleAfterWin));
|
||||
data.append('stageSelectionRuleAfterDraw', JSON.stringify(stageSelectionRuleAfterDraw));
|
||||
data.append('ForceSameDeckAfterDrawBox', gameSetupForceSameDeckAfterDrawBox.checked.toString());
|
||||
}
|
||||
request.send(data.toString());
|
||||
setLoadingMessage('Creating a room...');
|
||||
|
||||
@@ -4,4 +4,20 @@ interface PlayerData {
|
||||
deck: Deck | null;
|
||||
cardsUsed: number[];
|
||||
move: Move | null;
|
||||
stageSelectionPrompt: StageSelectionPrompt | null;
|
||||
}
|
||||
|
||||
interface StageSelectionPrompt {
|
||||
promptType: StageSelectionPromptType;
|
||||
numberOfStagesToStrike: number;
|
||||
struckStages: number[] | null;
|
||||
bannedStages: number[] | null;
|
||||
}
|
||||
|
||||
enum StageSelectionPromptType {
|
||||
Vote,
|
||||
VoteOrder,
|
||||
Strike,
|
||||
Choose,
|
||||
Wait
|
||||
}
|
||||
|
||||
13
TableturfBattleClient/src/StageSelectionRule.ts
Normal file
13
TableturfBattleClient/src/StageSelectionRule.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
enum StageSelectionMethod {
|
||||
Same,
|
||||
Vote,
|
||||
Random,
|
||||
Counterpick,
|
||||
Strike
|
||||
}
|
||||
|
||||
interface StageSelectionRule {
|
||||
method: StageSelectionMethod;
|
||||
bannedStages: number[];
|
||||
strikeCounts: number[];
|
||||
}
|
||||
@@ -51,6 +51,7 @@ function initCardDatabase(cards: Card[]) {
|
||||
deckEditInitCardDatabase(cards);
|
||||
}
|
||||
function initStageDatabase(stages: Stage[]) {
|
||||
preGameInitStageDatabase(stages);
|
||||
lobbyInitStageDatabase(stages);
|
||||
deckEditInitStageDatabase(stages);
|
||||
}
|
||||
@@ -140,15 +141,15 @@ function onGameStateChange(game: any, playerData: PlayerData | null) {
|
||||
initLobbyPage(window.location.toString());
|
||||
showPage('lobby');
|
||||
clearConfirmLeavingGame();
|
||||
showStageSelectionForm(playerData?.stageSelectionPrompt ?? null, playerData && game.players[playerData.playerIndex]?.isReady);
|
||||
lobbySelectedStageSection.hidden = true;
|
||||
lobbyStageSection.hidden = !playerData || game.players[playerData.playerIndex]?.isReady;
|
||||
break;
|
||||
case GameState.ChoosingDeck:
|
||||
showPage('lobby');
|
||||
if (currentGame.me) setConfirmLeavingGame();
|
||||
if (selectedStageIndicator)
|
||||
lobbySelectedStageSection.removeChild(selectedStageIndicator.buttonElement);
|
||||
selectedStageIndicator = new StageButton(stageDatabase.stages?.find(s => s.name == game.stage)!);
|
||||
selectedStageIndicator = new StageButton(stageDatabase.stages![game.stage]);
|
||||
selectedStageIndicator.buttonElement.id = 'selectedStageButton';
|
||||
selectedStageIndicator.buttonElement.disabled = true;
|
||||
selectedStageIndicator.setStartSpaces(game.players.length);
|
||||
@@ -280,8 +281,10 @@ function setupWebSocket(gameID: string) {
|
||||
playerBars[i].visible = i < currentGame.game.maxPlayers;
|
||||
}
|
||||
|
||||
for (const button of stageButtons.buttons)
|
||||
for (const button of stageButtons.buttons) {
|
||||
if (!(button instanceof StageButton)) continue;
|
||||
(button as StageButton).setStartSpaces(currentGame.game.maxPlayers);
|
||||
}
|
||||
|
||||
onGameStateChange(payload.data, payload.playerData);
|
||||
|
||||
@@ -336,7 +339,6 @@ function setupWebSocket(gameID: string) {
|
||||
lobbySetReady(payload.data.playerIndex);
|
||||
|
||||
if (payload.data.playerIndex == currentGame.me?.playerIndex) {
|
||||
lobbyStageSection.hidden = true;
|
||||
lobbyDeckSection.hidden = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -115,6 +115,14 @@ footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#stageSwitch button { display: block; }
|
||||
#stageSwitch button[data-status='0'] { color: lime; }
|
||||
#stageSwitch button[data-status='1'] { color: yellow; }
|
||||
#stageSwitch button[data-status='2'] { color: red; }
|
||||
#stageSwitch button * { display: inline-block; }
|
||||
#stageSwitch button .stageName { width: 12em; text-align: start; }
|
||||
#stageSwitch button .stageStatus { width: 8em; text-align: end; }
|
||||
|
||||
/* Lobby page */
|
||||
|
||||
#lobbyPage:not([hidden]) {
|
||||
@@ -286,7 +294,7 @@ dialog::backdrop {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stage, .stageRandom {
|
||||
.stage, .stageRandom:not([hidden]) {
|
||||
font: inherit;
|
||||
color: currentColor; /* Override disabled colour */
|
||||
background: black;
|
||||
@@ -299,6 +307,8 @@ dialog::backdrop {
|
||||
flex-flow: column;
|
||||
margin: 5px;
|
||||
}
|
||||
.stage.banned { display: none; }
|
||||
.stage.struck { opacity: 0.25; }
|
||||
|
||||
.stageBody {
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -26,13 +26,15 @@ public class PlayerData {
|
||||
public Deck? Deck;
|
||||
public Move? Move;
|
||||
public List<int>? CardsUsed;
|
||||
public StageSelectionPrompt? StageSelectionPrompt;
|
||||
|
||||
public PlayerData(int playerIndex, Card[]? hand, Deck? deck, Move? move, List<int>? cardsUsed) {
|
||||
public PlayerData(int playerIndex, Card[]? hand, Deck? deck, Move? move, List<int>? cardsUsed, StageSelectionPrompt? stageSelectionPrompt) {
|
||||
this.PlayerIndex = playerIndex;
|
||||
this.Hand = hand;
|
||||
this.Deck = deck;
|
||||
this.Move = move;
|
||||
this.CardsUsed = cardsUsed;
|
||||
this.StageSelectionPrompt = stageSelectionPrompt;
|
||||
}
|
||||
public PlayerData(int playerIndex, Player player) : this(playerIndex, player.Hand, player.CurrentGameData.Deck, player.Move, player.CardsUsed) { }
|
||||
public PlayerData(int playerIndex, Player player) : this(playerIndex, player.Hand, player.CurrentGameData.Deck, player.Move, player.CardsUsed, player.StageSelectionPrompt) { }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using System.Net;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
public struct Error {
|
||||
public readonly struct Error {
|
||||
[JsonIgnore]
|
||||
public HttpStatusCode HttpStatusCode { get; }
|
||||
public string Code { get; }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
@@ -14,7 +13,7 @@ public class Game {
|
||||
public List<Player> Players { get; } = new(4);
|
||||
public int MaxPlayers { get; set; }
|
||||
[JsonProperty("stage")]
|
||||
public string? StageName { get; private set; }
|
||||
public int? StageIndex { get; private set; }
|
||||
public Space[,]? Board { get; private set; }
|
||||
public Point[]? StartSpaces;
|
||||
|
||||
@@ -25,10 +24,17 @@ public class Game {
|
||||
[JsonIgnore]
|
||||
internal DateTime abandonedSince = DateTime.UtcNow;
|
||||
|
||||
public required StageSelectionRules StageSelectionRuleFirst { get; set; }
|
||||
public required StageSelectionRules StageSelectionRuleAfterWin { get; set; }
|
||||
public required StageSelectionRules StageSelectionRuleAfterDraw { get; set; }
|
||||
public bool ForceSameDeckAfterDraw { get; set; }
|
||||
|
||||
public List<int> StruckStages = new();
|
||||
|
||||
[JsonIgnore]
|
||||
internal List<Deck> deckCache = new();
|
||||
[JsonIgnore]
|
||||
internal List<string> setStages = new();
|
||||
internal List<int> setStages = new();
|
||||
|
||||
public Game(int maxPlayers) => this.MaxPlayers = maxPlayers;
|
||||
|
||||
@@ -63,6 +69,13 @@ public class Game {
|
||||
}
|
||||
playerIndex = this.Players.Count;
|
||||
this.Players.Add(player);
|
||||
|
||||
player.StageSelectionPrompt = this.StageSelectionRuleFirst.Method switch {
|
||||
StageSelectionMethod.Vote => new() { PromptType = StageSelectionPromptType.Vote, BannedStages = this.StageSelectionRuleFirst.BannedStages, StruckStages = Array.Empty<int>() },
|
||||
StageSelectionMethod.Strike => new() { PromptType = StageSelectionPromptType.VoteOrder, BannedStages = this.StageSelectionRuleFirst.BannedStages, StruckStages = Array.Empty<int>() },
|
||||
_ => new() { PromptType = StageSelectionPromptType.Wait, BannedStages = this.StageSelectionRuleFirst.BannedStages, StruckStages = Array.Empty<int>() }
|
||||
};
|
||||
|
||||
error = default;
|
||||
return true;
|
||||
}
|
||||
@@ -82,6 +95,40 @@ public class Game {
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryChooseStages(Player player, ICollection<int> stages, out Error error) {
|
||||
if (player.StageSelectionPrompt == null || player.StageSelectionPrompt.Value.PromptType == StageSelectionPromptType.Wait) {
|
||||
error = new(HttpStatusCode.Conflict, "CannotChooseStage", "You cannot choose stages now.");
|
||||
return false;
|
||||
}
|
||||
if (player.selectedStages != null) {
|
||||
error = new(HttpStatusCode.Conflict, "StageAlreadyChosen", "You've already chosen a stage.");
|
||||
return false;
|
||||
}
|
||||
if (player.StageSelectionPrompt.Value.PromptType == StageSelectionPromptType.VoteOrder) {
|
||||
if (stages.Count != 1) {
|
||||
error = new(HttpStatusCode.BadRequest, "InvalidStage", "Invalid stage selection.");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
var rule = this.GetCurrentStageSelectionRule();
|
||||
if (stages.Intersect(rule.BannedStages).Any()) {
|
||||
error = new(HttpStatusCode.UnprocessableEntity, "IllegalStage", "A selected stage is banned.");
|
||||
return false;
|
||||
}
|
||||
if (player.StageSelectionPrompt.Value.StruckStages != null && stages.Intersect(player.StageSelectionPrompt.Value.StruckStages).Any()) { // Includes stages previously won on when counterpicking.
|
||||
error = new(HttpStatusCode.UnprocessableEntity, "IllegalStage", "A selected stage was struck.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (stages.Any(i => i >= StageDatabase.Stages.Count)) {
|
||||
error = new(HttpStatusCode.UnprocessableEntity, "StageNotFound", "No such stage is known.");
|
||||
return false;
|
||||
}
|
||||
player.selectedStages = stages;
|
||||
error = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
public Deck GetDeck(string name, int sleeves, IEnumerable<int> cardNumbers, IEnumerable<int> cardUpgrades) {
|
||||
var deck = this.deckCache.FirstOrDefault(d => d.Name == name && d.Sleeves == sleeves && cardNumbers.SequenceEqual(from c in d.Cards select c.Number) && cardUpgrades.SequenceEqual(d.Upgrades));
|
||||
if (deck == null) {
|
||||
@@ -143,11 +190,18 @@ public class Game {
|
||||
return isAnchored;
|
||||
}
|
||||
|
||||
private StageSelectionRules GetCurrentStageSelectionRule() {
|
||||
return this.Players.Count == 0 || this.Players[0].Games.Count <= 1
|
||||
? this.StageSelectionRuleFirst
|
||||
: this.Players.Any(p => p.Games[^2].won) ? this.StageSelectionRuleAfterWin : this.StageSelectionRuleAfterDraw;
|
||||
}
|
||||
|
||||
internal void Tick() {
|
||||
if (this.State is GameState.WaitingForPlayers or GameState.ChoosingStage && this.Players.Count >= 2 && this.Players.All(p => p.selectedStageIndex != null)) {
|
||||
if (this.State is GameState.WaitingForPlayers or GameState.ChoosingStage && this.Players.Count >= 2 && this.Players.All(p => p.StageSelectionPrompt == null || p.StageSelectionPrompt.Value.PromptType == StageSelectionPromptType.Wait || p.selectedStages != null)) {
|
||||
// Choose colours.
|
||||
var random = new Random();
|
||||
if (this.State == GameState.WaitingForPlayers) {
|
||||
this.State = GameState.ChoosingStage;
|
||||
var index = random.Next(Colours.Length);
|
||||
var increment = this.Players.Count switch {
|
||||
2 => random.Next(3, 7),
|
||||
@@ -165,29 +219,88 @@ public class Game {
|
||||
}
|
||||
|
||||
// Choose the stage.
|
||||
var stageIndex = this.Players[random.Next(this.Players.Count)].selectedStageIndex!.Value;
|
||||
if (stageIndex < 0) stageIndex = random.Next(StageDatabase.Stages.Count);
|
||||
var stage = StageDatabase.Stages[stageIndex];
|
||||
this.StageName = stage.Name;
|
||||
this.setStages.Add(stage.Name);
|
||||
this.Board = (Space[,]) stage.grid.Clone();
|
||||
var rule = this.GetCurrentStageSelectionRule();
|
||||
switch (rule.Method) {
|
||||
case StageSelectionMethod.Vote: {
|
||||
var stageIndex = this.Players[random.Next(this.Players.Count)].selectedStages!.First();
|
||||
if (stageIndex < 0) stageIndex = random.Next(StageDatabase.Stages.Count);
|
||||
this.LockInStage(stageIndex);
|
||||
this.SendEvent("stateChange", this, true);
|
||||
break;
|
||||
}
|
||||
case StageSelectionMethod.Random: {
|
||||
var legalStages = Enumerable.Range(0, StageDatabase.Stages.Count).Except(rule.BannedStages).ToList();
|
||||
var stageIndex = legalStages[random.Next(legalStages.Count)];
|
||||
this.LockInStage(stageIndex);
|
||||
this.SendEvent("stateChange", this, true);
|
||||
break;
|
||||
}
|
||||
case StageSelectionMethod.Counterpick: {
|
||||
var player = this.Players.FirstOrDefault(p => p.StageSelectionPrompt != null && p.StageSelectionPrompt?.PromptType != StageSelectionPromptType.Wait);
|
||||
if (player == null) {
|
||||
var legalStages = Enumerable.Range(0, StageDatabase.Stages.Count).Except(rule.BannedStages).ToList();
|
||||
this.LockInStage(legalStages[random.Next(legalStages.Count)]);
|
||||
} else {
|
||||
if (player.selectedStages!.First() >= 0)
|
||||
this.LockInStage(player.selectedStages.First());
|
||||
else {
|
||||
var legalStages = Enumerable.Range(0, StageDatabase.Stages.Count).Except(rule.BannedStages).Except(player.StageSelectionPrompt!.Value.StruckStages).ToList();
|
||||
this.LockInStage(legalStages[random.Next(legalStages.Count)]);
|
||||
}
|
||||
}
|
||||
this.SendEvent("stateChange", this, true);
|
||||
break;
|
||||
}
|
||||
case StageSelectionMethod.Strike: {
|
||||
var player = this.Players.FirstOrDefault(p => p.StageSelectionPrompt != null && p.StageSelectionPrompt?.PromptType != StageSelectionPromptType.Wait);
|
||||
switch (player.StageSelectionPrompt!.Value.PromptType) {
|
||||
case StageSelectionPromptType.VoteOrder:
|
||||
// Choose who will strike first.
|
||||
Player? firstPlayer = null;
|
||||
foreach (var player2 in this.Players) {
|
||||
if (player2.selectedStages!.First() == 0) {
|
||||
if (firstPlayer == null || random.Next(2) == 0)
|
||||
firstPlayer = player2;
|
||||
}
|
||||
}
|
||||
firstPlayer ??= this.Players[random.Next(this.Players.Count)];
|
||||
// Present new prompts.
|
||||
foreach (var player2 in this.Players) {
|
||||
player2.StageSelectionPrompt = new() { PromptType = player2 == firstPlayer ? StageSelectionPromptType.Strike : StageSelectionPromptType.Wait, BannedStages = rule.BannedStages, NumberOfStagesToStrike = 1 };
|
||||
player2.selectedStages = null;
|
||||
}
|
||||
break;
|
||||
case StageSelectionPromptType.Strike:
|
||||
this.StruckStages.AddRange(player.selectedStages!);
|
||||
var index = this.Players.IndexOf(player);
|
||||
index = (index + 1) % this.Players.Count;
|
||||
|
||||
// Place starting positions.
|
||||
var list = stage.startSpaces.Where(s => s.Length >= this.Players.Count).MinBy(s => s.Length) ?? throw new InvalidOperationException("Couldn't find start spaces");
|
||||
this.StartSpaces = list;
|
||||
for (int i = 0; i < this.Players.Count; i++)
|
||||
this.Board[list[i].X, list[i].Y] = Space.SpecialInactive1 | (Space) i;
|
||||
|
||||
this.State = GameState.ChoosingDeck;
|
||||
this.SendEvent("stateChange", this, true);
|
||||
// Present new prompts.
|
||||
for (var i = 0; i < this.Players.Count; i++) {
|
||||
this.Players[i].StageSelectionPrompt = i == index
|
||||
? this.StruckStages.Count == 2 || Enumerable.Range(0, StageDatabase.Stages.Count).Except(rule.BannedStages).Except(this.StruckStages).Count() <= 3
|
||||
? new() { PromptType = StageSelectionPromptType.Choose, BannedStages = rule.BannedStages, StruckStages = this.StruckStages }
|
||||
: new() { PromptType = StageSelectionPromptType.Strike, BannedStages = rule.BannedStages, StruckStages = this.StruckStages, NumberOfStagesToStrike = 2 }
|
||||
: new() { PromptType = StageSelectionPromptType.Wait, BannedStages = rule.BannedStages, StruckStages = this.StruckStages };
|
||||
}
|
||||
break;
|
||||
case StageSelectionPromptType.Choose:
|
||||
if (player.selectedStages!.First() >= 0)
|
||||
this.LockInStage(player.selectedStages.First());
|
||||
else {
|
||||
var legalStages = Enumerable.Range(0, StageDatabase.Stages.Count).Except(rule.BannedStages).Except(player.StageSelectionPrompt!.Value.StruckStages).ToList();
|
||||
this.LockInStage(legalStages[random.Next(legalStages.Count)]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
this.SendEvent("stateChange", this, true);
|
||||
foreach (var player2 in this.Players)
|
||||
player2.selectedStages = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (this.State == GameState.ChoosingDeck && this.Players.All(p => p.CurrentGameData.Deck != null)) {
|
||||
// Draw cards.
|
||||
var random = new Random();
|
||||
foreach (var player in this.Players)
|
||||
player.Shuffle(random);
|
||||
|
||||
this.State = GameState.Redraw;
|
||||
this.TurnTimeLeft = this.TurnTimeLimit;
|
||||
this.StartGame();
|
||||
this.SendEvent("stateChange", this, true);
|
||||
} else if (this.State == GameState.Redraw && this.Players.All(p => p.Move != null)) {
|
||||
var random = new Random();
|
||||
@@ -368,19 +481,117 @@ public class Game {
|
||||
}
|
||||
}
|
||||
} else if (this.State is GameState.GameEnded or GameState.SetEnded && this.Players.All(p => p.Move != null)) {
|
||||
foreach (var player in this.Players) {
|
||||
player.selectedStageIndex = null;
|
||||
player.Hand = null;
|
||||
player.CardsUsed.Clear();
|
||||
player.Games.Add(new());
|
||||
player.ClearMoves();
|
||||
}
|
||||
this.State = GameState.ChoosingStage;
|
||||
this.TurnTimeLeft = this.TurnTimeLimit;
|
||||
this.SendEvent("stateChange", this, true);
|
||||
SetupNextGame();
|
||||
}
|
||||
}
|
||||
|
||||
private void LockInStage(int stageIndex) {
|
||||
var stage = StageDatabase.Stages[stageIndex];
|
||||
this.StageIndex = stageIndex;
|
||||
this.setStages.Add(stageIndex);
|
||||
this.Board = (Space[,]) stage.grid.Clone();
|
||||
|
||||
// Place starting positions.
|
||||
var list = stage.startSpaces.Where(s => s.Length >= this.Players.Count).MinBy(s => s.Length) ?? throw new InvalidOperationException("Couldn't find start spaces");
|
||||
this.StartSpaces = list;
|
||||
for (int i = 0; i < this.Players.Count; i++)
|
||||
this.Board[list[i].X, list[i].Y] = Space.SpecialInactive1 | (Space) i;
|
||||
|
||||
foreach (var player in this.Players) {
|
||||
player.StageSelectionPrompt = null;
|
||||
player.selectedStages = null;
|
||||
}
|
||||
|
||||
if (this.ForceSameDeckAfterDraw && this.Players[0].Games.Count > 1 && !this.Players.Any(p => p.WonLastGame)) {
|
||||
foreach (var player in this.Players)
|
||||
player.CurrentGameData.Deck = player.Games[^2].Deck;
|
||||
this.StartGame();
|
||||
} else
|
||||
this.State = GameState.ChoosingDeck;
|
||||
}
|
||||
|
||||
private void StartGame() {
|
||||
// Draw cards.
|
||||
var random = new Random();
|
||||
foreach (var player in this.Players)
|
||||
player.Shuffle(random);
|
||||
|
||||
this.State = GameState.Redraw;
|
||||
this.TurnTimeLeft = this.TurnTimeLimit;
|
||||
}
|
||||
|
||||
private void SetupNextGame() {
|
||||
this.State = GameState.ChoosingStage;
|
||||
this.StruckStages.Clear();
|
||||
this.TurnTimeLeft = this.TurnTimeLimit;
|
||||
|
||||
var winner = this.Players.FirstOrDefault(p => p.CurrentGameData.won);
|
||||
|
||||
foreach (var player in this.Players) {
|
||||
player.selectedStages = null;
|
||||
player.Hand = null;
|
||||
player.CardsUsed.Clear();
|
||||
player.ClearMoves();
|
||||
player.Games.Add(new());
|
||||
}
|
||||
|
||||
var rule = this.GetCurrentStageSelectionRule();
|
||||
var legalStages = Enumerable.Range(0, StageDatabase.Stages.Count).Except(rule.BannedStages).ToList();
|
||||
switch (rule.Method) {
|
||||
case StageSelectionMethod.Same:
|
||||
this.LockInStage(this.setStages[^1]);
|
||||
break;
|
||||
case StageSelectionMethod.Vote: {
|
||||
if (legalStages.Count == 1) {
|
||||
this.LockInStage(legalStages[0]);
|
||||
break;
|
||||
}
|
||||
var bannedStages = legalStages.Count == 0 ? Array.Empty<int>() : rule.BannedStages;
|
||||
foreach (var player in this.Players) {
|
||||
player.StageSelectionPrompt = new() { PromptType = StageSelectionPromptType.Vote, BannedStages = bannedStages, StruckStages = Array.Empty<int>() };
|
||||
}
|
||||
break;
|
||||
}
|
||||
case StageSelectionMethod.Random: {
|
||||
var random = new Random();
|
||||
var stage = legalStages.Count switch {
|
||||
0 => random.Next(StageDatabase.Stages.Count),
|
||||
1 => legalStages[0],
|
||||
_ => legalStages[random.Next(legalStages.Count)]
|
||||
};
|
||||
this.LockInStage(stage);
|
||||
break;
|
||||
}
|
||||
case StageSelectionMethod.Counterpick: {
|
||||
var playerPicking = this.Players.FirstOrDefault(p => p != winner) ?? this.Players[0]; // Should never reach the latter case.
|
||||
|
||||
// Prevent picking stages that the player has previously won on if that would leave any legal stages.
|
||||
var struckStages = new HashSet<int>();
|
||||
for (var i = 0; i < playerPicking.Games.Count; i++) {
|
||||
if (playerPicking.Games[i].won && !rule.BannedStages.Contains(this.setStages[i]))
|
||||
struckStages.Add(this.setStages[i]);
|
||||
}
|
||||
|
||||
foreach (var player in this.Players)
|
||||
player.StageSelectionPrompt = new() { PromptType = player == playerPicking ? StageSelectionPromptType.Choose : StageSelectionPromptType.Wait, BannedStages = rule.BannedStages, StruckStages = struckStages };
|
||||
break;
|
||||
}
|
||||
case StageSelectionMethod.Strike: {
|
||||
if (winner == null) {
|
||||
foreach (var player in this.Players)
|
||||
player.StageSelectionPrompt = new() { PromptType = StageSelectionPromptType.VoteOrder, BannedStages = rule.BannedStages, StruckStages = Array.Empty<int>() };
|
||||
} else {
|
||||
// After a win, the winner strikes first.
|
||||
foreach (var player in this.Players)
|
||||
player.StageSelectionPrompt = new() { PromptType = player == winner ? StageSelectionPromptType.Strike : StageSelectionPromptType.Wait, BannedStages = rule.BannedStages, StruckStages = Array.Empty<int>(), NumberOfStagesToStrike = 2 };
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.SendEvent("stateChange", this, true);
|
||||
}
|
||||
|
||||
internal void SendPlayerReadyEvent(int playerIndex, bool isTimeout) => this.SendEvent("playerReady", new { playerIndex, isTimeout }, false);
|
||||
|
||||
internal void SendEvent<T>(string eventType, T data, bool includePlayerData) {
|
||||
@@ -446,7 +657,7 @@ public class Game {
|
||||
|
||||
// Games
|
||||
for (int i = 0; i < this.Players[0].Games.Count; i++) {
|
||||
var stageNumber = Enumerable.Range(0, StageDatabase.Stages.Count).First(j => this.setStages[i] == StageDatabase.Stages[j].Name);
|
||||
var stageNumber = this.setStages[i];
|
||||
writer.Write((byte) stageNumber);
|
||||
|
||||
foreach (var player in this.Players) {
|
||||
|
||||
@@ -6,4 +6,5 @@ internal class JsonUtils {
|
||||
private static readonly JsonSerializerSettings serializerSettings = new() { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
||||
|
||||
internal static string Serialise(object? o) => JsonConvert.SerializeObject(o, serializerSettings);
|
||||
internal static T? Deserialise<T>(string json) => JsonConvert.DeserializeObject<T>(json, serializerSettings);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ public class Player {
|
||||
public Colour SpecialAccentColour { get; set; }
|
||||
public bool UIBaseColourIsSpecialColour { get; set; }
|
||||
|
||||
public StageSelectionPrompt? StageSelectionPrompt { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly Game game;
|
||||
[JsonIgnore]
|
||||
@@ -28,6 +30,8 @@ public class Player {
|
||||
|
||||
[JsonIgnore]
|
||||
public SingleGameData CurrentGameData => this.Games[^1];
|
||||
[JsonIgnore]
|
||||
public bool WonLastGame => this.Games.Count > 1 && this.Games[^2].won;
|
||||
|
||||
public int SpecialPoints => this.CurrentGameData.SpecialPoints;
|
||||
|
||||
@@ -36,13 +40,15 @@ public class Player {
|
||||
public int? Sleeves => this.CurrentGameData.Deck?.Sleeves;
|
||||
|
||||
public bool IsReady => this.game.State switch {
|
||||
GameState.WaitingForPlayers or GameState.ChoosingStage => this.selectedStageIndex != null,
|
||||
GameState.WaitingForPlayers or GameState.ChoosingStage => this.selectedStages != null,
|
||||
GameState.ChoosingDeck => this.CurrentGameData.Deck != null,
|
||||
_ => this.Move != null
|
||||
};
|
||||
|
||||
[JsonIgnore]
|
||||
internal int? selectedStageIndex;
|
||||
internal ICollection<int>? selectedStages;
|
||||
|
||||
internal static readonly int[] RandomStageSelection = new[] { -1 };
|
||||
|
||||
public Player(Game game, string name, Guid token) {
|
||||
this.game = game ?? throw new ArgumentNullException(nameof(game));
|
||||
|
||||
@@ -159,8 +159,37 @@ internal class Program {
|
||||
}
|
||||
} else
|
||||
clientToken = Guid.NewGuid();
|
||||
var game = new Game(maxPlayers) { GoalWinCount = goalWinCount, TurnTimeLimit = turnTimeLimit };
|
||||
game.Players.Add(new(game, name, clientToken));
|
||||
|
||||
StageSelectionRules? stageSelectionRuleFirst = null, stageSelectionRuleAfterWin = null, stageSelectionRuleAfterDraw = null;
|
||||
if (d.TryGetValue("stageSelectionRuleFirst", out var json1)) {
|
||||
if (!TryParseStageSelectionRule(json1, out stageSelectionRuleFirst) || stageSelectionRuleFirst.Method is StageSelectionMethod.Same or StageSelectionMethod.Counterpick) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidGameSettings", "stageSelectionRuleFirst was invalid."));
|
||||
return;
|
||||
}
|
||||
} else
|
||||
stageSelectionRuleFirst = StageSelectionRules.Default;
|
||||
if (d.TryGetValue("stageSelectionRuleAfterWin", out var json2)) {
|
||||
if (!TryParseStageSelectionRule(json2, out stageSelectionRuleAfterWin)) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidGameSettings", "stageSelectionRuleAfterWin was invalid."));
|
||||
return;
|
||||
}
|
||||
} else
|
||||
stageSelectionRuleAfterWin = stageSelectionRuleFirst;
|
||||
if (d.TryGetValue("stageSelectionRuleAfterDraw", out var json3)) {
|
||||
if (!TryParseStageSelectionRule(json3, out stageSelectionRuleAfterDraw) || stageSelectionRuleAfterDraw.Method == StageSelectionMethod.Counterpick) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidGameSettings", "stageSelectionRuleAfterDraw was invalid."));
|
||||
return;
|
||||
}
|
||||
} else
|
||||
stageSelectionRuleAfterDraw = stageSelectionRuleFirst;
|
||||
|
||||
if (d.TryGetValue("forceSameDeckAfterDraw", out var forceSameDeckAfterDrawString) && !bool.TryParse(forceSameDeckAfterDrawString, out var forceSameDeckAfterDraw))
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidGameSettings", "forceSameDeckAfterDraw was invalid."));
|
||||
else
|
||||
forceSameDeckAfterDraw = false;
|
||||
|
||||
var game = new Game(maxPlayers) { GoalWinCount = goalWinCount, TurnTimeLimit = turnTimeLimit, StageSelectionRuleFirst = stageSelectionRuleFirst, StageSelectionRuleAfterWin = stageSelectionRuleAfterWin, StageSelectionRuleAfterDraw = stageSelectionRuleAfterDraw, ForceSameDeckAfterDraw = forceSameDeckAfterDraw };
|
||||
game.TryAddPlayer(new(game, name, clientToken), out _, out _);
|
||||
games.Add(game.ID, game);
|
||||
timer.Start();
|
||||
|
||||
@@ -318,35 +347,28 @@ internal class Program {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "NotInGame", "You're not in the game."));
|
||||
return;
|
||||
}
|
||||
if (player.selectedStageIndex != null) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.Conflict, "StageAlreadyChosen", "You've already chosen a stage."));
|
||||
if (!d.TryGetValue("stages", out var stagesString)) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.BadRequest, "InvalidStage", "Missing stages."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!d.TryGetValue("stage", out var stageName)) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.BadRequest, "InvalidStage", "Missing stage name."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (stageName == "random") {
|
||||
player.selectedStageIndex = -1;
|
||||
e.Response.StatusCode = (int) HttpStatusCode.NoContent;
|
||||
game.SendPlayerReadyEvent(playerIndex, false);
|
||||
timer.Start();
|
||||
return;
|
||||
} else {
|
||||
for (var i = 0; i < StageDatabase.Stages.Count; i++) {
|
||||
var stage = StageDatabase.Stages[i];
|
||||
if (stageName == stage.Name) {
|
||||
player.selectedStageIndex = i;
|
||||
e.Response.StatusCode = (int) HttpStatusCode.NoContent;
|
||||
game.SendPlayerReadyEvent(playerIndex, false);
|
||||
timer.Start();
|
||||
return;
|
||||
}
|
||||
var stages = new HashSet<int>();
|
||||
foreach (var field in stagesString.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)) {
|
||||
if (!int.TryParse(field, out var i)) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.BadRequest, "InvalidStage", "Invalid stages."));
|
||||
return;
|
||||
}
|
||||
stages.Add(i);
|
||||
}
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "StageNotFound", "No such stage is known."));
|
||||
|
||||
if (!game.TryChooseStages(player, stages, out var error)) {
|
||||
SetErrorResponse(e.Response, error);
|
||||
return;
|
||||
}
|
||||
|
||||
e.Response.StatusCode = (int) HttpStatusCode.NoContent;
|
||||
game.SendPlayerReadyEvent(playerIndex, false);
|
||||
timer.Start();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -416,15 +438,6 @@ internal class Program {
|
||||
cards[i] = cardNumber;
|
||||
}
|
||||
|
||||
if (d.TryGetValue("stageIndex", out var stageIndexString) && stageIndexString is not ("" or "null" or "undefined")) {
|
||||
if (int.TryParse(stageIndexString, out var stageIndex) && stageIndex >= 0 && stageIndex < StageDatabase.Stages.Count)
|
||||
player.selectedStageIndex = stageIndex;
|
||||
else {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidStage", "Invalid stage index."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
player.CurrentGameData.Deck = game.GetDeck(deckName, deckSleeves, cards, upgrades ?? Enumerable.Repeat(0, 15));
|
||||
e.Response.StatusCode = (int) HttpStatusCode.NoContent;
|
||||
game.SendPlayerReadyEvent(playerIndex, false);
|
||||
@@ -664,4 +677,14 @@ internal class Program {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseStageSelectionRule(string json, [MaybeNullWhen(false)] out StageSelectionRules stageSelectionRule) {
|
||||
try {
|
||||
stageSelectionRule = JsonUtils.Deserialise<StageSelectionRules>(json);
|
||||
return stageSelectionRule != null;
|
||||
} catch (JsonSerializationException) {
|
||||
stageSelectionRule = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
TableturfBattleServer/StageSelectionPrompt.cs
Normal file
20
TableturfBattleServer/StageSelectionPrompt.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace TableturfBattleServer;
|
||||
public struct StageSelectionPrompt {
|
||||
public StageSelectionPromptType PromptType;
|
||||
public int NumberOfStagesToStrike;
|
||||
public ICollection<int> StruckStages;
|
||||
public ICollection<int> BannedStages;
|
||||
}
|
||||
|
||||
public enum StageSelectionPromptType {
|
||||
/// <summary>The player is prompted to vote for a stage.</summary>
|
||||
Vote,
|
||||
/// <summary>The player is prompted to vote for whether to strike first or second.</summary>
|
||||
VoteOrder,
|
||||
/// <summary>The player is prompted to choose stages to strike.</summary>
|
||||
Strike,
|
||||
/// <summary>The player is prompted to choose the stage.</summary>
|
||||
Choose,
|
||||
/// <summary>It is another player's turn to make a choice.</summary>
|
||||
Wait
|
||||
}
|
||||
29
TableturfBattleServer/StageSelectionRules.cs
Normal file
29
TableturfBattleServer/StageSelectionRules.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace TableturfBattleServer;
|
||||
|
||||
public class StageSelectionRules {
|
||||
public StageSelectionMethod Method { get; set; }
|
||||
public int[] BannedStages { get; set; }
|
||||
|
||||
public StageSelectionRules(StageSelectionMethod method, int[]? bannedStages) {
|
||||
this.Method = method;
|
||||
this.BannedStages = bannedStages ?? Array.Empty<int>();
|
||||
}
|
||||
|
||||
public static StageSelectionRules Default { get; } = new(StageSelectionMethod.Vote, Array.Empty<int>());
|
||||
}
|
||||
|
||||
public enum StageSelectionMethod {
|
||||
/// <summary>The battle will be on the same stage as the last battle. This cannot be used for the first battle.</summary>
|
||||
Same,
|
||||
/// <summary>Each player votes for a stage, or random. One of the votes, chosen randomly, decides the stage.</summary>
|
||||
Vote,
|
||||
/// <summary>The stage is chosen randomly. If only one stage is allowed, all battles will be on that stage.</summary>
|
||||
Random,
|
||||
/// <summary>The loser of the last battle chooses the stage. This cannot be used for the first battle or after a draw.</summary>
|
||||
Counterpick,
|
||||
/// <summary>
|
||||
/// Players take turns to ban stages for the next match, until the final player chooses the stage from among the remaining ones.
|
||||
/// For the first battle or after a draw, players vote on who shall strike first. For subsequent battles, the winner of the last battle strikes first.
|
||||
/// </summary>
|
||||
Strike
|
||||
}
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageProjectUrl>https://github.com/AndrioCelos/TableturfBattleApp</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/AndrioCelos/TableturfBattleApp</RepositoryUrl>
|
||||
<Version>0.0.0.0</Version>
|
||||
<Version>0.0.0.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user