import React, { useEffect, useState, useContext, useRef, useMemo } from "react"; import equal from "fast-deep-equal"; import { assetsPath, debounce } from "./Common.js"; import "./Board.css"; import { GlobalContext } from "./GlobalContext.js"; const rows = [3, 4, 5, 4, 3, 2]; /* The final row of 2 is to place roads and corners */ const hexRatio = 1.1547, tileWidth = 67, tileHalfWidth = tileWidth * 0.5, tileHeight = tileWidth * hexRatio, tileHalfHeight = tileHeight * 0.5, radius = tileHeight * 2, borderOffsetX = 86, /* ~1/10th border image width... hand tuned */ borderOffsetY = 3; /* Actual sizing */ const tileImageWidth = 90, /* Based on hand tuned and image width */ tileImageHeight = tileImageWidth/hexRatio, borderImageWidth = (2 + 2/3) * tileImageWidth, /* 2.667 * .Tile.width */ borderImageHeight = borderImageWidth * 0.29; /* 0.29 * .Border.height */ const Board = () => { const { ws } = useContext(GlobalContext); const board = useRef(); const [pipElements, setPipElements] = useState(<>); const [borderElements, setBorderElements] = useState(<>); const [tileElements, setTileElements] = useState(<>); const [cornerElements, setCornerElements] = useState(<>); const [roadElements, setRoadElements] = useState(<>); const [ signature, setSignature ] = useState(""); const [ generated, setGenerated ] = useState(""); const [ robber, setRobber ] = useState(-1); const [ robberName, setRobberName ] = useState([]); const [ pips, setPips ] = useState(); const [ pipOrder, setPipOrder ] = useState(); const [ borders, setBorders ] = useState(); const [ borderOrder, setBorderOrder ] = useState(); const [ tiles, setTiles ] = useState(); const [ tileOrder, setTileOrder ] = useState([]); const [ placements, setPlacements ] = useState(undefined); const [ turn, setTurn ] = useState({}); const [ state, setState ] = useState(""); const [ color, setColor ] = useState(""); const [ longestRoadLength, setLongestRoadLength ] = useState(0); const fields = useMemo(() => [ 'signature', 'robber', 'robberName', 'pips', 'pipOrder', 'borders', 'borderOrder', 'tiles', 'tileOrder', 'placements', 'turn', 'state', 'color', 'longestRoadLength' ], []); /* Placements is a structure of roads and corners arrays * indicating what is stored at each of the locations * * Corners consist of a type and color * Roads consist of a color, and longestRoad * * See games.js resetGame, placeRoad, placeSettlement, placeCity, * and calculateRoadLengths * * Returns: true === differences, false === same */ const comparePlacements = (A, B) => { if (!A && !B) { return false; /* same */ } if ((A && !B) || (!A && B)) { return true; } if ((A.roads.length !== B.roads.length) || (A.corners.length !== B.corners.length)) { return true; } /* Roads compare color and longestRoad */ for (let i = 0; i < A.roads.length; i++) { if (A.roads[i].color !== B.roads[i].color) { return true; } if (A.roads[i].longestRoad !== B.roads[i].longestRoad) { return true; } } /* Corners compare type and color */ for (let i = 0; i < A.corners.length; i++) { if (A.corners[i].type !== B.corners[i].type) { return true; } if (A.corners[i].color !== B.corners[i].color) { return true; } } return false; /* same */ }; const onWsMessage = (event) => { const data = JSON.parse(event.data); switch (data.type) { case 'game-update': console.log(`board - game update`, data.update); if ('robber' in data.update && data.update.robber !== robber) { setRobber(data.update.robber); } if ('robberName' in data.update && data.update.robberName !== robberName) { setRobberName(data.update.robberName); } if ('state' in data.update && data.update.state !== state) { setState(data.update.state); } if ('color' in data.update && data.update.color !== color) { setColor(data.update.color); } if ('longestRoadLength' in data.update && data.update.longestRoadLength !== longestRoadLength) { setLongestRoadLength(data.update.longestRoadLength); } if ('turn' in data.update) { if (!equal(data.update.turn, turn)) { console.log(`board - turn`, data.update.turn); setTurn(data.update.turn); } } if ('placement' in data.update) { if (!equal(data.update.placements, placements)) { console.log(`board - placements`, data.update.placements); setPlacements(data.update.placements); } } if ('signature' in data.update && data.update.signature !== signature) { setSignature(data.update.signature); /* The following are only updated if there is a new game * signature */ if ('pipOrder' in data.update) { setPipOrder(data.update.pipOrder); } if ('borderOrder' in data.update) { setBorderOrder(data.update.borderOrder); } if ('tileOrder' in data.update) { setTileOrder(data.update.tileOrder); } } /* This is permanent static data from the server -- do not update * once set */ if ('pips' in data.update && !pips) { setPips(data.update.pips); } if ('tiles' in data.update && !tiles) { setTiles(data.update.tiles); } if ('borders' in data.update && !borders) { setBorders(data.update.borders); } break; default: break; } }; const refWsMessage = useRef(onWsMessage); useEffect(() => { refWsMessage.current = onWsMessage; }); useEffect(() => { if (!board.current) { return; } const onResize = debounce(() => { if (!board.current) { return; } /* Adjust the 'transform: scale' for the BoardBox * so the board fills the Board * * The board is H tall, and H * hexRatio wide */ const width = board.current.offsetWidth, height = board.current.offsetHeight; let _transform; if (height * hexRatio > width) { _transform = width / (450. * hexRatio); } else { _transform = height / (450.); } const boardBox = board.current.querySelector('.BoardBox'); if (boardBox) { console.log(`Setting transofrm scale to ${_transform}`); boardBox.style.transform = `scale(${_transform})`; } }, 250); window.addEventListener('resize', onResize); onResize(); return () => { window.removeEventListener('resize', onResize); } }, [board]); useEffect(() => { if (!ws) { return; } console.log('board - bind'); const cbMessage = e => refWsMessage.current(e); ws.addEventListener('message', cbMessage); return () => { console.log('board - unbind'); ws.removeEventListener('message', cbMessage); } }, [ws, refWsMessage]); useEffect(() => { if (!ws) { return; } ws.send(JSON.stringify({ type: 'get', fields })); }, [ws, fields]); const Tile = ({tile}) => { const onClick = (event) => { console.log(`Tile clicked: ${tile.index}`); }; return
; }; const sendPlacement = (type, index) => { ws.send(JSON.stringify({ type, index })); }; const Road = ({road}) => { const onClick = (event) => { console.log(`Road clicked: ${road.index}`); sendPlacement('place-road', road.index); }; return
; }; const Corner = ({corner}) => { const onClick = (event) => { let type; if (event.currentTarget.getAttribute('data-type') === 'settlement') { type = 'place-city'; } else { type = 'place-settlement'; } sendPlacement(type, corner.index); }; return
; }; const Pip = ({pip}) => { const onClick = (event) => { sendPlacement('place-robber', pip.index); }; return
; }; useEffect(() => { if (!signature) { return; } if (signature === generated) { return; } if (!pips || !pipOrder || !borders || !borderOrder || !tiles || !tileOrder) { return; } console.log(`board - Generate board - ${signature}`); const generateRoads = () => { let row = 0, rowCount = 0; let y = -2.5 + tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth, x = -tileHalfHeight -(rows[row] - 1) * 0.5 * tileHeight; let index = 0; let road; const corners = []; for (let i = 0; i < 21; i++) { const lastRow = row === rows.length - 1; if (row > 2 && rowCount === 0) { road = { index: index++, angle: -60, top: y-0.5*tileHalfHeight, left: x-tileHalfHeight }; corners.push(); } road = { index: index++, angle: 240, top: y, left: x }; corners.push(); road = { index: index++, angle: -60, top: y-0.5*tileHalfHeight, left: x+tileHalfHeight }; corners.push(); if (!lastRow) { road = { index: index++, angle: 0, top: y, left: x }; corners.push(); } if (++rowCount === rows[row]) { if (!lastRow) { road = { index: index++, angle: 0, top: y, left: x+2.*tileHalfHeight }; corners.push(); } if (row > 2) { road = { index: index++, angle: 60, top: y-0.5*tileHalfHeight, left: x+3.*tileHalfHeight }; corners.push(); } row++; rowCount = 0; y += tileHeight - 10.5; x = -tileHalfHeight - (rows[row] - 1) * 0.5 * tileHeight; } else { x += tileHeight; } } return corners; } const generateCorners = () => { let row = 0, rowCount = 0; let y = -8 + 0.5 * tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth, x = -tileHalfHeight -(rows[row] - 1) * 0.5 * tileHeight; let index = 0; const corners = []; let corner; for (let i = 0; i < 21; i++) { if (row > 2 && rowCount === 0) { corner = { index: index++, top: y-0.5*tileHalfHeight, left: x-tileHalfHeight }; corners.push(); } corner = { index: index++, top: y, left: x }; corners.push(); corner = { index: index++, top: y-0.5*tileHalfHeight, left: x+tileHalfHeight }; corners.push(); if (++rowCount === rows[row]) { corner = { index: index++, top: y, left: x+2.*tileHalfHeight }; corners.push(); if (row > 2) { corner = { index: index++, top: y-0.5*tileHalfHeight, left: x+3.*tileHalfHeight }; corners.push(); } row++; rowCount = 0; y += tileHeight - 10.5; x = -tileHalfHeight - (rows[row] - 1) * 0.5 * tileHeight; } else { x += tileHeight; } } return corners; } const generatePips = () => { let row = 0, rowCount = 0; let y = tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth, x = -(rows[row] - 1) * 0.5 * tileHeight; let index = 0; let pip; return pipOrder.map(order => { pip = { roll: pips[order].roll, index: index++, top: y, left: x, order: order }; const div = ; if (++rowCount === rows[row]) { row++; rowCount = 0; y += tileWidth; x = - (rows[row] - 1) * 0.5 * tileHeight; } else { x += tileHeight; } return div; }); }; const generateTiles = () => { let row = 0, rowCount = 0; let y = tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth, x = -(rows[row] - 1) * 0.5 * tileHeight; let index = 0; return tileOrder.map(order => { const tile = Object.assign({}, tiles[order], { index: index++, left: x, top: y}); let div = ; if (++rowCount === rows[row]) { row++; rowCount = 0; y += tileWidth; x = - (rows[row] - 1) * 0.5 * tileHeight; } else { x += tileHeight; } return div; }); }; const generateBorders = () => { const sides = 6; let side = -1; return borderOrder.map(order => { const border = borders[order]; side++; let x = + Math.sin(Math.PI - side / sides * 2. * Math.PI) * radius, y = Math.cos(Math.PI - side / sides * 2. * Math.PI) * radius; let prev = (order === 0) ? 6 : order; const file = `borders-${order+1}.${prev}.png`; return
; }); }; setPipElements(generatePips()); setBorderElements(generateBorders()); setTileElements(generateTiles()); setCornerElements(generateCorners()); setRoadElements(generateRoads()); setGenerated(signature); }, [ signature, generated, setPipElements, setBorderElements, setTileElements, setCornerElements, setRoadElements, borderOrder, borders, pipOrder, pips, tileOrder, tiles ]); useEffect(() => { if (!turn) { return; } let nodes = document.querySelectorAll('.Active'); for (let i = 0; i < nodes.length; i++) { nodes[i].classList.remove('Active'); } if (turn.roll) { nodes = document.querySelectorAll(`.Pip[data-roll="${turn.roll}"]`); for (let i = 0; i < nodes.length; i++) { const index = nodes[i].getAttribute('data-index'); if (index !== null) { const tile = document.querySelector(`.Tile[data-index="${index}"]`); if (tile) { tile.classList.add('Active'); } } nodes[i].classList.add('Active'); } } }, [ turn ]); useEffect(() => { if (!placements) { return; } /* Set color and type based on placement data from the server */ placements.corners.forEach((corner, index) => { const el = document.querySelector(`.Corner[data-index="${index}"]`); if (!el) { return; } if (!corner.color) { el.removeAttribute('data-color'); el.removeAttribute('data-type'); } else { el.setAttribute('data-color', corner.color); el.setAttribute('data-type', corner.type); } }); placements.roads.forEach((road, index) => { const el = document.querySelector(`.Road[data-index="${index}"]`); if (!el) { return; } if (!road.color) { el.removeAttribute('data-color'); } else { if (road.longestRoad) { if (road.longestRoad === longestRoadLength) { el.classList.add('LongestRoad'); } else { el.classList.remove('LongestRoad'); } el.setAttribute('data-longest', road.longestRoad); } else { el.removeAttribute('data-longest'); } el.setAttribute('data-color', road.color); } }); /* Clear all 'Option' targets */ let nodes = document.querySelectorAll(`.Option`); for (let i = 0; i < nodes.length; i++) { nodes[i].classList.remove('Option'); } /* Add 'Option' based on turn.limits */ if (turn && turn.limits) { if (turn.limits['roads']) { turn.limits['roads'].forEach(index => { const el = document.querySelector(`.Road[data-index="${index}"]`); if (!el) { return; } el.classList.add('Option'); }); } if (turn.limits['corners']) { turn.limits['corners'].forEach(index => { const el = document.querySelector(`.Corner[data-index="${index}"]`); if (!el) { return; } el.classList.add('Option'); }); } if (turn.limits['tiles']) { turn.limits['tiles'].forEach(index => { const el = document.querySelector(`.Tile[data-index="${index}"]`); if (!el) { return; } el.classList.add('Option'); }); } if (turn.limits['pips']) { turn.limits['pips'].forEach(index => { const el = document.querySelector(`.Pip[data-index="${index}"]`); if (!el) { return; } el.classList.add('Option'); }); } } }, [ placements, turn]); useEffect(() => { let nodes = document.querySelectorAll(`.Pip.Robber`); for (let i = 0; i < nodes.length; i++) { nodes[i].classList.remove('Robber'); [ 'Robert', 'Roberta', 'Velocirobber' ].forEach(robberName => nodes[i].classList.remove(robberName) ); } if (robber !== undefined) { const el = document.querySelector(`.Pip[data-index="${robber}"]`); if (el) { el.classList.add('Robber'); el.classList.add(robberName); } } }, [ robber, robberName ]); const canAction = (action) => { return (turn && Array.isArray(turn.actions) && turn.actions.indexOf(action) !== -1); }; const canRoad = (canAction('place-road') && turn.color === color && (state === 'initial-placement' || state === 'normal')); const canCorner = ((canAction('place-settlement') || canAction('place-city')) && turn.color === color && (state === 'initial-placement' || state === 'normal')); const canPip = (canAction('place-robber') && turn.color === color && (state === 'initial-placement' || state === 'normal')); return (
{ borderElements }
{ tileElements }
{ pipElements }
{ cornerElements }
{ roadElements }
); }; export { Board };