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