Skip to content

Commit

Permalink
Add Optional Coordinate Labels to the Grid (#71)
Browse files Browse the repository at this point in the history
* Get setting piped through Local storage

We have a checkbox on the settings page that can round trip through
Local Storage nicely

Also touched up the README a bit, since I missed the part about the port
at first so moved added it to the sentence I was reading

* Add Coordinate Labels

First we adjust the size of the `Grid` we are going to render within `Board`

If we have the `showCoordinateLabels` option enabled, make the `Grid` inside
smaller, so that we have room for the labels.

The labels are rendered inside `Grid` but OUTSIDE it's SVGs bounding box.
We allow the SVG to overflow so that the labels are visible, when the option
is set.
This allowed us to use `-1` for the row/col where the labels went and means
we didn't need to modify the rest of the grid generation for this PR.

To get the text centered nicely in the cell, we use a `foreignObject`
and HTML + CSS. SVGs don't seem to support text styling as well as HTML,
and foreignObject seems supported in all non-IE browsers
  • Loading branch information
coreyja authored Mar 21, 2023
1 parent b5619af commit cecb3f6
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 14 deletions.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Battlesnake Board

![CI Build Status](https://github.com/BattlesnakeOfficial/board/actions/workflows/ci.yml/badge.svg) ![Release Build Status](https://github.com/BattlesnakeOfficial/board/actions/workflows/release.yml/badge.svg)

The board project is used to display Battlesnake games, both during live streams and competitions, as well as on [play.battlesnake.com](https://play.battlesnake.com/). It's built using React, HTML Canvas, and SVGs.
Expand All @@ -11,14 +12,16 @@ This project follows most React conventions and tools described in the react doc

This project requires Node 10.19. The dependencies may fail to install on newer versions.

Create a `.env.local` file at the root of the project to set the host that the local board URL serves from. `localhost` is the default but will not work with the CORS policy for snake part svg files. `127.0.0.1` is whitelisted for CORS.
Create a `.env.local` file at the root of the project to set the host that the local board URL serves from. `localhost` is the default but will not work with the CORS policy for snake part svg files.
`127.0.0.1:3000` is whitelisted for CORS.

```shell
# File: /.env.local
HOST=127.0.0.1
```

### Install & Run

```shell
# Installs dependencies from package-lock.json
npm ci
Expand All @@ -41,14 +44,16 @@ The game board requires a few parameters to work, including a `game` ID and an `

## Running tests

React will run tests locally in watch mode. More info: https://create-react-app.dev/docs/running-tests/#command-line-interface
React will run tests locally in watch mode. More info: <https://create-react-app.dev/docs/running-tests/#command-line-interface>

```shell
npm test
```

## Board parameters

#### Required

- `engine` - the Battlesnake engine to request frames from.
- `game` - the id of the game to fetch frames for.

Expand All @@ -57,6 +62,7 @@ http://127.0.0.1:3000/?engine=[ENGINE_URL]&game=[GAME_ID]
```

#### Optional

- `autoplay` - start game playback immediately. Values true / false. Defaults to false.
- `boardTheme` - the theme of the board. Values dark / light. Defaults to light.
- `frameRate` - the maximum frame rate used for playback. Takes an integer value equal to FPS. Defaults to 6 FPS. (medium speed)
Expand Down Expand Up @@ -87,10 +93,10 @@ More info on setting up in popular editors here: [create-react-app.dev/docs/sett

We use [Storybook.js](https://storybook.js.org/) to document and test the board components.

You can view and interact with board components here https://battlesnakeofficial.github.io/board/
You can view and interact with board components here <https://battlesnakeofficial.github.io/board/>

While developing a component you can run a local copy of storybook with the command `npm run storybook` to view and test it

## Feedback

* **Do you have an issue or suggestions for this repository?** Head over to our [Feedback Repository](https://play.battlesnake.com/feedback) today and let us know!
- **Do you have an issue or suggestions for this repository?** Head over to our [Feedback Repository](https://play.battlesnake.com/feedback) today and let us know!
11 changes: 10 additions & 1 deletion src/app/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export function rehydrateLocalSettings() {
let checkAutoplay = getLocalSetting("autoplay");
let checkShowFrameScrubber = getLocalSetting("showFrameScrubber");

let showCoordinateLabels = getLocalSetting("showCoordinateLabels");

if (typeof checkAutoplay === "undefined") {
checkAutoplay = initialSettings.autoplay;
} else {
Expand All @@ -58,10 +60,17 @@ export function rehydrateLocalSettings() {
checkShowFrameScrubber = checkShowFrameScrubber === "true";
}

if (typeof showCoordinateLabels === "undefined") {
showCoordinateLabels = initialSettings.showCoordinateLabels;
} else {
showCoordinateLabels = showCoordinateLabels === "true";
}

return {
frameRate: Number(getLocalSetting("frameRate")) || DEFAULT_FRAMERATE,
theme: getLocalSetting("theme") || initialSettings.theme,
showFrameScrubber: checkShowFrameScrubber,
autoplay: checkAutoplay
autoplay: checkAutoplay,
showCoordinateLabels: showCoordinateLabels
};
}
15 changes: 12 additions & 3 deletions src/components/board.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@ import Grid from "./grid";

const BOARD_SIZE = 100;

const LABEL_ADJUSTMENT = 10;

class Board extends React.Component {
render() {
const boardSize = this.props.showCoordinateLabels
? BOARD_SIZE - LABEL_ADJUSTMENT
: BOARD_SIZE;

const x = this.props.showCoordinateLabels ? LABEL_ADJUSTMENT : 0;

return (
<svg viewBox={`0 0 ${BOARD_SIZE} ${BOARD_SIZE}`}>
<Grid
Expand All @@ -16,11 +24,12 @@ class Board extends React.Component {
rows={this.props.rows}
highlightedSnake={this.props.highlightedSnake}
theme={this.props.theme}
maxWidth={BOARD_SIZE}
maxHeight={BOARD_SIZE}
x={0}
maxWidth={boardSize}
maxHeight={boardSize}
x={x}
y={0}
turn={this.props.turn}
showCoordinateLabels={this.props.showCoordinateLabels}
/>
</svg>
);
Expand Down
1 change: 1 addition & 0 deletions src/components/game.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ class Game extends React.Component {
highlightedSnake={this.props.highlightedSnake}
theme={options.theme}
turn={currentFrame.turn}
showCoordinateLabels={options.showCoordinateLabels}
/>
<MediaControls
currentFrame={currentFrame}
Expand Down
44 changes: 41 additions & 3 deletions src/components/grid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,43 @@ class Grid extends React.Component {
);
}

renderLabel(row, col, label) {
return (
<foreignObject
key={"label" + row + col}
x={toGridSpaceX(col)}
y={toGridSpaceY(row)}
width={CELL_SIZE}
height={CELL_SIZE}
>
<div
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity: 0.5
}}
>
<div style={{ fontSize: "80%" }}>{label}</div>
</div>
</foreignObject>
);
}

renderLabels() {
return (
<>
{range(this.props.rows).map((_, row) => this.renderLabel(row, -1, row))}

{range(this.props.columns).map((_, col) =>
this.renderLabel(-1, col, col)
)}
</>
);
}

renderGrid() {
// GRID_COLUMNS = this.props.columns;
GRID_ROWS = this.props.rows;
Expand Down Expand Up @@ -506,6 +543,8 @@ class Grid extends React.Component {

const hazardOpacity = parseFloat(colors.hazardOpacity);

const overflow = this.props.showCoordinateLabels ? "visible" : "hidden";

return (
<svg
className="grid"
Expand All @@ -514,7 +553,9 @@ class Grid extends React.Component {
x={this.props.x}
y={this.props.y}
viewBox={`0 0 ${viewBoxWidth} ${viewBoxHeight}`}
overflow={overflow}
>
{this.props.showCoordinateLabels && this.renderLabels()}
{range(this.props.rows).map((_, row) =>
range(this.props.columns).map((_, col) => (
<rect
Expand All @@ -532,7 +573,6 @@ class Grid extends React.Component {
/>
))
)}

{sortedSnakes.map((snake, snakeIndex) => {
return (
<g
Expand All @@ -554,7 +594,6 @@ class Grid extends React.Component {
</g>
);
})}

{hazards.map((o, hazardIndex) => (
<rect
key={"hazard" + hazardIndex}
Expand All @@ -567,7 +606,6 @@ class Grid extends React.Component {
shapeRendering="auto"
/>
))}

{food.map((f, foodIndex) => {
if (this.props.foodImage) {
return (
Expand Down
33 changes: 32 additions & 1 deletion src/components/settings/SettingsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
currentShowFrameScrubber,
frameRateUpdated,
themeSelected,
showFrameScrubberUpdated
showFrameScrubberUpdated,
currentShowCoordinateLabels,
showCoordinateLabelsUpdated
} from "./settings-slice";
import PlaybackSpeed from "./playback/PlaybackSpeed";
import styles from "./SettingsPage.module.css";
Expand All @@ -21,6 +23,7 @@ const SettingsPage = () => {
const playbackSpeed = useSelector(currentFrameRate);
const autoplay = useSelector(currentAutoplay);
const showFrameScrubber = useSelector(currentShowFrameScrubber);
const showCoordinateLabels = useSelector(currentShowCoordinateLabels);
const dispatch = useDispatch();

useEffect(() => {
Expand Down Expand Up @@ -128,6 +131,34 @@ const SettingsPage = () => {
</label>
</div>
</fieldset>
<fieldset>
<legend>Show Coordinate Labels [EXPERIMENTAL]</legend>
<div className={styles.info}>
Adds coordinate labels to the game board. &nbsp; These go from 0 to
the width/height of the board to make it easier to debug games
<a
href="https://github.com/orgs/BattlesnakeOfficial/discussions/231"
target="_blank"
rel="noreferrer"
>
Let us know what you think!
</a>
</div>
<div className={styles.inputContainer}>
<label>
<input
type="checkbox"
name="showCoordinateLabels"
checked={showCoordinateLabels}
onChange={e =>
dispatch(showCoordinateLabelsUpdated(e.target.checked))
}
/>
<span className={styles.checkmark} />
{showCoordinateLabels ? "On" : "Off"}
</label>
</div>
</fieldset>
<fieldset className={styles.centered}>
<button onClick={() => history.goBack()}>Return to Game</button>
</fieldset>
Expand Down
3 changes: 2 additions & 1 deletion src/components/settings/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export const initialSettings = {
theme: themes.light,
autoplay: false,
showFrameScrubber: false,
persistAvailable: false
persistAvailable: false,
showCoordinateLabels: false
};
18 changes: 17 additions & 1 deletion src/components/settings/settings-slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export const settingsSlice = createSlice({
},
showFrameScrubberUpdated(state, action) {
state.showFrameScrubber = action.payload;
},
showCoordinateLabelsUpdated(state, action) {
state.showCoordinateLabels = action.payload;
}
}
});
Expand All @@ -26,7 +29,8 @@ export const {
frameRateUpdated,
themeSelected,
autoPlayUpdated,
showFrameScrubberUpdated
showFrameScrubberUpdated,
showCoordinateLabelsUpdated
} = settingsSlice.actions;

// The function below is called a selector and allows us to select a value from
Expand All @@ -37,6 +41,8 @@ export const currentTheme = state => state.settings.theme;
export const currentAutoplay = state => state.settings.autoplay;
export const currentShowFrameScrubber = state =>
state.settings.showFrameScrubber;
export const currentShowCoordinateLabels = state =>
state.settings.showCoordinateLabels;

export function settingsStoreListener(state) {
if (state.settings.frameRate !== getLocalSetting("frameRate")) {
Expand All @@ -56,6 +62,16 @@ export function settingsStoreListener(state) {
) {
setLocalSetting("showFrameScrubber", state.settings.showFrameScrubber);
}

if (
state.settings.showCoordinateLabels !==
getLocalSetting("showCoordinateLabels")
) {
setLocalSetting(
"showCoordinateLabels",
state.settings.showCoordinateLabels
);
}
}

export default settingsSlice.reducer;

0 comments on commit cecb3f6

Please sign in to comment.