Skip to content

Commit

Permalink
Merge pull request #70 from battlecode/client
Browse files Browse the repository at this point in the history
Client 2024
  • Loading branch information
TheApplePieGod authored Jan 8, 2024
2 parents d73b0da + 3b0014e commit 6467db1
Show file tree
Hide file tree
Showing 13 changed files with 230 additions and 57 deletions.
7 changes: 4 additions & 3 deletions client/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,10 @@ async fn tauri_api(
Ok(vec!(final_path.to_str().unwrap().to_string()))
},
"path.relative" => {
let final_path = RelativePath::new(&args[0])
.relative(&args[1]);
Ok(vec!(final_path.to_string()))
let path_from = RelativePath::new(&args[0]);
let path_to = RelativePath::new(&args[1]);
let result = path_from.relative(path_to);
Ok(vec!(result.to_string()))
},
"path.dirname" => {
let path = Path::new(&args[0]).parent().unwrap_or(Path::new(""));
Expand Down
2 changes: 2 additions & 0 deletions client/src/app-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface AppState {
activeGame: Game | undefined
activeMatch: Match | undefined
tournament: Tournament | undefined
loadingRemoteContent: boolean
updatesPerSecond: number
paused: boolean
disableHotkeys: boolean
Expand All @@ -20,6 +21,7 @@ const DEFAULT_APP_STATE: AppState = {
activeGame: undefined,
activeMatch: undefined,
tournament: undefined,
loadingRemoteContent: false,
updatesPerSecond: 1,
paused: true,
disableHotkeys: false,
Expand Down
6 changes: 4 additions & 2 deletions client/src/client-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ const DEFAULT_CONFIG = {
showAllIndicators: false,
showAllRobotRadii: false,
showHealthBars: false,
showMapXY: true
showMapXY: true,
showFlagCarryIndicator: true
}

const configDescription: { [key: string]: string } = {
showAllIndicators: 'Show all indicator dots and lines',
showAllRobotRadii: 'Show all robot view and attack radii',
showHealthBars: 'Show health bars below all robots',
showMapXY: 'Show X,Y when hovering a tile'
showMapXY: 'Show X,Y when hovering a tile',
showFlagCarryIndicator: 'Show an obvious indicator over flag carriers'
}

export function getDefaultConfig(): ClientConfig {
Expand Down
6 changes: 5 additions & 1 deletion client/src/components/game/game-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,11 @@ export const GameRenderer: React.FC = () => {
return (
<div className="w-full h-screen flex items-center justify-center">
{!activeMatch ? (
<p className="text-white text-center">Select a game from the queue</p>
appContext.state.loadingRemoteContent ? (
<p className="text-white text-center">Loading remote game...</p>
) : (
<p className="text-white text-center">Select a game from the queue</p>
)
) : (
<div ref={wrapperRef} className="relative max-w-full max-h-full flex-grow">
<canvas
Expand Down
17 changes: 11 additions & 6 deletions client/src/components/sidebar/game/game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@ import { useAppContext } from '../../../app-context'
import { SectionHeader } from '../../section-header'
import { Crown } from '../../../icons/crown'
import { EventType, useListenEvent } from '../../../app-events'
import Tooltip from '../../tooltip'

const NO_GAME_TEAM_NAME = '?????'

interface Props {
open: boolean
}

const CrownElement = () => {
return (
<Tooltip text={'Majority match winner'}>
<Crown className="ml-2 mt-1" />
</Tooltip>
)
}

export const GamePage: React.FC<Props> = (props) => {
const context = useAppContext()
const activeGame = context.state.activeGame
Expand All @@ -39,9 +48,7 @@ export const GamePage: React.FC<Props> = (props) => {
<div className={teamBoxClasses + ' bg-team0'}>
<p className="flex">
{activeGame?.teams[0].name ?? NO_GAME_TEAM_NAME}
{activeGame && activeGame.winner === activeGame.teams[0] && showWinner && (
<Crown className="ml-2 mt-1" />
)}
{activeGame && activeGame.winner === activeGame.teams[0] && showWinner && <CrownElement />}
</p>
</div>
<TeamTable teamIdx={0} />
Expand All @@ -51,9 +58,7 @@ export const GamePage: React.FC<Props> = (props) => {
<div className={teamBoxClasses + ' bg-team1'}>
<p className="flex">
{activeGame?.teams[1].name ?? NO_GAME_TEAM_NAME}
{activeGame && activeGame.winner === activeGame.teams[1] && showWinner && (
<Crown className="ml-2 mt-1" />
)}
{activeGame && activeGame.winner === activeGame.teams[1] && showWinner && <CrownElement />}
</p>
</div>
<TeamTable teamIdx={1} />
Expand Down
129 changes: 101 additions & 28 deletions client/src/components/sidebar/help/help.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,120 @@
import React from 'react'
import { SectionHeader } from '../../section-header'
import { BATTLECODE_YEAR } from '../../../constants'

enum TabType {
NONE = '',
OVERVIEW = 'Overview',
GAME = 'Game Tab',
RUNNER = 'Runner Tab',
HOTKEYS = 'Hotkeys'
}

interface Props {
open: boolean
}

export const HelpPage: React.FC<Props> = (props) => {
const [openTabType, setOpenTabType] = React.useState(TabType.OVERVIEW)

const toggleTab = (newType: TabType) => {
setOpenTabType(newType == openTabType ? TabType.NONE : newType)
}

const hotkeyElement = (key: string, description: string) => {
return (
<div className="font-light">
<b>{key}</b> - {description}
<div>
<div className="font-bold">{key}</div>
<div>{description}</div>
</div>
)
}

if (!props.open) return null

const sections: Record<TabType, JSX.Element> = {
[TabType.NONE]: <></>,
[TabType.OVERVIEW]: (
<>
<div>
{`Welcome to the Battlecode ${BATTLECODE_YEAR} client! `}
{`We've completely overhauled the client this year, and we hope it is a better experience overall. `}
{`As such, there may be issues, so please let us know if you come across anything at all. `}
{`On this page, you will find some basic information about some of the more complex features of the client. `}
{`If anything is confusing or you have other questions, feel free to ask. `}
<b>{`NOTE: If you are experiencing performance issues on Mac or a laptop, turn off low power mode. `}</b>
</div>
</>
),
[TabType.GAME]: (
<>
<div>
{`The game page is where you will visualize the stats for a game. `}
{`Each statistic is specific to a match, and a game may contain multiple matches. `}
{`The crown that appears above one team indicates who has won the majority of matches within a game. `}
{`Each duck indicates how many ducks currently exist of that type for each team. The first duck is `}
{`the standard duck, and the next three are the ducks that have specialized to level four and above. `}
{`Red is attack, purple is build, and yellow is heal. The final caged duck represents how many ducks `}
{`are in jail. `}
{`Finally, the flags represent how many flags each team has that have not been captured. If a flag is `}
{`outlined red, it means the flag is currently being carried. `}
</div>
</>
),
[TabType.RUNNER]: (
<>
<div>
{`The runner is an easy way to run games from within the client. `}
{`To get started, make sure you are running the desktop version of the client. `}
{`Then, select the root folder of the scaffold (battlecode${BATTLECODE_YEAR}-scaffold). `}
{`Once you do that, you should see all of your maps and robots loaded in automatically. `}
{`Before you run a game, ensure that your JDK installation has been correctly set up. `}
{`The runner will attempt to detect the correct version of the JDK and display it in the `}
{`dropdown. However, if no versions are listed and the 'Auto' setting does not work, you will `}
{`have to manually customize the path to your JDK installation. `}
{`Once everything is working, you'll be able to run games from within the client, and the `}
{`client will automatically load the game to be visualized once it is complete. `}
</div>
</>
),
[TabType.HOTKEYS]: (
<div className="flex flex-col gap-[10px]">
{hotkeyElement(`Space`, 'Pauses / Unpauses game')}
{hotkeyElement(
`LeftArrow and RightArrow`,
'Controls speed if game is unpaused, or moves the current round if paused'
)}
{hotkeyElement(`\` and 1`, 'Scroll through Game, Runner, and Queue')}
{/*
{hotkeyElement(
`Shift`,
'Switches to Queue tab. If you are already on it, prompts you to select a replay file'
)}
*/}
{hotkeyElement(`Shift`, 'If you are on the queue tab, prompts you to select a replay file')}
{hotkeyElement(`C`, 'Hides and Unhides Game control bar')}
{hotkeyElement(`.`, 'Skip to the very last turn of the current game')}
{hotkeyElement(`,`, 'Skip to the first turn of the current game')}
</div>
)
}

return (
<div>
Shortcuts: <br />
<br />
{hotkeyElement(`Space`, 'Pauses / Unpauses game')}
<br />
{hotkeyElement(
`LeftArrow and RightArrow`,
'Controls speed if game is unpaused, or moves the current round if paused'
)}
<br />
{hotkeyElement(`~ and 1`, 'Scroll through Game, Runner, and Queue')}
<br />
{/*
{hotkeyElement(
`Shift`,
'Switches to Queue tab. If you are already on it, prompts you to select a replay file'
)}
<br />
*/}
{hotkeyElement(`Shift`, 'If you are on the queue tab, prompts you to select a replay file')}
<br />
{hotkeyElement(`C`, 'Hides and Unhides Game control bar')}
<br />
{hotkeyElement(`.`, 'Skip to the very last turn of the current game')}
<br />
{hotkeyElement(`,`, 'Skip to the first turn of the current game')}
<div className="pb-5">
{Object.getOwnPropertyNames(sections).map((tabType) => {
if (tabType == TabType.NONE) return null
return (
<SectionHeader
key={tabType}
title={tabType}
open={tabType == openTabType}
onClick={() => toggleTab(tabType as TabType)}
titleClassName="py-2"
>
<div className="pl-3 text-xs">{sections[tabType as TabType]}</div>
</SectionHeader>
)
})}
</div>
)
}
22 changes: 21 additions & 1 deletion client/src/components/sidebar/queue/queue-game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Match from '../../../playback/Match'
import { useAppContext } from '../../../app-context'
import { IconContext } from 'react-icons'
import { IoCloseCircle, IoCloseCircleOutline } from 'react-icons/io5'
import { schema } from 'battlecode-schema'

interface Props {
game: Game
Expand Down Expand Up @@ -33,6 +34,25 @@ export const QueuedGame: React.FC<Props> = (props) => {
})
}

const getWinText = (winType: schema.WinType) => {
switch (winType) {
case schema.WinType.CAPTURE:
return 'by capturing all flags '
case schema.WinType.MORE_FLAG_CAPTURES:
return 'with more captured flags '
case schema.WinType.LEVEL_SUM:
return 'with a higher level sum '
case schema.WinType.MORE_BREAD:
return 'with a higher crumb count '
case schema.WinType.COIN_FLIP:
return 'by coin flip '
case schema.WinType.RESIGNATION:
return 'by resignation '
default:
return ''
}
}

return (
<div className="relative mr-auto rounded-md bg-lightCard border-gray-500 border mb-4 p-3 w-full shadow-md">
<div className="text-xs whitespace mb-2 overflow-ellipsis overflow-hidden">
Expand All @@ -55,7 +75,7 @@ export const QueuedGame: React.FC<Props> = (props) => {
<span className="text-xxs leading-tight">
<span className="mx-1">-</span>
<span className={`font-bold text-team${match.winner.id - 1}`}>{match.winner.name}</span>
<span>{` wins after ${match.maxTurn} rounds`}</span>
<span>{` wins ${getWinText(match.winType)}after ${match.maxTurn} rounds`}</span>
</span>
)}
</p>
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/sidebar/runner/runner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ const MapSelector: React.FC<MapSelectorProps> = ({ maps, availableMaps, onSelect
onClick={() => (maps.has(m) ? onDeselect(m) : onSelect(m))}
>
{m}
<input type={'checkbox'} checked={selected} className="pointer-events-none mr-2" />
<input type={'checkbox'} readOnly checked={selected} className="pointer-events-none mr-2" />
</div>
)
})}
Expand Down Expand Up @@ -263,7 +263,7 @@ const JavaSelector: React.FC<JavaSelectorProps> = (props) => {
open={selectPath}
onClose={closeDialog}
title="Custom Java Path"
description="Enter the Java path (should end with /Home)"
description="Enter the Java path (should end with /Home on Mac/Linux, root path otherwise)"
placeholder="Path..."
/>
</>
Expand Down
9 changes: 7 additions & 2 deletions client/src/components/sidebar/runner/scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,13 @@ async function fetchData(scaffoldPath: string) {
file.endsWith('RobotPlayer.scala')
)
.map(async (file) => {
const relPath = await path.relative(sourcePath, file)
const botName = relPath.split(sep)[0] // Name of folder
// Relative path will contain the folder and filename, so we can split on the separator
// to get the folder name. We must first normalize the path to have forward slashes in the
// case of windows so the relative path function works correctly
const p1 = sourcePath.replace(/\\/g, '/')
const p2 = file.replace(/\\/g, '/')
const relPath = (await path.relative(p1, p2)).replace(/\\/g, '/')
const botName = relPath.split('/')[0]
return botName
})
)
Expand Down
36 changes: 36 additions & 0 deletions client/src/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useAppContext } from '../../app-context'
import { useScaffold } from './runner/scaffold'
import { ConfigPage } from '../../client-config'
import { UpdateWarning } from './update-warning'
import Game from '../../playback/Game'

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

// Remote game loading ====================================================================================================
const [gameSource, setGameSource] = useSearchParamString('gameSource', '')
const fetchRemoteGame = (gameSource: string) => {
fetch(gameSource)
.then((response) => response.arrayBuffer())
.then((buffer) => {
if (buffer.byteLength === 0) {
alert('Error: Game file is empty.')
return
}

const loadedGame = Game.loadFullGameRaw(buffer)
context.setState({
...context.state,
activeGame: loadedGame,
activeMatch: loadedGame.currentMatch,
queue: context.state.queue.concat([loadedGame]),
loadingRemoteContent: false
})

setPage(PageType.GAME)
})
}
React.useEffect(() => {
if (gameSource) {
context.setState({
...context.state,
loadingRemoteContent: true
})
fetchRemoteGame(gameSource)
setPage(PageType.GAME)
}
}, [gameSource])
// End remote game loading ====================================================================================================

// Skip going through map and help tab, it's annoying for competitors.
const hotkeyPageLoop = tournamentMode
? [PageType.GAME, PageType.QUEUE, PageType.TOURNAMENT]
Expand Down
Loading

0 comments on commit 6467db1

Please sign in to comment.