Skip to content

Commit 6467db1

Browse files
Merge pull request #70 from battlecode/client
Client 2024
2 parents d73b0da + 3b0014e commit 6467db1

File tree

13 files changed

+230
-57
lines changed

13 files changed

+230
-57
lines changed

client/src-tauri/src/main.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,10 @@ async fn tauri_api(
122122
Ok(vec!(final_path.to_str().unwrap().to_string()))
123123
},
124124
"path.relative" => {
125-
let final_path = RelativePath::new(&args[0])
126-
.relative(&args[1]);
127-
Ok(vec!(final_path.to_string()))
125+
let path_from = RelativePath::new(&args[0]);
126+
let path_to = RelativePath::new(&args[1]);
127+
let result = path_from.relative(path_to);
128+
Ok(vec!(result.to_string()))
128129
},
129130
"path.dirname" => {
130131
let path = Path::new(&args[0]).parent().unwrap_or(Path::new(""));

client/src/app-context.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface AppState {
99
activeGame: Game | undefined
1010
activeMatch: Match | undefined
1111
tournament: Tournament | undefined
12+
loadingRemoteContent: boolean
1213
updatesPerSecond: number
1314
paused: boolean
1415
disableHotkeys: boolean
@@ -20,6 +21,7 @@ const DEFAULT_APP_STATE: AppState = {
2021
activeGame: undefined,
2122
activeMatch: undefined,
2223
tournament: undefined,
24+
loadingRemoteContent: false,
2325
updatesPerSecond: 1,
2426
paused: true,
2527
disableHotkeys: false,

client/src/client-config.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ const DEFAULT_CONFIG = {
1212
showAllIndicators: false,
1313
showAllRobotRadii: false,
1414
showHealthBars: false,
15-
showMapXY: true
15+
showMapXY: true,
16+
showFlagCarryIndicator: true
1617
}
1718

1819
const configDescription: { [key: string]: string } = {
1920
showAllIndicators: 'Show all indicator dots and lines',
2021
showAllRobotRadii: 'Show all robot view and attack radii',
2122
showHealthBars: 'Show health bars below all robots',
22-
showMapXY: 'Show X,Y when hovering a tile'
23+
showMapXY: 'Show X,Y when hovering a tile',
24+
showFlagCarryIndicator: 'Show an obvious indicator over flag carriers'
2325
}
2426

2527
export function getDefaultConfig(): ClientConfig {

client/src/components/game/game-renderer.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,11 @@ export const GameRenderer: React.FC = () => {
133133
return (
134134
<div className="w-full h-screen flex items-center justify-center">
135135
{!activeMatch ? (
136-
<p className="text-white text-center">Select a game from the queue</p>
136+
appContext.state.loadingRemoteContent ? (
137+
<p className="text-white text-center">Loading remote game...</p>
138+
) : (
139+
<p className="text-white text-center">Select a game from the queue</p>
140+
)
137141
) : (
138142
<div ref={wrapperRef} className="relative max-w-full max-h-full flex-grow">
139143
<canvas

client/src/components/sidebar/game/game.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,22 @@ import { useAppContext } from '../../../app-context'
77
import { SectionHeader } from '../../section-header'
88
import { Crown } from '../../../icons/crown'
99
import { EventType, useListenEvent } from '../../../app-events'
10+
import Tooltip from '../../tooltip'
1011

1112
const NO_GAME_TEAM_NAME = '?????'
1213

1314
interface Props {
1415
open: boolean
1516
}
1617

18+
const CrownElement = () => {
19+
return (
20+
<Tooltip text={'Majority match winner'}>
21+
<Crown className="ml-2 mt-1" />
22+
</Tooltip>
23+
)
24+
}
25+
1726
export const GamePage: React.FC<Props> = (props) => {
1827
const context = useAppContext()
1928
const activeGame = context.state.activeGame
@@ -39,9 +48,7 @@ export const GamePage: React.FC<Props> = (props) => {
3948
<div className={teamBoxClasses + ' bg-team0'}>
4049
<p className="flex">
4150
{activeGame?.teams[0].name ?? NO_GAME_TEAM_NAME}
42-
{activeGame && activeGame.winner === activeGame.teams[0] && showWinner && (
43-
<Crown className="ml-2 mt-1" />
44-
)}
51+
{activeGame && activeGame.winner === activeGame.teams[0] && showWinner && <CrownElement />}
4552
</p>
4653
</div>
4754
<TeamTable teamIdx={0} />
@@ -51,9 +58,7 @@ export const GamePage: React.FC<Props> = (props) => {
5158
<div className={teamBoxClasses + ' bg-team1'}>
5259
<p className="flex">
5360
{activeGame?.teams[1].name ?? NO_GAME_TEAM_NAME}
54-
{activeGame && activeGame.winner === activeGame.teams[1] && showWinner && (
55-
<Crown className="ml-2 mt-1" />
56-
)}
61+
{activeGame && activeGame.winner === activeGame.teams[1] && showWinner && <CrownElement />}
5762
</p>
5863
</div>
5964
<TeamTable teamIdx={1} />
Lines changed: 101 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,120 @@
11
import React from 'react'
2+
import { SectionHeader } from '../../section-header'
3+
import { BATTLECODE_YEAR } from '../../../constants'
4+
5+
enum TabType {
6+
NONE = '',
7+
OVERVIEW = 'Overview',
8+
GAME = 'Game Tab',
9+
RUNNER = 'Runner Tab',
10+
HOTKEYS = 'Hotkeys'
11+
}
212

313
interface Props {
414
open: boolean
515
}
616

717
export const HelpPage: React.FC<Props> = (props) => {
18+
const [openTabType, setOpenTabType] = React.useState(TabType.OVERVIEW)
19+
20+
const toggleTab = (newType: TabType) => {
21+
setOpenTabType(newType == openTabType ? TabType.NONE : newType)
22+
}
23+
824
const hotkeyElement = (key: string, description: string) => {
925
return (
10-
<div className="font-light">
11-
<b>{key}</b> - {description}
26+
<div>
27+
<div className="font-bold">{key}</div>
28+
<div>{description}</div>
1229
</div>
1330
)
1431
}
1532

1633
if (!props.open) return null
1734

35+
const sections: Record<TabType, JSX.Element> = {
36+
[TabType.NONE]: <></>,
37+
[TabType.OVERVIEW]: (
38+
<>
39+
<div>
40+
{`Welcome to the Battlecode ${BATTLECODE_YEAR} client! `}
41+
{`We've completely overhauled the client this year, and we hope it is a better experience overall. `}
42+
{`As such, there may be issues, so please let us know if you come across anything at all. `}
43+
{`On this page, you will find some basic information about some of the more complex features of the client. `}
44+
{`If anything is confusing or you have other questions, feel free to ask. `}
45+
<b>{`NOTE: If you are experiencing performance issues on Mac or a laptop, turn off low power mode. `}</b>
46+
</div>
47+
</>
48+
),
49+
[TabType.GAME]: (
50+
<>
51+
<div>
52+
{`The game page is where you will visualize the stats for a game. `}
53+
{`Each statistic is specific to a match, and a game may contain multiple matches. `}
54+
{`The crown that appears above one team indicates who has won the majority of matches within a game. `}
55+
{`Each duck indicates how many ducks currently exist of that type for each team. The first duck is `}
56+
{`the standard duck, and the next three are the ducks that have specialized to level four and above. `}
57+
{`Red is attack, purple is build, and yellow is heal. The final caged duck represents how many ducks `}
58+
{`are in jail. `}
59+
{`Finally, the flags represent how many flags each team has that have not been captured. If a flag is `}
60+
{`outlined red, it means the flag is currently being carried. `}
61+
</div>
62+
</>
63+
),
64+
[TabType.RUNNER]: (
65+
<>
66+
<div>
67+
{`The runner is an easy way to run games from within the client. `}
68+
{`To get started, make sure you are running the desktop version of the client. `}
69+
{`Then, select the root folder of the scaffold (battlecode${BATTLECODE_YEAR}-scaffold). `}
70+
{`Once you do that, you should see all of your maps and robots loaded in automatically. `}
71+
{`Before you run a game, ensure that your JDK installation has been correctly set up. `}
72+
{`The runner will attempt to detect the correct version of the JDK and display it in the `}
73+
{`dropdown. However, if no versions are listed and the 'Auto' setting does not work, you will `}
74+
{`have to manually customize the path to your JDK installation. `}
75+
{`Once everything is working, you'll be able to run games from within the client, and the `}
76+
{`client will automatically load the game to be visualized once it is complete. `}
77+
</div>
78+
</>
79+
),
80+
[TabType.HOTKEYS]: (
81+
<div className="flex flex-col gap-[10px]">
82+
{hotkeyElement(`Space`, 'Pauses / Unpauses game')}
83+
{hotkeyElement(
84+
`LeftArrow and RightArrow`,
85+
'Controls speed if game is unpaused, or moves the current round if paused'
86+
)}
87+
{hotkeyElement(`\` and 1`, 'Scroll through Game, Runner, and Queue')}
88+
{/*
89+
{hotkeyElement(
90+
`Shift`,
91+
'Switches to Queue tab. If you are already on it, prompts you to select a replay file'
92+
)}
93+
*/}
94+
{hotkeyElement(`Shift`, 'If you are on the queue tab, prompts you to select a replay file')}
95+
{hotkeyElement(`C`, 'Hides and Unhides Game control bar')}
96+
{hotkeyElement(`.`, 'Skip to the very last turn of the current game')}
97+
{hotkeyElement(`,`, 'Skip to the first turn of the current game')}
98+
</div>
99+
)
100+
}
101+
18102
return (
19-
<div>
20-
Shortcuts: <br />
21-
<br />
22-
{hotkeyElement(`Space`, 'Pauses / Unpauses game')}
23-
<br />
24-
{hotkeyElement(
25-
`LeftArrow and RightArrow`,
26-
'Controls speed if game is unpaused, or moves the current round if paused'
27-
)}
28-
<br />
29-
{hotkeyElement(`~ and 1`, 'Scroll through Game, Runner, and Queue')}
30-
<br />
31-
{/*
32-
{hotkeyElement(
33-
`Shift`,
34-
'Switches to Queue tab. If you are already on it, prompts you to select a replay file'
35-
)}
36-
<br />
37-
*/}
38-
{hotkeyElement(`Shift`, 'If you are on the queue tab, prompts you to select a replay file')}
39-
<br />
40-
{hotkeyElement(`C`, 'Hides and Unhides Game control bar')}
41-
<br />
42-
{hotkeyElement(`.`, 'Skip to the very last turn of the current game')}
43-
<br />
44-
{hotkeyElement(`,`, 'Skip to the first turn of the current game')}
103+
<div className="pb-5">
104+
{Object.getOwnPropertyNames(sections).map((tabType) => {
105+
if (tabType == TabType.NONE) return null
106+
return (
107+
<SectionHeader
108+
key={tabType}
109+
title={tabType}
110+
open={tabType == openTabType}
111+
onClick={() => toggleTab(tabType as TabType)}
112+
titleClassName="py-2"
113+
>
114+
<div className="pl-3 text-xs">{sections[tabType as TabType]}</div>
115+
</SectionHeader>
116+
)
117+
})}
45118
</div>
46119
)
47120
}

client/src/components/sidebar/queue/queue-game.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Match from '../../../playback/Match'
44
import { useAppContext } from '../../../app-context'
55
import { IconContext } from 'react-icons'
66
import { IoCloseCircle, IoCloseCircleOutline } from 'react-icons/io5'
7+
import { schema } from 'battlecode-schema'
78

89
interface Props {
910
game: Game
@@ -33,6 +34,25 @@ export const QueuedGame: React.FC<Props> = (props) => {
3334
})
3435
}
3536

37+
const getWinText = (winType: schema.WinType) => {
38+
switch (winType) {
39+
case schema.WinType.CAPTURE:
40+
return 'by capturing all flags '
41+
case schema.WinType.MORE_FLAG_CAPTURES:
42+
return 'with more captured flags '
43+
case schema.WinType.LEVEL_SUM:
44+
return 'with a higher level sum '
45+
case schema.WinType.MORE_BREAD:
46+
return 'with a higher crumb count '
47+
case schema.WinType.COIN_FLIP:
48+
return 'by coin flip '
49+
case schema.WinType.RESIGNATION:
50+
return 'by resignation '
51+
default:
52+
return ''
53+
}
54+
}
55+
3656
return (
3757
<div className="relative mr-auto rounded-md bg-lightCard border-gray-500 border mb-4 p-3 w-full shadow-md">
3858
<div className="text-xs whitespace mb-2 overflow-ellipsis overflow-hidden">
@@ -55,7 +75,7 @@ export const QueuedGame: React.FC<Props> = (props) => {
5575
<span className="text-xxs leading-tight">
5676
<span className="mx-1">-</span>
5777
<span className={`font-bold text-team${match.winner.id - 1}`}>{match.winner.name}</span>
58-
<span>{` wins after ${match.maxTurn} rounds`}</span>
78+
<span>{` wins ${getWinText(match.winType)}after ${match.maxTurn} rounds`}</span>
5979
</span>
6080
)}
6181
</p>

client/src/components/sidebar/runner/runner.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ const MapSelector: React.FC<MapSelectorProps> = ({ maps, availableMaps, onSelect
205205
onClick={() => (maps.has(m) ? onDeselect(m) : onSelect(m))}
206206
>
207207
{m}
208-
<input type={'checkbox'} checked={selected} className="pointer-events-none mr-2" />
208+
<input type={'checkbox'} readOnly checked={selected} className="pointer-events-none mr-2" />
209209
</div>
210210
)
211211
})}
@@ -263,7 +263,7 @@ const JavaSelector: React.FC<JavaSelectorProps> = (props) => {
263263
open={selectPath}
264264
onClose={closeDialog}
265265
title="Custom Java Path"
266-
description="Enter the Java path (should end with /Home)"
266+
description="Enter the Java path (should end with /Home on Mac/Linux, root path otherwise)"
267267
placeholder="Path..."
268268
/>
269269
</>

client/src/components/sidebar/runner/scaffold.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,13 @@ async function fetchData(scaffoldPath: string) {
175175
file.endsWith('RobotPlayer.scala')
176176
)
177177
.map(async (file) => {
178-
const relPath = await path.relative(sourcePath, file)
179-
const botName = relPath.split(sep)[0] // Name of folder
178+
// Relative path will contain the folder and filename, so we can split on the separator
179+
// to get the folder name. We must first normalize the path to have forward slashes in the
180+
// case of windows so the relative path function works correctly
181+
const p1 = sourcePath.replace(/\\/g, '/')
182+
const p2 = file.replace(/\\/g, '/')
183+
const relPath = (await path.relative(p1, p2)).replace(/\\/g, '/')
184+
const botName = relPath.split('/')[0]
180185
return botName
181186
})
182187
)

client/src/components/sidebar/sidebar.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { useAppContext } from '../../app-context'
1717
import { useScaffold } from './runner/scaffold'
1818
import { ConfigPage } from '../../client-config'
1919
import { UpdateWarning } from './update-warning'
20+
import Game from '../../playback/Game'
2021

2122
export const Sidebar: React.FC = () => {
2223
const { width, height } = useWindowDimensions()
@@ -65,6 +66,41 @@ export const Sidebar: React.FC = () => {
6566
}, [tournamentSource])
6667
// End tournament mode loading ====================================================================================================
6768

69+
// Remote game loading ====================================================================================================
70+
const [gameSource, setGameSource] = useSearchParamString('gameSource', '')
71+
const fetchRemoteGame = (gameSource: string) => {
72+
fetch(gameSource)
73+
.then((response) => response.arrayBuffer())
74+
.then((buffer) => {
75+
if (buffer.byteLength === 0) {
76+
alert('Error: Game file is empty.')
77+
return
78+
}
79+
80+
const loadedGame = Game.loadFullGameRaw(buffer)
81+
context.setState({
82+
...context.state,
83+
activeGame: loadedGame,
84+
activeMatch: loadedGame.currentMatch,
85+
queue: context.state.queue.concat([loadedGame]),
86+
loadingRemoteContent: false
87+
})
88+
89+
setPage(PageType.GAME)
90+
})
91+
}
92+
React.useEffect(() => {
93+
if (gameSource) {
94+
context.setState({
95+
...context.state,
96+
loadingRemoteContent: true
97+
})
98+
fetchRemoteGame(gameSource)
99+
setPage(PageType.GAME)
100+
}
101+
}, [gameSource])
102+
// End remote game loading ====================================================================================================
103+
68104
// Skip going through map and help tab, it's annoying for competitors.
69105
const hotkeyPageLoop = tournamentMode
70106
? [PageType.GAME, PageType.QUEUE, PageType.TOURNAMENT]

0 commit comments

Comments
 (0)