diff --git a/TableturfBattleClient/src/Pages/LobbyPage.ts b/TableturfBattleClient/src/Pages/LobbyPage.ts index 7d41f95..ec4f072 100644 --- a/TableturfBattleClient/src/Pages/LobbyPage.ts +++ b/TableturfBattleClient/src/Pages/LobbyPage.ts @@ -75,7 +75,7 @@ function showStageSelectionForm(prompt: StageSelectionPrompt | null, isReady: bo let i = -1; for (const button of stageButtons.buttons) { const originalClass = i < 0 ? 'stageRandom' : 'stage'; - if (prompt.bannedStages?.includes(i)) { + if (i >= 0 && (currentGame!.game.maxPlayers > stageDatabase.stages![i].maxPlayers || prompt.bannedStages?.includes(i))) { button.buttonElement.className = `${originalClass} banned`; button.enabled = false; } else if (prompt.struckStages?.includes(i)) { diff --git a/TableturfBattleClient/src/Pages/PreGamePage.ts b/TableturfBattleClient/src/Pages/PreGamePage.ts index fbc5dff..ea1ce9c 100644 --- a/TableturfBattleClient/src/Pages/PreGamePage.ts +++ b/TableturfBattleClient/src/Pages/PreGamePage.ts @@ -93,7 +93,7 @@ function stageSwitchButton_click(e: Event) { let status = button.dataset.status == '0' ? 1 : button.dataset.status == '1' ? 2 : 0; button.dataset.status = status.toString(); (button.getElementsByClassName('stageStatus')[0]).innerText = [ 'Allowed', 'Counterpick only', 'Banned' ][status]; - gameSetupSubmitButton.disabled = stageSwitchButtons.every(b => b.dataset.status != '0'); + updateCreateRoomButton(); } maxPlayersBox.addEventListener('change', () => { @@ -103,8 +103,18 @@ maxPlayersBox.addEventListener('change', () => { else maxPlayersBox.value = '2'; } + const maxPlayers = parseInt(maxPlayersBox.value); + for (let i = 0; i < stageDatabase.stages!.length; i++) { + stageSwitchButtons[i].disabled = maxPlayers > stageDatabase.stages![i].maxPlayers; + } + updateCreateRoomButton(); }); +function updateCreateRoomButton() { + const maxPlayers = parseInt(maxPlayersBox.value); + gameSetupSubmitButton.disabled = stageSwitchButtons.every((b, i) => b.dataset.status != '0' || maxPlayers > stageDatabase.stages![i].maxPlayers); +} + newGameSetupButton.addEventListener('click', _ => { gameSetupDialog.showModal(); }); @@ -172,8 +182,9 @@ function createRoom(useOptionsForm: boolean) { data.append('name', name); data.append('clientToken', clientToken); if (useOptionsForm) { + const maxPlayers = parseInt(maxPlayersBox.value); const settings = { - maxPlayers: parseInt(maxPlayersBox.value), + maxPlayers, turnTimeLimit: turnTimeLimitBox.value ? turnTimeLimitBox.valueAsNumber : null, goalWinCount: goalWinCountBox.value ? parseInt(goalWinCountBox.value) : null, allowUpcomingCards: gameSetupAllowUpcomingCardsBox.checked, diff --git a/TableturfBattleClient/src/Stage.ts b/TableturfBattleClient/src/Stage.ts index 13aee3c..86b4b14 100644 --- a/TableturfBattleClient/src/Stage.ts +++ b/TableturfBattleClient/src/Stage.ts @@ -13,6 +13,8 @@ class Stage { return new Stage(obj.name, obj.grid, obj.startSpaces); } + get maxPlayers() { return Math.max(...this.startSpaces.map(a => a.length)); } + getStartSpaces(numPlayers: number) { let list = null as Point[] | null; for (const list2 of this.startSpaces) { diff --git a/TableturfBattleClient/src/StageButton.ts b/TableturfBattleClient/src/StageButton.ts index da56ad9..22fbb73 100644 --- a/TableturfBattleClient/src/StageButton.ts +++ b/TableturfBattleClient/src/StageButton.ts @@ -75,6 +75,7 @@ class StageButton extends CheckButton { this.startCells.splice(0); const startSpaces = this.stage.getStartSpaces(numPlayers); + if (startSpaces == null) return; for (let i = 0; i < numPlayers; i++) { const space = startSpaces[i]; const cell = this.cells[space.x][space.y]!; diff --git a/TableturfBattleServer/Game.cs b/TableturfBattleServer/Game.cs index f9ccef8..ca66c0f 100644 --- a/TableturfBattleServer/Game.cs +++ b/TableturfBattleServer/Game.cs @@ -115,6 +115,10 @@ public class Game(int maxPlayers) { return false; } } else { + if (stages.Any(i => i >= StageDatabase.Stages.Count)) { + error = new(HttpStatusCode.UnprocessableEntity, "InvalidStage", "Invalid stage selection."); + return false; + } var rule = this.GetCurrentStageSelectionRule(); if (stages.Intersect(rule.BannedStages).Any()) { error = new(HttpStatusCode.UnprocessableEntity, "IllegalStage", "A selected stage is banned."); @@ -125,10 +129,6 @@ public class Game(int maxPlayers) { 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; @@ -495,10 +495,10 @@ public class Game(int maxPlayers) { var stage = StageDatabase.Stages[stageIndex]; this.StageIndex = stageIndex; this.setStages.Add(stageIndex); - this.Board = (Space[,]) stage.grid.Clone(); + 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"); + 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; diff --git a/TableturfBattleServer/Program.cs b/TableturfBattleServer/Program.cs index 417f86b..10f399b 100644 --- a/TableturfBattleServer/Program.cs +++ b/TableturfBattleServer/Program.cs @@ -179,21 +179,21 @@ internal partial class Program { 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) { + if (!TryParseStageSelectionRule(json1, maxPlayers, 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)) { + if (!TryParseStageSelectionRule(json2, maxPlayers, 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) { + if (!TryParseStageSelectionRule(json3, maxPlayers, out stageSelectionRuleAfterDraw) || stageSelectionRuleAfterDraw.Method == StageSelectionMethod.Counterpick) { SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidGameSettings", "stageSelectionRuleAfterDraw was invalid.")); return; } @@ -766,10 +766,16 @@ internal partial class Program { return false; } - private static bool TryParseStageSelectionRule(string json, [MaybeNullWhen(false)] out StageSelectionRules stageSelectionRule) { + private static bool TryParseStageSelectionRule(string json, int maxPlayers, [MaybeNullWhen(false)] out StageSelectionRules stageSelectionRule) { try { stageSelectionRule = JsonUtils.Deserialise(json); - return stageSelectionRule != null; + if (stageSelectionRule == null) return false; + stageSelectionRule.AddUnavailableStages(maxPlayers); + // Check that at least one stage is allowed. + for (var i = 0; i < StageDatabase.Stages.Count; i++) { + if (!stageSelectionRule.BannedStages.Contains(i)) return true; + } + return false; } catch (JsonSerializationException) { stageSelectionRule = null; return false; diff --git a/TableturfBattleServer/Stage.cs b/TableturfBattleServer/Stage.cs index 878e0d3..5b0f9d1 100644 --- a/TableturfBattleServer/Stage.cs +++ b/TableturfBattleServer/Stage.cs @@ -4,7 +4,7 @@ namespace TableturfBattleServer; public class Stage(string name, Space[,] grid, Point[][] startSpaces) { public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); [JsonProperty] - internal readonly Space[,] grid = grid ?? throw new ArgumentNullException(nameof(grid)); + internal Space[,] Grid = grid ?? throw new ArgumentNullException(nameof(grid)); /// /// The lists of starting spaces on this stage. /// @@ -13,5 +13,8 @@ public class Stage(string name, Space[,] grid, Point[][] startSpaces) { /// 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. /// [JsonProperty] - internal readonly Point[][] startSpaces = startSpaces ?? throw new ArgumentNullException(nameof(startSpaces)); + internal Point[][] StartSpaces = startSpaces ?? throw new ArgumentNullException(nameof(startSpaces)); + + [JsonIgnore] + public int MaxPlayers => this.StartSpaces.Max(a => a.Length); } diff --git a/TableturfBattleServer/StageSelectionRules.cs b/TableturfBattleServer/StageSelectionRules.cs index 29917bb..441f551 100644 --- a/TableturfBattleServer/StageSelectionRules.cs +++ b/TableturfBattleServer/StageSelectionRules.cs @@ -5,6 +5,17 @@ public class StageSelectionRules(StageSelectionMethod method, int[]? bannedStage public int[] BannedStages { get; set; } = bannedStages ?? Array.Empty(); public static StageSelectionRules Default { get; } = new(StageSelectionMethod.Vote, Array.Empty()); + + public void AddUnavailableStages(int maxPlayers) { + if (maxPlayers == 2) return; + + var list = new List(this.BannedStages); + for (var i = 0; i < StageDatabase.Stages.Count; i++) { + if (maxPlayers > StageDatabase.Stages[i].MaxPlayers) + list.Add(i); + } + this.BannedStages = list.ToArray(); + } } public enum StageSelectionMethod {