From cecb3f62941a2ae44d7fbd095dea192acb41190f Mon Sep 17 00:00:00 2001 From: Corey Alexander Date: Tue, 21 Mar 2023 10:30:48 -0400 Subject: [PATCH] Add Optional Coordinate Labels to the Grid (#71) * 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 --- README.md | 14 +++++--- src/app/storage.js | 11 +++++- src/components/board.jsx | 15 ++++++-- src/components/game.jsx | 1 + src/components/grid.jsx | 44 +++++++++++++++++++++-- src/components/settings/SettingsPage.jsx | 33 ++++++++++++++++- src/components/settings/defaults.js | 3 +- src/components/settings/settings-slice.js | 18 +++++++++- 8 files changed, 125 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index cfba237..140d093 100644 --- a/README.md +++ b/README.md @@ -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. @@ -11,7 +12,8 @@ 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 @@ -19,6 +21,7 @@ HOST=127.0.0.1 ``` ### Install & Run + ```shell # Installs dependencies from package-lock.json npm ci @@ -41,7 +44,8 @@ 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: + ```shell npm test ``` @@ -49,6 +53,7 @@ npm test ## Board parameters #### Required + - `engine` - the Battlesnake engine to request frames from. - `game` - the id of the game to fetch frames for. @@ -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) @@ -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 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! diff --git a/src/app/storage.js b/src/app/storage.js index a59ce6d..e77a1c9 100644 --- a/src/app/storage.js +++ b/src/app/storage.js @@ -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 { @@ -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 }; } diff --git a/src/components/board.jsx b/src/components/board.jsx index 9b8bee1..3bc4566 100644 --- a/src/components/board.jsx +++ b/src/components/board.jsx @@ -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 ( ); diff --git a/src/components/game.jsx b/src/components/game.jsx index 7688374..7d2e86d 100644 --- a/src/components/game.jsx +++ b/src/components/game.jsx @@ -150,6 +150,7 @@ class Game extends React.Component { highlightedSnake={this.props.highlightedSnake} theme={options.theme} turn={currentFrame.turn} + showCoordinateLabels={options.showCoordinateLabels} /> +
+
{label}
+
+ + ); + } + + 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; @@ -506,6 +543,8 @@ class Grid extends React.Component { const hazardOpacity = parseFloat(colors.hazardOpacity); + const overflow = this.props.showCoordinateLabels ? "visible" : "hidden"; + return ( + {this.props.showCoordinateLabels && this.renderLabels()} {range(this.props.rows).map((_, row) => range(this.props.columns).map((_, col) => ( )) )} - {sortedSnakes.map((snake, snakeIndex) => { return ( ); })} - {hazards.map((o, hazardIndex) => ( ))} - {food.map((f, foodIndex) => { if (this.props.foodImage) { return ( diff --git a/src/components/settings/SettingsPage.jsx b/src/components/settings/SettingsPage.jsx index 73ac7f9..0881ab7 100644 --- a/src/components/settings/SettingsPage.jsx +++ b/src/components/settings/SettingsPage.jsx @@ -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"; @@ -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(() => { @@ -128,6 +131,34 @@ const SettingsPage = () => { +
+ Show Coordinate Labels [EXPERIMENTAL] +
+ Adds coordinate labels to the game board.   These go from 0 to + the width/height of the board to make it easier to debug games + + Let us know what you think! + +
+
+ +
+
diff --git a/src/components/settings/defaults.js b/src/components/settings/defaults.js index 5a5ebdf..4e4c799 100644 --- a/src/components/settings/defaults.js +++ b/src/components/settings/defaults.js @@ -7,5 +7,6 @@ export const initialSettings = { theme: themes.light, autoplay: false, showFrameScrubber: false, - persistAvailable: false + persistAvailable: false, + showCoordinateLabels: false }; diff --git a/src/components/settings/settings-slice.js b/src/components/settings/settings-slice.js index 0e222b1..d54e949 100644 --- a/src/components/settings/settings-slice.js +++ b/src/components/settings/settings-slice.js @@ -17,6 +17,9 @@ export const settingsSlice = createSlice({ }, showFrameScrubberUpdated(state, action) { state.showFrameScrubber = action.payload; + }, + showCoordinateLabelsUpdated(state, action) { + state.showCoordinateLabels = action.payload; } } }); @@ -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 @@ -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")) { @@ -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;