diff --git a/client/src/components/forms.tsx b/client/src/components/forms.tsx index 0634ea13..933ce7e0 100644 --- a/client/src/components/forms.tsx +++ b/client/src/components/forms.tsx @@ -65,10 +65,21 @@ export const NumInput: React.FC = (props) => { handleInputChange(tempValue || '') } + /* + * TODO: these useEffects() are a little mid, they should really store the disabled state + * and restore it instead of absolutely setting it to false + */ + React.useEffect(() => { context.setState((prevState) => ({ ...prevState, disableHotkeys: focused })) }, [focused]) + React.useEffect(() => { + return () => { + context.setState((prevState) => ({ ...prevState, disableHotkeys: false })) + } + }, []) + return ( = React.memo((props) => { if (!props.open) return null + const getWinCount = (team: Team) => { + // Only return up to the current match if tournament mode is enabled + if (!activeGame) return 0 + let stopCounting = false + const isWinner = (match: Match) => { + if (context.state.tournament && stopCounting) return 0 + if (match == activeGame.currentMatch) stopCounting = true + return match.winner?.id === team.id ? 1 : 0 + } + return activeGame.matches.reduce((val, match) => val + isWinner(match), 0) + } + const teamBox = (teamIdx: number) => { - let showMatchWinner = - !context.state.tournament || (context.state.activeMatch && context.state.activeMatch.currentTurn.isEnd()) + const winCount = activeGame ? getWinCount(activeGame.teams[teamIdx]) : 0 + const isEndOfMatch = context.state.activeMatch && context.state.activeMatch.currentTurn.isEnd() + + let showMatchWinner = !context.state.tournament || isEndOfMatch showMatchWinner = showMatchWinner && activeGame && activeGame.currentMatch?.winner === activeGame.teams[teamIdx] - let showGameWinner = - !context.state.tournament || - (showMatchWinner && - context.state.activeMatch == - context.state.activeGame?.matches[context.state.activeGame.matches.length - 1]) + let showGameWinner = !context.state.tournament || (showMatchWinner && winCount >= 3) showGameWinner = showGameWinner && activeGame && activeGame.winner === activeGame.teams[teamIdx] return ( @@ -44,9 +56,19 @@ export const GamePage: React.FC = React.memo((props) => {
{activeGame?.teams[teamIdx].name ?? NO_GAME_TEAM_NAME}
{showMatchWinner && ( - - - +
+
+ + + +
+
+ {winCount > 0 && winCount} +
+
)}
diff --git a/client/src/components/sidebar/game/team-table.tsx b/client/src/components/sidebar/game/team-table.tsx index 9267e57d..29f59475 100644 --- a/client/src/components/sidebar/game/team-table.tsx +++ b/client/src/components/sidebar/game/team-table.tsx @@ -160,20 +160,20 @@ export const UnitsTable: React.FC = ({ teamStat, teamIdx }) => } const GlobalUpgradeSection: React.FC<{ teamStat: TeamTurnStat | undefined }> = ({ teamStat }) => { - const upgradeTypes: [schema.GlobalUpgradeType, string][] = [ - [schema.GlobalUpgradeType.ACTION_UPGRADE, 'Global Attack Upgrade'], - [schema.GlobalUpgradeType.CAPTURING_UPGRADE, 'Global Capturing Upgrade'], - [schema.GlobalUpgradeType.HEALING_UPGRADE, 'Global Healing Upgrade'] - ] + const upgradeTypes: Record = { + [schema.GlobalUpgradeType.ACTION_UPGRADE]: 'Global Attack Upgrade', + [schema.GlobalUpgradeType.CAPTURING_UPGRADE]: 'Global Capturing Upgrade', + [schema.GlobalUpgradeType.HEALING_UPGRADE]: 'Global Healing Upgrade' + } if (!teamStat) return <> return ( <> - {upgradeTypes.map( - ([type, name]) => - teamStat.globalUpgrades.has(type) && ( + {teamStat.globalUpgrades.map( + (type) => + upgradeTypes[type] && (
- {name} + {upgradeTypes[type]}
) )} diff --git a/client/src/components/sidebar/runner/runner.tsx b/client/src/components/sidebar/runner/runner.tsx index e7ae61d9..b22b57a9 100644 --- a/client/src/components/sidebar/runner/runner.tsx +++ b/client/src/components/sidebar/runner/runner.tsx @@ -9,6 +9,7 @@ import { SectionHeader } from '../../section-header' import { FixedSizeList, ListOnScrollProps } from 'react-window' import { OpenExternal } from '../../../icons/open-external' import { BasicDialog } from '../../basic-dialog' +import { RingBuffer } from '../../../util/ring-buffer' type RunnerPageProps = { open: boolean @@ -67,7 +68,7 @@ export const RunnerPage: React.FC = ({ open, scaffold }) => { if (availablePlayers.size > 1) setTeamB([...availablePlayers][1]) }, [availablePlayers]) - const MemoConsole = React.useMemo(() => , [consoleLines]) + const MemoConsole = React.useMemo(() => , [consoleLines.effectiveLength()]) if (!open) return null @@ -299,7 +300,7 @@ const JavaSelector: React.FC = (props) => { export type ConsoleLine = { content: string; type: 'output' | 'error' | 'bold' } type Props = { - lines: ConsoleLine[] + lines: RingBuffer } export const Console: React.FC = ({ lines }) => { @@ -320,8 +321,8 @@ export const Console: React.FC = ({ lines }) => { } const ConsoleRow = (props: { index: number; style: any }) => ( - - {lines[props.index].content} + + {lines.get(props.index)!.content} ) @@ -345,17 +346,17 @@ export const Console: React.FC = ({ lines }) => { } useEffect(() => { - if (lines.length == 0) setTail(true) + if (lines.effectiveLength() == 0) setTail(true) if (tail && consoleRef.current) { scrollToBottom() } - }, [lines]) + }, [lines.effectiveLength()]) const lineList = ( ) => Promise, killMatch: (() => Promise) | undefined, - console: ConsoleLine[] + console: RingBuffer ] export function useScaffold(): Scaffold { @@ -35,12 +36,15 @@ export function useScaffold(): Scaffold { const [scaffoldPath, setScaffoldPath] = useState(undefined) const matchPID = useRef(undefined) const forceUpdate = useForceUpdate() - const [consoleLines, setConsoleLines] = useState([]) - const log = (line: ConsoleLine) => - setConsoleLines((prev) => (prev.length > 10000 ? [...prev.slice(1), line] : [...prev, line])) + const consoleLines = useRef>(new RingBuffer(10000)) const [webSocketListener, setWebSocketListener] = useState() + const log = (line: ConsoleLine) => { + consoleLines.current.push(line) + forceUpdate() + } + async function manuallySetupScaffold() { if (!nativeAPI) return setLoading(true) @@ -52,6 +56,7 @@ export function useScaffold(): Scaffold { async function runMatch(javaPath: string, teamA: string, teamB: string, selectedMaps: Set): Promise { if (matchPID.current || !scaffoldPath) return const shouldProfile = false + consoleLines.current.clear() try { const newPID = await dispatchMatch( javaPath, @@ -63,10 +68,9 @@ export function useScaffold(): Scaffold { appContext.state.config.validateMaps, shouldProfile ) - setConsoleLines([]) matchPID.current = newPID } catch (e: any) { - setConsoleLines([{ content: e, type: 'error' }]) + consoleLines.current.push({ content: e, type: 'error' }) } forceUpdate() } @@ -193,7 +197,7 @@ export function useScaffold(): Scaffold { loading, runMatch, matchPID.current ? killMatch : undefined, - consoleLines + consoleLines.current ] } diff --git a/client/src/constants.ts b/client/src/constants.ts index a4c2ec9f..2d593560 100644 --- a/client/src/constants.ts +++ b/client/src/constants.ts @@ -28,6 +28,24 @@ export const ENGINE_BUILTIN_MAP_NAMES: string[] = [ 'DefaultMedium', 'DefaultLarge', 'DefaultHuge', + 'BedWars', + 'Bunkers', + 'Checkered', + 'Diagonal', + 'Divergent', + 'EndAround', + 'FloodGates', + 'Foxes', + 'Fusbol', + 'GaltonBoard', + 'HeMustBeFreed', + 'Intercontinental', + 'Klein', + 'QueenOfHearts', + 'QuestionableChess', + 'Racetrack', + 'Rainbow', + 'TreeSearch', 'AceOfSpades', 'Alien', 'Ambush', diff --git a/client/src/playback/Actions.ts b/client/src/playback/Actions.ts index 70fdb1f5..ad7d615a 100644 --- a/client/src/playback/Actions.ts +++ b/client/src/playback/Actions.ts @@ -254,7 +254,7 @@ export const ACTION_DEFINITIONS: Record = { [schema.Action.GLOBAL_UPGRADE]: class GlobalUpgrade extends Action { apply(turn: Turn): void { const team = turn.bodies.getById(this.robotID).team - turn.stat.getTeamStat(team).globalUpgrades.add(this.target) + turn.stat.getTeamStat(team).globalUpgrades.push(this.target) } } } diff --git a/client/src/playback/TurnStat.ts b/client/src/playback/TurnStat.ts index 7a67f7f1..1a25f694 100644 --- a/client/src/playback/TurnStat.ts +++ b/client/src/playback/TurnStat.ts @@ -8,15 +8,15 @@ export class TeamTurnStat { specializationTotalLevels: [number, number, number, number, number] = [0, 0, 0, 0, 0] resourceAmount: number = 0 resourceAmountAverageDatapoint: number | undefined = undefined - globalUpgrades: Set = new Set() + globalUpgrades: schema.GlobalUpgradeType[] = [] copy(): TeamTurnStat { - const newStat = Object.assign(Object.create(Object.getPrototypeOf(this)), this) + const newStat: TeamTurnStat = Object.assign(Object.create(Object.getPrototypeOf(this)), this) // Copy any internal objects here newStat.robots = [...this.robots] newStat.specializationTotalLevels = [...this.specializationTotalLevels] - newStat.globalUpgrades = new Set(this.globalUpgrades) + newStat.globalUpgrades = [...this.globalUpgrades] return newStat } diff --git a/client/src/util/ring-buffer.ts b/client/src/util/ring-buffer.ts new file mode 100644 index 00000000..d5babf21 --- /dev/null +++ b/client/src/util/ring-buffer.ts @@ -0,0 +1,53 @@ +export class RingBuffer { + private _array: T[] + private _effectiveLength: number + + constructor(n: number) { + this._array = new Array(n) + this._effectiveLength = 0 + } + + public toString() { + return '[object RingBuffer(' + this._array.length + ') effectiveLength ' + this._effectiveLength + ']' + } + + public length() { + return Math.min(this._array.length, this._effectiveLength) + } + + public effectiveLength() { + return this._effectiveLength + } + + public get(i: number) { + if (i < 0 || i >= this.length()) return undefined + const index = this.computeActualIndex(i) + return this._array[index] + } + + public set(i: number, v: T) { + if (i < 0 || i >= this.length()) throw new Error('set() Index out of range') + const index = this.computeActualIndex(i) + this._array[index] = v + } + + public push(v: T) { + const index = this.computeActualIndex(this._effectiveLength) + this._array[index] = v + this._effectiveLength++ + } + + public clear() { + this._effectiveLength = 0 + } + + public *[Symbol.iterator]() { + for (let i = 0; i < this.length(); i++) { + yield this.get(i) + } + } + + private computeActualIndex(offset: number) { + return Math.max((this._effectiveLength - this._array.length, 0) + offset) % this._array.length + } +} diff --git a/engine/src/main/battlecode/common/GameConstants.java b/engine/src/main/battlecode/common/GameConstants.java index d9b8dc45..32babb80 100644 --- a/engine/src/main/battlecode/common/GameConstants.java +++ b/engine/src/main/battlecode/common/GameConstants.java @@ -90,7 +90,7 @@ public class GameConstants { public static final int PASSIVE_CRUMBS_INCREASE = 10; /** The amount of crumbs you gain if your bot kills an enemy while in enemy territory */ - public static final int KILL_CRUMB_REWARD = 50; + public static final int KILL_CRUMB_REWARD = 30; /** The end of the setup rounds in the game */ public static final int SETUP_ROUNDS = 200; diff --git a/engine/src/main/battlecode/common/GlobalUpgrade.java b/engine/src/main/battlecode/common/GlobalUpgrade.java index ced593e4..4d2a4e88 100644 --- a/engine/src/main/battlecode/common/GlobalUpgrade.java +++ b/engine/src/main/battlecode/common/GlobalUpgrade.java @@ -9,9 +9,9 @@ public enum GlobalUpgrade { /** - * Attack upgrade increases the base attack by 75. + * Attack upgrade increases the base attack by 60. */ - ATTACK(75, 0, 0, 0), + ATTACK(60, 0, 0, 0), /** * Healing increases base heal by 50. @@ -19,9 +19,9 @@ public enum GlobalUpgrade { HEALING(0, 50, 0, 0), /** - * Capture upgrade increases the dropped flag delay from 4 rounds to 12 rounds. It also decreases the movement penalty for holding a flag by 8. + * Capture upgrade increases the dropped flag delay from 4 rounds to 25 rounds. It also decreases the movement penalty for holding a flag by 8. */ - CAPTURING(0, 0, 8, -8), + CAPTURING(0, 0, 21, -8), /** * !DO NOT USE! diff --git a/engine/src/main/battlecode/common/TrapType.java b/engine/src/main/battlecode/common/TrapType.java index ff033c62..863f60cf 100644 --- a/engine/src/main/battlecode/common/TrapType.java +++ b/engine/src/main/battlecode/common/TrapType.java @@ -12,7 +12,7 @@ public enum TrapType { * When an opponent enters, explosive traps deal 750 damage to all opponents within a sqrt 13 radius * If an opponent digs/breaks under the trap, it deals 500 damage to all opponnets in radius sqrt 9 */ - EXPLOSIVE (250, 0, 4, 2, 750, 200, false, 5, true, 0), + EXPLOSIVE (200, 0, 4, 2, 750, 200, false, 5, true, 0), /** * When an opponent enters, water traps dig all unoccupied tiles within a radius of sqrt 9 @@ -22,7 +22,7 @@ public enum TrapType { /** * When an opponent enters, all opponent robots movement and action cooldowns are set to 40. */ - STUN (100, 2, 13, 0, 0, 0, false, 5, true, 50), + STUN (100, 2, 13, 0, 0, 0, false, 5, true, 40), NONE (100, 0, 0, 0, 0, 0, false, 0, false, 0); diff --git a/engine/src/main/battlecode/world/resources/BedWars.map24 b/engine/src/main/battlecode/world/resources/BedWars.map24 new file mode 100644 index 00000000..587d6fea Binary files /dev/null and b/engine/src/main/battlecode/world/resources/BedWars.map24 differ diff --git a/engine/src/main/battlecode/world/resources/Bunkers.map24 b/engine/src/main/battlecode/world/resources/Bunkers.map24 new file mode 100644 index 00000000..ae50e779 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Bunkers.map24 differ diff --git a/engine/src/main/battlecode/world/resources/Checkered.map24 b/engine/src/main/battlecode/world/resources/Checkered.map24 new file mode 100644 index 00000000..df7019ae Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Checkered.map24 differ diff --git a/engine/src/main/battlecode/world/resources/Diagonal.map24 b/engine/src/main/battlecode/world/resources/Diagonal.map24 new file mode 100644 index 00000000..3a186f79 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Diagonal.map24 differ diff --git a/engine/src/main/battlecode/world/resources/Divergent.map24 b/engine/src/main/battlecode/world/resources/Divergent.map24 new file mode 100644 index 00000000..d4aeac80 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Divergent.map24 differ diff --git a/engine/src/main/battlecode/world/resources/EndAround.map24 b/engine/src/main/battlecode/world/resources/EndAround.map24 new file mode 100644 index 00000000..b5c7d75e Binary files /dev/null and b/engine/src/main/battlecode/world/resources/EndAround.map24 differ diff --git a/engine/src/main/battlecode/world/resources/FloodGates.map24 b/engine/src/main/battlecode/world/resources/FloodGates.map24 new file mode 100644 index 00000000..95e40687 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/FloodGates.map24 differ diff --git a/engine/src/main/battlecode/world/resources/Foxes.map24 b/engine/src/main/battlecode/world/resources/Foxes.map24 new file mode 100644 index 00000000..f909fdda Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Foxes.map24 differ diff --git a/engine/src/main/battlecode/world/resources/Fusbol.map24 b/engine/src/main/battlecode/world/resources/Fusbol.map24 new file mode 100644 index 00000000..3506f866 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Fusbol.map24 differ diff --git a/engine/src/main/battlecode/world/resources/GaltonBoard.map24 b/engine/src/main/battlecode/world/resources/GaltonBoard.map24 new file mode 100644 index 00000000..2c7c4d37 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/GaltonBoard.map24 differ diff --git a/engine/src/main/battlecode/world/resources/HeMustBeFreed.map24 b/engine/src/main/battlecode/world/resources/HeMustBeFreed.map24 new file mode 100644 index 00000000..9b8debbc Binary files /dev/null and b/engine/src/main/battlecode/world/resources/HeMustBeFreed.map24 differ diff --git a/engine/src/main/battlecode/world/resources/Intercontinental.map24 b/engine/src/main/battlecode/world/resources/Intercontinental.map24 new file mode 100644 index 00000000..88c19e8e Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Intercontinental.map24 differ diff --git a/engine/src/main/battlecode/world/resources/Klein.map24 b/engine/src/main/battlecode/world/resources/Klein.map24 new file mode 100644 index 00000000..19eccd37 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Klein.map24 differ diff --git a/engine/src/main/battlecode/world/resources/QueenOfHearts.map24 b/engine/src/main/battlecode/world/resources/QueenOfHearts.map24 new file mode 100644 index 00000000..853689be Binary files /dev/null and b/engine/src/main/battlecode/world/resources/QueenOfHearts.map24 differ diff --git a/engine/src/main/battlecode/world/resources/QuestionableChess.map24 b/engine/src/main/battlecode/world/resources/QuestionableChess.map24 new file mode 100644 index 00000000..b00990a0 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/QuestionableChess.map24 differ diff --git a/engine/src/main/battlecode/world/resources/Racetrack.map24 b/engine/src/main/battlecode/world/resources/Racetrack.map24 new file mode 100644 index 00000000..d40b33f4 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Racetrack.map24 differ diff --git a/engine/src/main/battlecode/world/resources/Rainbow.map24 b/engine/src/main/battlecode/world/resources/Rainbow.map24 new file mode 100644 index 00000000..dfb4e5c6 Binary files /dev/null and b/engine/src/main/battlecode/world/resources/Rainbow.map24 differ diff --git a/engine/src/main/battlecode/world/resources/TreeSearch.map24 b/engine/src/main/battlecode/world/resources/TreeSearch.map24 new file mode 100644 index 00000000..020b915e Binary files /dev/null and b/engine/src/main/battlecode/world/resources/TreeSearch.map24 differ diff --git a/specs/specs.md.html b/specs/specs.md.html index 3bbe87bd..576798b0 100644 --- a/specs/specs.md.html +++ b/specs/specs.md.html @@ -114,7 +114,7 @@ **Attacking** -Robots can attack enemy robots within **$\sqrt{4}$** tiles. Robots may only attack tiles that contain an enemy robot (no missed attacks are allowed). Attacking incurs a base health penalty of **-150** points to the enemy robot and adds **+20** to the attacking robot’s action cooldown. If your robot kills an enemy robot while your robot is in enemy territory, your team gains **50** crumbs. Enemy territory consists of all tiles that were originally accessible to the enemy during the setup phase. +Robots can attack enemy robots within **$\sqrt{4}$** tiles. Robots may only attack tiles that contain an enemy robot (no missed attacks are allowed). Attacking incurs a base health penalty of **-150** points to the enemy robot and adds **+20** to the attacking robot’s action cooldown. If your robot kills an enemy robot while your robot is in enemy territory, your team gains **30** crumbs. Enemy territory consists of all tiles that were originally accessible to the enemy during the setup phase. **Healing** @@ -156,9 +156,9 @@ | Name | Cost | Function | Action cooldown | --- | --- | --- | --- -| Explosive trap | 250 crumbs | Can be built on land or in water. When an opponent robot enters the cell containing this trap, it explodes dealing **750** damage to all opponent robots within a radius of $\sqrt{4}$ cells. When an opponent robot digs, fills, or builds on the trap, it explodes dealing **200** damage to all opponent robots within a radius of $\sqrt{2}$ cells. The build will fail when this happens, while dig and fill will succeed. | 5 +| Explosive trap | 200 crumbs | Can be built on land or in water. When an opponent robot enters the cell containing this trap, it explodes dealing **750** damage to all opponent robots within a radius of $\sqrt{4}$ cells. When an opponent robot digs, fills, or builds on the trap, it explodes dealing **200** damage to all opponent robots within a radius of $\sqrt{2}$ cells. The build will fail when this happens, while dig and fill will succeed. | 5 | Water trap | 100 crumbs | Can only be built on land. Digs all non-occupied land in a radius of $\sqrt{9}$ when an opponent robot enters a tile within $\sqrt{2}$ units of the trap. | 5 -| Stun trap | 100 crumbs | Can only be built on land. Stuns all enemy robots in a radius of $\sqrt{13}$ when an opponent enters a tile within $\sqrt{2}$ units of the trap, setting all of those robots’ movement and action cooldowns to **50**. | 5 +| Stun trap | 100 crumbs | Can only be built on land. Stuns all enemy robots in a radius of $\sqrt{13}$ when an opponent enters a tile within $\sqrt{2}$ units of the trap, setting all of those robots’ movement and action cooldowns to **40**. | 5 # **Global Upgrades** @@ -166,9 +166,9 @@ | Name | Function | --- | --- | -| Attack Upgrade - Swift Beaks | Increases base attack by **+75**. +| Attack Upgrade - Swift Beaks | Increases base attack by **+60**. | Healing Upgrade - Down Feathers | Increases base heal by **+50** health points. -| Capturing Upgrade - Thin Slices | Increases the dropped flag return delay of the other team’s flag to **12** rounds. Decreases movement cooldown when carrying the flag to **+12**. +| Capturing Upgrade - Thin Slices | Increases the dropped flag return delay of the other team’s flag to **25** rounds. Decreases movement cooldown when carrying the flag to **+12**. # **Specialization Stats** @@ -286,6 +286,14 @@ # **Appendix: Changelog** +- Version 3.0.0 (January 24, 2024) + - Balance changes + - Explosive trap cost 250 -> 200 + - Stun trap stun cooldown 50 -> 40 + - Kill crumb reward 50 -> 30 + - Capturing upgrade flag return delay 8 -> 21 (for a total of 25 rounds) + - Attack upgrade attack increase 75 -> 60 + - Version 2.0.3 (January 22, 2024) - Client improvements - Added HungerGames sprint 1 map