board.tsx

  1import { Button, createStyles, makeStyles, Theme, Typography } from '@material-ui/core';
  2import { grey, orange, red } from '@material-ui/core/colors';
  3import { Fireworks } from 'fireworks/lib/react';
  4import * as React from 'react';
  5import isEqual from 'react-fast-compare';
  6
  7import { isDefined, noop } from '../common';
  8import { StateBoard, StateTile } from '../protocol';
  9import { TeamHue, teamSpecs } from '../teams';
 10import { AspectDiv } from './aspectDiv';
 11
 12function neutralStyle(revealed: boolean, spymaster: boolean): React.CSSProperties {
 13    return {
 14        color: revealed ? 'white' : 'black',
 15        backgroundColor: grey[revealed ? 500 : 200],
 16        fontWeight: spymaster ? 'bold' : undefined,
 17    };
 18}
 19
 20function bombStyle(revealed: boolean, spymaster: boolean): React.CSSProperties {
 21    return {
 22        color: revealed ? 'white' : grey[900],
 23        backgroundColor: grey[revealed ? 900 : 700],
 24        fontWeight: spymaster ? 'bold' : undefined,
 25    };
 26}
 27
 28function teamStyle(teamHue: TeamHue, revealed: boolean, spymaster: boolean): React.CSSProperties {
 29    return {
 30        color: revealed ? 'white' : teamHue[900],
 31        backgroundColor: teamHue[revealed ? 600 : 200],
 32        fontWeight: spymaster ? 'bold' : undefined,
 33    };
 34}
 35
 36function tileStyle(tile: StateTile, spymaster: boolean): React.CSSProperties {
 37    if (!isDefined(tile.view) || tile.view.neutral) {
 38        return neutralStyle(tile.revealed, spymaster);
 39    }
 40
 41    if (tile.view.bomb) {
 42        return bombStyle(tile.revealed, spymaster);
 43    }
 44
 45    const teamHue = teamSpecs[tile.view.team].hue;
 46    return teamStyle(teamHue, tile.revealed, spymaster);
 47}
 48
 49const useTileStyles = makeStyles((theme: Theme) =>
 50    createStyles({
 51        button: {
 52            position: 'absolute',
 53            top: 0,
 54            left: 0,
 55            height: '100%',
 56            width: '100%',
 57            textAlign: 'center',
 58            [theme.breakpoints.down('sm')]: {
 59                padding: '6px',
 60            },
 61        },
 62        typo: {
 63            wordWrap: 'break-word',
 64            width: '100%',
 65            fontSize: theme.typography.h6.fontSize,
 66            [theme.breakpoints.down('sm')]: {
 67                fontSize: theme.typography.button.fontSize,
 68                lineHeight: '1rem',
 69            },
 70        },
 71        explosionWrapper: {
 72            zIndex: 100,
 73            position: 'absolute',
 74            margin: 'auto',
 75            height: 0,
 76            width: 0,
 77            top: 0,
 78            left: 0,
 79            bottom: 0,
 80            right: 0,
 81        },
 82        explosion: {
 83            transform: 'translate(-50%, -50%)',
 84            pointerEvents: 'none',
 85        },
 86    })
 87);
 88
 89const fireworksProps = {
 90    interval: 0,
 91    colors: [red[700], orange[800], grey[500]],
 92    x: 0,
 93    y: 0,
 94};
 95
 96interface TileProps {
 97    row: number;
 98    col: number;
 99    onClick: (row: number, col: number) => void;
100    tile: StateTile;
101    spymaster: boolean;
102    myTurn: boolean;
103    winner: boolean;
104}
105
106const Tile = React.memo(function Tile({ row, col, onClick, tile, spymaster, myTurn, winner }: TileProps) {
107    const classes = useTileStyles();
108
109    const bombRevealed = !!(tile.revealed && tile.view?.bomb);
110    const alreadyExploded = React.useRef(bombRevealed);
111    const explode = bombRevealed && !alreadyExploded.current;
112    const disabled = spymaster || !myTurn || winner || tile.revealed;
113
114    const reveal = React.useMemo(() => {
115        if (disabled) {
116            return noop;
117        }
118        return () => onClick(row, col);
119    }, [disabled, row, col, onClick]);
120
121    return (
122        <AspectDiv aspectRatio="75%">
123            <Button
124                type="button"
125                variant="contained"
126                className={classes.button}
127                onClick={reveal}
128                style={tileStyle(tile, spymaster)}
129                disabled={disabled}
130            >
131                <Typography variant="h6" className={classes.typo}>
132                    {tile.word}
133                </Typography>
134            </Button>
135            {explode ? (
136                <div className={classes.explosionWrapper}>
137                    <div className={classes.explosion}>
138                        <Fireworks {...fireworksProps} />
139                    </div>
140                </div>
141            ) : null}
142        </AspectDiv>
143    );
144}, isEqual);
145
146export interface BoardProps {
147    words: StateBoard;
148    spymaster: boolean;
149    myTurn: boolean;
150    winner: boolean;
151    onClick: (row: number, col: number) => void;
152}
153
154const useStyles = makeStyles((theme: Theme) =>
155    createStyles({
156        root: {
157            display: 'grid',
158            gridGap: theme.spacing(0.5),
159            [theme.breakpoints.up('lg')]: {
160                gridGap: theme.spacing(1),
161            },
162            gridTemplateRows: (props: BoardProps) => `repeat(${props.words.length}, 1fr)`,
163            gridTemplateColumns: (props: BoardProps) => `repeat(${props.words[0].length}, 1fr)`,
164        },
165    })
166);
167
168export const Board = React.memo(function Board(props: BoardProps) {
169    const classes = useStyles(props);
170
171    return (
172        <div className={classes.root}>
173            {props.words.map((arr, row) =>
174                arr.map((tile, col) => (
175                    <div key={row * props.words.length + col}>
176                        <Tile
177                            row={row}
178                            col={col}
179                            onClick={props.onClick}
180                            tile={tile}
181                            spymaster={props.spymaster}
182                            myTurn={props.myTurn}
183                            winner={props.winner}
184                        />
185                    </div>
186                ))
187            )}
188        </div>
189    );
190}, isEqual);