diff --git a/package.json b/package.json index 6f36c842..248c0cd7 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "dependencies": { "@babel/cli": "^7.13.10", "@davidwu226/file-api": "^0.11.1", + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", "@fortawesome/fontawesome-svg-core": "^1.2.27", "@fortawesome/free-brands-svg-icons": "^5.13.0", "@fortawesome/free-regular-svg-icons": "^5.13.0", @@ -14,6 +16,9 @@ "@material-ui/core": "^4.9.3", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.45", + "@mui/material": "^5.2.6", + "@mui/utils": "^5.2.3", + "@mui/x-data-grid": "^5.2.1", "@svgr/webpack": "4.3.3", "@typescript-eslint/eslint-plugin": "^2.10.0", "@typescript-eslint/parser": "^2.10.0", @@ -66,12 +71,13 @@ "postcss-safe-parser": "4.0.1", "react": "^16.12.0", "react-app-polyfill": "^1.0.6", - "react-chessground": "^1.0.0", + "react-chessground": "^1.5.0", "react-datepicker": "^3.5.0", "react-dev-utils": "^11.0.4", "react-dom": "^16.12.0", "react-faq-component": "^1.2.1", "react-ga": "^2.7.0", + "react-is": "^17.0.2", "react-lazy-load-image-component": "^1.4.3", "react-select-search": "0.10.2", "react-step-progress-bar": "^1.0.3", @@ -95,7 +101,8 @@ "scripts": { "start": "node scripts/start.js", "build": "node scripts/build.js", - "test": "export NODE_ENV=test; npx babel ./src --out-dir dist && cucumber-js tests/functionalTests $REPORT -t 'not @skipTests' $EXTRAS", + "jest" : "export NODE_ENV=test; jest", + "test": "export NODE_ENV=test; jest && npx babel ./src --out-dir dist && cucumber-js tests/functionalTests $REPORT -t 'not @skipTests' $EXTRAS", "testUI": "cucumber-js tests/uiTests $REPORT $EXTRAS", "testciUI": "cucumber-js tests/uiTests -t 'not @skipci' $REPORT $EXTRAS" }, @@ -168,7 +175,8 @@ "extends": "react-app" }, "resolutions": { - "**/@babel/runtime": "7.13.10" + "**/@babel/runtime": "7.13.10", + "**/chessground": "7.7.2" }, "devDependencies": { "@babel/runtime": "^7.13.10", diff --git a/public/css/customv620.css b/public/css/customv620.css index bc40dba7..22e81bee 100644 --- a/public/css/customv620.css +++ b/public/css/customv620.css @@ -202,6 +202,21 @@ .fenDiv { padding-top:10px; } + +.searchSettingsIcon { + cursor: pointer; + margin-left: 10px; +} + +.searchGridDiv { + /* XXX: ideally we'd make this responsive based on the grid layout */ + height: 400px; +} + +.searchResultPositionCell { + font-size: 13px; +} + .errorCard { margin-top:10px; } @@ -430,6 +445,9 @@ body.dark-theme .dropdown-menu, body.dark-theme .MuiCheckbox-colorPrimary.Mui-checked, body.dark-theme .MuiSlider-thumb, body.dark-theme .MuiSlider-markLabel, +body.dark-theme .MuiDataGrid-root, +body.dark-theme .searchSettingsIcon, +body.dark-theme .searchResultLink, body.dark-theme .performanceRatingRow, body.dark-theme .list-group-item, body.dark-theme .card-body { @@ -445,6 +463,10 @@ body.dark-theme input[type="search"] { color: #333; } +body.dark-theme button:disabled span { + color: #777; +} + body.dark-theme .MuiButton-containedPrimary { background-color: #3f51b5; } @@ -488,4 +510,15 @@ body.dark-theme input.select-search-box__search::placeholder{ body.dark-theme span.MuiChip-label{ color:#DDD -} \ No newline at end of file +} + +body.dark-theme .MuiDataGrid-overlay { + background-color: #333; +} + +body.dark-theme .MuiDataGrid-columnHeader:focus, +body.dark-theme .MuiDataGrid-cell:focus, +body.dark-theme .MuiDataGrid-columnHeader:focus-within, +body.dark-theme .MuiDataGrid-cell:focus-within { + outline: none; +} diff --git a/src/app/GameState.js b/src/app/GameState.js new file mode 100644 index 00000000..4ebefc72 --- /dev/null +++ b/src/app/GameState.js @@ -0,0 +1,63 @@ +import ChessEcoCodes from 'chess-eco-codes' +import {chessLogic, rootFen} from '../app/chess/ChessLogic' + +export default class GameState { + constructor(variant, fen, headers) { + this.variant = variant + this.initialFen = fen !== undefined ? fen : rootFen(variant) + this.headers = headers + this.opening = undefined + this.chess = chessLogic(this.variant, this.initialFen) + this.moves = [] + this.ply = 0 + } + + getFen() { + return this.chess.fen() + } + + getPly() { + return this.ply + } + + getTurn() { + return this.chess.turn() + } + + getMoves() { + return this.moves.slice() + } + + getOpening() { + return this.opening + } + + makeMove(move) { + while (this.moves.length > 0 && + this.ply !== this.moves[this.moves.length - 1].ply) { + this.moves.pop() + } + move = this.chess.move(move) + if (move !== null) { + move.ply = this.ply = this.ply + 1 + this.moves.push(move) + let opening = ChessEcoCodes(this.chess.fen()) + if (opening) { + this.opening = opening + } + } + return move + } + + navigateToMove(ply) { + this.ply = 0 + this.chess = chessLogic(this.variant, this.initialFen) + this.moves.forEach((move) => { + if (move.ply > ply) { + return + } + this.chess.move(move) + this.ply = move.ply + }) + } +} diff --git a/src/app/GameState.test.js b/src/app/GameState.test.js new file mode 100644 index 00000000..3c350052 --- /dev/null +++ b/src/app/GameState.test.js @@ -0,0 +1,27 @@ +import GameState from './GameState' + +test('make move', () => { + let game = new GameState() + expect(game.getOpening()).toBe(undefined) + expect(game.makeMove('e4')).not.toBeNull() + expect(game.makeMove('e5')).not.toBeNull() + expect(game.makeMove('Nf3')).not.toBeNull() + expect(game.getPly()).toBe(3) + expect(game.getTurn()).toBe('b') + expect(game.getMoves().map((move) => move.san)).toEqual(['e4', 'e5', 'Nf3']) + expect(game.getOpening().code).toBe('C40') +}) + +test('overwrite move', () => { + let game = new GameState() + expect(game.getOpening()).toBe(undefined) + game.makeMove('e4') + let move = game.makeMove('e5') + game.makeMove('Nf3') + game.navigateToMove(move.ply) + expect(game.makeMove('Bc4')).not.toBeNull() + expect(game.getPly()).toBe(3) + expect(game.getTurn()).toBe('b') + expect(game.getMoves().map((move) => move.san)).toEqual(['e4', 'e5', 'Bc4']) + expect(game.getOpening().code).toBe('C23') +}) diff --git a/src/app/OpeningGraph.js b/src/app/OpeningGraph.js index e11622f4..d73916bb 100644 --- a/src/app/OpeningGraph.js +++ b/src/app/OpeningGraph.js @@ -1,20 +1,47 @@ +import ChessEcoCodes from 'chess-eco-codes' import {simplifiedFen, isDateMoreRecentThan} from './util' import * as Constants from './Constants' import {chessLogic, rootFen} from '../app/chess/ChessLogic' +class Game { + constructor(headers, moves) { + this.headers = headers + this.moves = moves + this.opening = undefined + } + + getOpening() { + if (this.opening === undefined) { + for (var ply = 1; ply < this.moves.length; ply++) { + let opening = ChessEcoCodes(this.moves[ply].sourceFen) + if (!opening) { + break + } + this.opening = opening + } + } + return this.opening + } +} + export default class OpeningGraph { constructor(variant) { - this.graph=new Graph() + this.graph = new Graph() + this.games = [] this.hasMoves = false this.variant = variant } + setEntries(arrayEntries, pgnStats){ - this.graph=new Graph(arrayEntries, pgnStats) + this.graph = new Graph(arrayEntries, pgnStats) + // .tree files don't preserve game history + this.games = undefined this.hasMoves = true } clear() { this.graph = new Graph() + this.games = [] this.hasMoves = false } @@ -24,10 +51,11 @@ export default class OpeningGraph { this.graph.playerColor = playerColor this.hasMoves = true parsedMoves.forEach(parsedMove => { - this.addMoveForFen(parsedMove.sourceFen, parsedMove.targetFen, parsedMove.moveSan, pgnStats) + this.addMoveForFen(parsedMove.sourceFen, parsedMove.targetFen, parsedMove.move.san, pgnStats) }) this.addGameResultOnFen(lastFen, pgnStats.index) this.addStatsToRoot(pgnStats, this.variant) + this.games.push(new Game(pgnStats.headers, parsedMoves)) } addGameResultOnFen(fullFen, resultIndex) { diff --git a/src/app/OpeningManager.js b/src/app/OpeningManager.js deleted file mode 100644 index ac0bc93e..00000000 --- a/src/app/OpeningManager.js +++ /dev/null @@ -1,73 +0,0 @@ -import * as ChessLogic from './chess/ChessLogic' - -export default class OpeningManager { - plys = [] - currentIndex = 0 - constructor(variant) { - this.plys = [{pgn:'', fen:ChessLogic.rootFen(variant), move:null}] - this.currentIndex = 0 - } - addPly(fen,move) { - if(this.currentIndex=0 && index {this.state.activeTab === 'report'?"Report":""} + + { this.toggle('search'); }} + > + {this.state.activeTab === 'search'?"Search":""} + + + + + @@ -229,4 +246,4 @@ export default class ControlsContainer extends React.Component { reportFooter(){ return Performance rating calculated based on FIDE regulations } -} \ No newline at end of file +} diff --git a/src/pres/MainContainer.js b/src/pres/MainContainer.js index a176bcca..1687a644 100644 --- a/src/pres/MainContainer.js +++ b/src/pres/MainContainer.js @@ -21,8 +21,8 @@ import { } from '@material-ui/core' import * as Constants from '../app/Constants' +import GameState from '../app/GameState' import OpeningGraph from '../app/OpeningGraph' -import { chessLogic } from '../app/chess/ChessLogic' import cookieManager from '../app/CookieManager' import { handleDarkMode } from '../pres/DarkMode'; import UserProfile, { USER_PROFILE_NEW_USER } from '../app/UserProfile' @@ -40,14 +40,16 @@ export default class MainContainer extends React.Component { let urlVariant = new URLSearchParams(window.location.search).get("variant") let selectedVariant = urlVariant || Constants.VARIANT_STANDARD - this.chess = chessLogic(selectedVariant) + this.game = new GameState(selectedVariant) addStateManagement(this) this.state = { resize:0, - fen: this.chess.fen(), - lastMove: null, + fen:this.game.getFen(), + ply:this.game.getPly(), + moves:this.game.getMoves(), gamesProcessed:0, openingGraph:new OpeningGraph(selectedVariant), + dataSourceKey:0, settings:{ playerName:'', orientation:Constants.PLAYER_COLOR_WHITE, @@ -61,8 +63,8 @@ export default class MainContainer extends React.Component { diagnosticsDataOpen:false, variant:selectedVariant, update:0,//increase count to force update the component - highlightedMove:null - } + highlightedMove:null, + } this.chessboardWidth = this.getChessboardWidth() this.forBrushes = ['blue','paleGrey', 'paleGreen', 'green'] @@ -109,7 +111,12 @@ export default class MainContainer extends React.Component { } render() { - let lastMoveArray = this.state.lastMove ? [this.state.lastMove.from, this.state.lastMove.to] : null + let lastMove = this.state.ply > 0 + ? this.state.moves[this.state.ply - 1] + : null + let lastMoveArray = lastMove + ? [lastMove.from, lastMove.to] + : null let snackBarOpen = Boolean(this.state.message) let playerMoves = this.getPlayerMoves() @@ -123,9 +130,13 @@ export default class MainContainer extends React.Component { - + diff --git a/src/pres/Navigator.js b/src/pres/Navigator.js index 0949f382..e7ba6f9a 100644 --- a/src/pres/Navigator.js +++ b/src/pres/Navigator.js @@ -1,6 +1,4 @@ import React from 'react' -import ChessEcoCodes from 'chess-eco-codes' -import OpeningManager from '../app/OpeningManager' import {Container, Row, Col, Button} from 'reactstrap' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faStepForward, faStepBackward } from '@fortawesome/free-solid-svg-icons' @@ -11,12 +9,27 @@ export default class Navigator extends React.Component { constructor(props){ super(props) - this.openingManager = new OpeningManager(this.props.variant) - this.state = { - currentMove:0, - } window.addEventListener("keydown",this.keyHandler.bind(this)) } + + movePairs() { + var idx = 0 + var pairs = [] + + if (this.props.moves[idx].color === 'b') { + // null first move for white + pairs.push([undefined, this.props.moves[idx]]) + idx += 1 + } + + while (idx < this.props.moves.length) { + pairs.push([this.props.moves[idx], this.props.moves[idx + 1]]) + idx += 2 + } + + return pairs + } + keyHandler(e){ switch(e.keyCode) { case 37: @@ -43,95 +56,88 @@ export default class Navigator extends React.Component { // } // } - shouldComponentUpdate(newProps) { - //console.log(newProps) - if(newProps.variant !== this.props.variant) { - this.openingManager = new OpeningManager(newProps.variant) - return true - - } - if(newProps.fen !== this.openingManager.fen()) { - if(newProps.move === null) { - // called when "clear" or "starting position" actions are hit - this.openingManager = new OpeningManager(newProps.variant) - return true - } - this.openingManager.addPly(newProps.fen, newProps.move) - return true - } - return true - } - previous(e, device) { - let newState = this.openingManager.moveBack() - this.props.onChange(newState.fen, newState.move) - this.setState({ - currentMove: this.openingManager.currentMove() - }) + this.props.navigateToMove(this.props.ply - 1) trackEvent(Constants.EVENT_CATEGORY_NAVIGATOR, "Previous", device || "mouse") } next(e, device) { - let newState = this.openingManager.moveForward() - this.props.onChange(newState.fen, newState.move) - this.setState({currentMove:this.openingManager.currentMove()}) + this.props.navigateToMove(this.props.ply + 1) trackEvent(Constants.EVENT_CATEGORY_NAVIGATOR, "Next", device || "mouse") } - moveTo(index) { - return () => { - let newState = this.openingManager.moveTo(index+1) - this.props.onChange(newState.fen, newState.move) - this.setState({currentMove:this.openingManager.currentMove()}) - trackEvent(Constants.EVENT_CATEGORY_NAVIGATOR, "move", null, index) - } + moveTo(ply) { + this.props.navigateToMove(ply) + trackEvent(Constants.EVENT_CATEGORY_NAVIGATOR, "move", null, ply) } render(){ - let opening = ChessEcoCodes(this.openingManager.fen()) - if (opening) { - this.opening = opening.name - this.openingCode = opening.code - } - if(!this.openingManager.pgnListSoFar()) { + if (this.props.moves.length === 0) { return
} - return - - - - - - + return ( + + + + + + + + + + { this.renderOpening() } + { + this.movePairs().map((pair, idx) => this.renderMovePair(idx + 1, pair)) + } + + ) + } + + renderOpening() { + if (this.props.opening === undefined) { + return <> + } + return ( + + {this.props.opening.code}: {this.props.opening.name} + + ) + } + + renderMovePair(idx, pair) { + let [white, black] = pair + return ( + + + {`${idx}.`} + { this.renderMoveColumn(white) } + { this.renderMoveColumn(black) } - {this.openingCode}: {this.opening} - { - this.openingManager.pgnListSoFar().map((move, index)=> - - - {`${move.moveNumber}.`} - - - {`${move.whitePly}`} - - - {`${move.blackPly}`} - - ) - } - + ) + } + + renderMoveColumn(move) { + var style = `navItem navMove border` + if (move === undefined) { + return + } + + if (move.ply === this.props.ply) { + style += ' selectedMove' + } + + return ( + this.moveTo(move.ply)}> + {move.san} + + ) } // TODO: Put scroll handler behind setting @@ -142,4 +148,4 @@ export default class Navigator extends React.Component { // if (chessBoard) chessBoard.onwheel = this.scrollHandler.bind(this); // if (navigator) navigator.onwheel = this.scrollHandler.bind(this); // } -} \ No newline at end of file +} diff --git a/src/pres/Search.js b/src/pres/Search.js new file mode 100644 index 00000000..fe7470a1 --- /dev/null +++ b/src/pres/Search.js @@ -0,0 +1,590 @@ +import React from 'react' +import { + Modal, + ModalHeader, + ModalBody, + ModalFooter, +} from 'reactstrap' +import { + Box, + Button, + FormControl, + FormControlLabel, + FormLabel, + Link, + Radio, + RadioGroup, + Table, + TableBody, + TableRow, + TableCell, + TextField, +} from '@material-ui/core' +import { + DataGrid, + GridOverlay, +} from '@mui/x-data-grid' +import CircularProgress from '@mui/material/CircularProgress' +import Tooltip from '@mui/material/Tooltip' +import { + FontAwesomeIcon, +} from '@fortawesome/react-fontawesome' +import { + faWrench, +} from '@fortawesome/free-solid-svg-icons' +import GameState from '../app/GameState' +import { simplifiedFen } from '../app/util' + +const SHOW_ALL_GAMES = 'show-all-games' +const SHOW_UNIQUE_POSITIONS = 'show-unique-positions' + +class Position { + constructor(game, ply) { + this.game = game + this.move = game.moves[ply - 1] + this.ply = ply + } + + turn() { + return this.move.sourceFen.split(/\s+/)[1] === 'w' + ? 'White' + : 'Black' + } +} + +export class Query { + constructor(text) { + this.text = text + this.fen = this.parseFen(text) + this.move = this.parseMove(text) + } + + // sloppy FEN validator + parseFen(text) { + let tokens = text.trim().split(/\s+/) + if (tokens.length > 6) { + // more than six words is probably not FEN + return undefined + } + + // validate grid of pieces + let grid = tokens[0].split('/') + if (grid.length !== 8) { + // need exactly 8 rows + return undefined + } + for (var row = 0; row < grid.length; row++) { + var sum = 0 + var [prv, cur] = [false, false] + for (var col = 0; col < grid[row].length; col++) { + cur = !isNaN(grid[row][col]) + if (cur) { + if (prv) { + // consecutive numbers + return undefined + } + sum += parseInt(grid[row][col], 10) + } else { + if (!/^[prnbqkPRNBQK]$/.test(grid[row][col])) { + // invalid piece + return undefined + } + sum += 1 + } + prv = cur + } + if (sum !== 8) { + // need exactly 8 columns + return undefined + } + } + + // validate active color, if present + if (tokens.length > 1 && !/^(w|b)$/.test(tokens[1])) { + return undefined + } + + // ok, good enough for our purposes + return text + } + + // sloppy SAN parser + parseMove(text) { + let matches = text + .replace(/=/, '') + .replace(/[?!]*$/, '') + .match(/([pnbrqkPNBRQK])?([a-h]?[1-8]?)(x?-?)([a-h][1-8])([qrbnQRBN])?/) + if (!matches) { + return undefined + } + let move = { + piece: matches[1], + from: matches[2], + capture: matches[3].includes('x') ? true : undefined, + to: matches[4], + promotion: matches[5] ? matches[5].toLowerCase() : undefined, + check: text.includes('+') ? true : undefined, + mate: text.includes('#') ? true : undefined, + } + if (!move.piece && move.capture && move.from.length === 1) { + move.piece = 'p' + } + return move + } + + evaluate(parsedMove) { + if (this.text === parsedMove.move.san) { + return true + } + if (this.fen !== undefined && parsedMove.targetFen.startsWith(this.fen)) { + return true + } + if (this.move !== undefined) { + let move = parsedMove.move + return ( + this.move.to === move.to + // `this.move.from` may be a square (e.g., 'b1' in 'Nb1d2') + // or a disambiguator (e.g., 'b' in 'Nbd2') + && (this.move.from === move.from + || (this.move.from.length === 1 + && (move.from.startsWith(this.move.from) + || move.from.endsWith(this.move.from)))) + && (!this.move.piece || this.move.piece.toLowerCase() === move.piece) + && this.move.promotion === move.promotion + && (this.move.capture === undefined || move.captured !== undefined) + && (this.move.check === undefined || move.san.endsWith('+')) + && (this.move.mate === undefined || move.san.endsWith('#')) + ) + } + return false + } +} + +export class Search extends React.Component { + constructor(props) { + super(props) + this.state = { + query: '', + results: [], + scanning: false, + scannedGames: 0, + totalGames: 0, + editSettings: false, + settings: { + mode: SHOW_ALL_GAMES, + }, + } + } + + updateQuery(event) { + this.setState({ + query: event.target.value, + }) + } + + toggleSettingsModal() { + this.setState({ editSettings: !this.state.editSettings }) + } + + updateSettings(settings) { + this.setState({ settings: settings }) + } + + clearResults() { + this.setState({ + results: [], + scanning: false, + scannedGames: 0, + totalGames: 0, + }) + } + + search() { + if (!this.state.query) { + this.clearResults() + return + } + this.setState({ + results: [], + scanning: true, + scannedGames: 0, + totalGames: this.props.openingGraph.games.length, + }) + setTimeout(() => this.scan(new Query(this.state.query), 0)) + } + + cancel() { + this.setState({ scanning: false }) + } + + scan(query, idx) { + if (!this.state.scanning) { + return + } + + let games = this.props.openingGraph.games + if (idx >= games.length) { + this.cancel() + return + } + + var results = [] + for (var i = 0; idx < games.length && i < 10; i++) { + let game = games[idx] + if (query.fen !== undefined + && game.moves.length > 0 + && game.moves[0].sourceFen.startsWith(query.fen)) { + // special case for starting position + results.push(new Position(game, 0)) + } + for (var ply = 0; ply < game.moves.length; ply++) { + let move = game.moves[ply] + if (query.evaluate(move)) { + results.push(new Position(game, ply + 1)) + } + } + idx++ + } + + if (results.length > 0) { + this.setState({ + results: [...this.state.results, ...results], + }) + } + this.setState({ scannedGames: idx }) + + setTimeout(() => this.scan(query, idx)) + } + + navigate(position) { + let game = new GameState( + this.props.openingGraph.variant, + position.game.moves[0].sourceFen, + position.game.headers, + ) + for (var i = 0; i < position.game.moves.length; i++) { + if (this.state.settings.mode === SHOW_UNIQUE_POSITIONS && i >= position.ply) { + break + } + game.makeMove(position.game.moves[i].move) + } + game.navigateToMove(position.ply) + this.props.navigateToGame(game) + } + + render() { + return <> + {this.renderSettings()} + {this.renderQueryTable()} + {this.renderResultsGrid()} + + } + + renderSettings() { + return ( + this.updateSettings(settings)} + toggle={() => this.toggleSettingsModal()} + /> + ) + } + + renderQueryTable() { + return ( + + + { this.renderQueryInput() } + { this.renderQueryButton() } + +
+ ) + } + + renderQueryInput() { + // disable autoComplete because the styling doesn't work well in dark mode + const message = 'search games for positions (in FEN format) or moves (e.g., Bxh7+)' + return ( + + + + + + + + ) + } + + renderQueryButton() { + const mkSearchButton = () => { + let supported = this.props.openingGraph.games !== undefined + let disabled = !supported || this.state.scanning + var button = ( + + ) + if (!supported) { + button = ( + + + {button} + + + ) + } + return ( + <> + {button} + this.toggleSettingsModal()} + /> + + ) + } + const mkCancelButton = () => { + if (!this.state.scanning) { + return '' + } + return ( + + ) + } + return ( + + + { mkCancelButton() } + { mkSearchButton() } + + + ) + } + + renderResultsGrid() { + if (!this.props.isOpen) { + // DataGrid complains about the parent container height + // when rendered for an inactive TabPane + return '' + } + const mkLabel = (position) => { + if (this.state.settings.mode === SHOW_ALL_GAMES) { + let headers = position.game.headers + return `${headers.White} - ${headers.Black}` + } else { + return simplifiedFen(position.move.sourceFen) + } + } + const mkDate = (date) => { + let [year, month, day] = date.split('.') + return new Date(year, month - 0, day) + } + // TODO valueFormatter for export + const gameColumns = [ + { + field: 'position', + headerName: 'Game', + flex: 1, + renderCell: (params) => ( + this.navigate(params.value)}> + {mkLabel(params.value)} + + ), + }, + { + field: 'date', + type: 'date', + headerName: 'Date', + }, + { + field: 'opening', + headerName: 'ECO', + valueGetter: (params) => { + let opening = params.row.position.game.getOpening() + return `${opening ? opening.code : '?'}` + }, + }, + { + field: 'ply', + headerName: 'Ply', + type: 'number', + hide: true, + valueGetter: (params) => params.row.position.ply, + }, + { + field: 'move', + headerName: 'Move', + hide: true, + valueGetter: (params) => params.row.position.turn(), + }, + ] + const positionColumns = [ + { + field: 'position', + headerName: 'Position', + flex: 1, + cellClassName: 'searchResultPositionCell', + renderCell: (params) => ( + this.navigate(params.value)}> + {mkLabel(params.value)} + + ), + }, + ] + let columns = this.state.settings.mode === SHOW_ALL_GAMES + ? gameColumns + : positionColumns; + let rows = this.getResults().map((position, idx) => { + return { + id: idx, + 'position': position, + 'date': mkDate(position.game.headers.Date), + } + }) + let message = this.state.scanning ? 'searching...' : 'no matches' + let progress = this.state.totalGames > 0 + ? 100 * this.state.scannedGames / this.state.totalGames + : 0 + return ( +
+ ( + + {message} + + ), + LoadingOverlay: () => ( + + + + ), + }} + /> +
+ ) + } + + getResults() { + if (this.state.settings.mode === SHOW_ALL_GAMES) { + return this.state.results + } + let positions = new Map() + return this.state.results.filter((position) => { + let duplicate = positions.has(position.move.sourceFen) + if (!duplicate) { + positions.set(position.move.sourceFen, position) + return true + } + return false + }) + } +} + +class SettingsModal extends React.Component { + constructor(props) { + super(props) + this.state = { ...this.props.settings } + } + + save() { + this.props.updateSettings(this.state) + this.props.toggle() + } + + cancel() { + this.setState({ ...this.props.settings }) + this.props.toggle() + } + + render() { + return ( + + search settings + + + {this.renderSearchOptions()} + + + + + + + ) + } + + renderSearchOptions() { + return ( + + show results for + this.setState({ mode: event.target.value })} + > + } + label='all games' + /> + } + label='unique positions' + /> + + + ) + } +} diff --git a/src/pres/Search.test.js b/src/pres/Search.test.js new file mode 100644 index 00000000..5652bed8 --- /dev/null +++ b/src/pres/Search.test.js @@ -0,0 +1,166 @@ +import { Query } from './Search' +import { chessLogic } from '../app/chess/ChessLogic' +import { simplifiedFen } from '../app/util' + +function mkMove(chess, san) { + let src = chess.fen() + let move = chess.move(san) + expect(move).not.toBeNull() + let dst = chess.fen() + return { + sourceFen: src, + move: move, + targetFen: dst, + } +} + +test('evaluate fen', () => { + let chess = chessLogic() + let move = mkMove(chess, 'e4') + expect(new Query(move.sourceFen).evaluate(move)).toBe(true) + expect(new Query(move.targetFen).evaluate(move)).toBe(false) +}) + +test('evaluate simplified fen', () => { + let chess = chessLogic() + let fen = simplifiedFen(chess.fen()) + mkMove(chess, 'Nc3') + mkMove(chess, 'Nf6') + mkMove(chess, 'Nb1') + mkMove(chess, 'Ng8') + let move = mkMove(chess, 'Nc3') + expect(new Query(fen).evaluate(move)).toBe(true) + expect(new Query(fen.split(' ')[0]).evaluate(move)).toBe(true) + expect(new Query(fen.split(' ')[0] + ' ').evaluate(move)).toBe(true) + expect(new Query(simplifiedFen(move.targetFen)).evaluate(move)).toBe(false) +}) + +test('evaluate san', () => { + let chess = chessLogic() + let move = mkMove(chess, 'Nc3') + expect(new Query('Nc3').evaluate(move)).toBe(true) + expect(new Query('e4').evaluate(move)).toBe(false) +}) + +test('evaluate lan', () => { + let chess = chessLogic() + mkMove(chess, 'e4') + mkMove(chess, 'e5') + mkMove(chess, 'Nf3') + mkMove(chess, 'Nf6') + let move = mkMove(chess, 'Nxe5') + expect(new Query('f3e5').evaluate(move)).toBe(true) + expect(new Query('f3-e5').evaluate(move)).toBe(true) + expect(new Query('f3xe5').evaluate(move)).toBe(true) + expect(new Query('Nf3e5').evaluate(move)).toBe(true) + expect(new Query('Nf3-e5').evaluate(move)).toBe(true) + expect(new Query('Nf3xe5').evaluate(move)).toBe(true) +}) + +test('evaluate disambiguated', () => { + let chess = chessLogic() + mkMove(chess, 'd4') + mkMove(chess, 'd5') + mkMove(chess, 'Nf3') + mkMove(chess, 'Nf6') + let move = mkMove(chess, 'Nbd2') + expect(new Query('Nbd2').evaluate(move)).toBe(true) +}) + +test('evaluate overly disambiguated', () => { + let chess = chessLogic() + mkMove(chess, 'd4') + mkMove(chess, 'd5') + let move = mkMove(chess, 'Nd2') + expect(new Query('Nbd2').evaluate(move)).toBe(true) +}) + +test('evaluate capture', () => { + let chess = chessLogic() + mkMove(chess, 'e4') + mkMove(chess, 'Nf6') + mkMove(chess, 'e5') + mkMove(chess, 'Ne4') + mkMove(chess, 'd4') + mkMove(chess, 'd5') + let capture = mkMove(chess, 'exd6') + expect(new Query('exd6').evaluate(capture)).toBe(true) + let recapture = mkMove(chess, 'Nxd6') + expect(new Query('exd6').evaluate(recapture)).toBe(false) + expect(new Query('Nxd6').evaluate(recapture)).toBe(true) +}) + +test('evaluate no capture', () => { + let chess = chessLogic() + mkMove(chess, 'e4') + mkMove(chess, 'c5') + mkMove(chess, 'Nf3') + mkMove(chess, 'd6') + let move = mkMove(chess, 'Ne5') + expect(new Query('Nxe5').evaluate(move)).toBe(false) +}) + +test('evaluate promotion', () => { + let chess = chessLogic() + mkMove(chess, 'e4') + mkMove(chess, 'd5') + mkMove(chess, 'e5') + mkMove(chess, 'd4') + mkMove(chess, 'e6') + mkMove(chess, 'd3') + mkMove(chess, 'exf7+') + mkMove(chess, 'Kd7') + let move = mkMove(chess, 'fxg8R') + expect(new Query('fxg8R').evaluate(move)).toBe(true) + expect(new Query('fxg8Q').evaluate(move)).toBe(false) +}) + +test('evaluate check', () => { + let chess = chessLogic() + mkMove(chess, 'e4') + mkMove(chess, 'e5') + mkMove(chess, 'Bc4') + mkMove(chess, 'Nf6') + let move = mkMove(chess, 'Bxf7+') + expect(new Query('Bxf7').evaluate(move)).toBe(false) + expect(new Query('Bxf7+').evaluate(move)).toBe(true) +}) + +test('evaluate no check', () => { + let chess = chessLogic() + mkMove(chess, 'e4') + mkMove(chess, 'e5') + mkMove(chess, 'Qh5') + mkMove(chess, 'd6') + let move = mkMove(chess, 'Qxh7') + expect(new Query('Qxh7').evaluate(move)).toBe(true) + expect(new Query('Qxh7+').evaluate(move)).toBe(false) +}) + +test('evaluate mate', () => { + let chess = chessLogic() + mkMove(chess, 'e4') + mkMove(chess, 'e5') + mkMove(chess, 'Bc4') + mkMove(chess, 'Nc6') + mkMove(chess, 'Qh5') + mkMove(chess, 'Nf6') + let move = mkMove(chess, 'Qxf7#') + expect(new Query('Qxf7').evaluate(move)).toBe(false) + expect(new Query('Qxf7+').evaluate(move)).toBe(false) + expect(new Query('Qxf7#').evaluate(move)).toBe(true) +}) + +test('evaluate no mate', () => { + let chess = chessLogic() + mkMove(chess, 'e4') + mkMove(chess, 'e5') + mkMove(chess, 'Bc4') + mkMove(chess, 'Nc6') + mkMove(chess, 'Qh5') + mkMove(chess, 'Nh6') + let move = mkMove(chess, 'Qxf7+') + expect(new Query('Qxf7').evaluate(move)).toBe(false) + expect(new Query('Qxf7+').evaluate(move)).toBe(true) + expect(new Query('Qxf7#').evaluate(move)).toBe(false) +}) diff --git a/src/pres/StateManagement.js b/src/pres/StateManagement.js index cf75f3db..03cdbfd4 100644 --- a/src/pres/StateManagement.js +++ b/src/pres/StateManagement.js @@ -2,6 +2,7 @@ import * as Constants from '../app/Constants' import {trackEvent} from '../app/Analytics' import {copyText} from './loader/Common' import {chessLogic} from '../app/chess/ChessLogic' +import GameState from '../app/GameState' import OpeningGraph from '../app/OpeningGraph' import {fetchBookMoves} from '../app/OpeningBook' import CookieManager from '../app/CookieManager' @@ -10,7 +11,7 @@ import { handleDarkMode } from './DarkMode'; var maxArrowsDrawn = 0 function turnColor() { - return fullTurnName(this.chess.turn()) + return fullTurnName(this.game.getTurn()) } function fullTurnName(shortName) { @@ -32,9 +33,9 @@ function highlightArrow(move) { } function calcMovable() { -const dests = {} - this.chess.SQUARES.forEach(s => { - const ms = this.chess.moves({square: s, verbose: true}) + let dests = {} + this.game.chess.SQUARES.forEach(s => { + let ms = this.game.chess.moves({square: s, verbose: true}) if (ms.length) dests[s] = ms.map(m => m.to) }) return { @@ -48,6 +49,15 @@ function orientation() { return this.state.settings.orientation } +function updateGameState() { + this.setState({ + fen: this.game.getFen(), + ply: this.game.getPly(), + moves: this.game.getMoves(), + opening: this.game.getOpening(), + }) +} + function onMove(sanOrOrig, dest) { let moveObj = null if(dest) { @@ -55,21 +65,25 @@ function onMove(sanOrOrig, dest) { } else { moveObj = sanOrOrig } - const chess = this.chess - let move = chess.move(moveObj) - this.setState({ fen: chess.fen(), lastMove: move}) + this.game.makeMove(moveObj) + this.updateGameState() } - function onMoveAction(sanOrOrig, dest) { this.onMove(sanOrOrig, dest) trackEvent(Constants.EVENT_CATEGORY_CHESSBOARD, "Move") } -function navigateTo(fen, previousMove){ - this.chess = chessLogic(this.state.variant, fen) - this.setState({fen:fen, lastMove:previousMove}) +function navigateToMove(ply) { + this.game.navigateToMove(ply) + this.updateGameState() +} + +function navigateToGame(game) { + this.game = game + this.updateGameState() } + function updateProcessedGames(downloadLimit, n, parsedGame) { let totalGamesProcessed = this.state.gamesProcessed+n this.state.openingGraph.addPGN(parsedGame.pgnStats, parsedGame.parsedMoves, @@ -123,7 +137,7 @@ function getPlayerMoves() { if(!this.state.openingGraph.hasMoves) { return null; } - var moves = this.state.openingGraph.movesForFen(this.chess.fen()) + var moves = this.state.openingGraph.movesForFen(this.game.getFen()) return moves?moves.sort((a,b)=>{ if(a.moveCount === b.moveCount) { return b.details.count - a.details.count @@ -133,7 +147,7 @@ function getPlayerMoves() { } function gameResults() { - return this.state.openingGraph.gameResultsForFen(this.chess.fen()) + return this.state.openingGraph.gameResultsForFen(this.game.getFen()) } function fillArray(arr, len) { @@ -144,8 +158,9 @@ function fillArray(arr, len) { } function reset() { - this.chess = chessLogic(this.state.variant) - this.setState({fen: this.chess.fen(), lastMove:null}) + this.setState({dataSourceKey: this.state.dataSourceKey + 1}) + this.game = new GameState(this.state.variant) + this.updateGameState() } function clear() { @@ -307,7 +322,7 @@ function variantChange(newVariant) { // 2. store them in openinggraph // 3. update the component so that getBookMoves gets called again function getBookMoves() { - let moves = this.state.openingGraph.getBookNode(this.chess.fen()) + let moves = this.state.openingGraph.getBookNode(this.game.getFen()) if(this.state.settings.movesSettings.openingBookType === Constants.OPENING_BOOK_TYPE_OFF) { return {fetch:'off'} } @@ -319,11 +334,13 @@ function getBookMoves() { } function forceFetchBookMoves() { - let moves = fetchBookMoves(this.state.fen, this.state.variant, this.state.settings.movesSettings, (moves)=>{ - this.state.openingGraph.addBookNode(this.chess.fen(), moves) + let fen = this.state.fen + let moves = fetchBookMoves( + fen, this.state.variant, this.state.settings.movesSettings, (moves) => { + this.state.openingGraph.addBookNode(fen, moves) this.setState({update:this.state.update+1}) }) - this.state.openingGraph.addBookNode(this.chess.fen(), moves) + this.state.openingGraph.addBookNode(fen, moves) setImmediate(()=>this.setState({update:this.state.update+1})) return moves } @@ -382,7 +399,9 @@ function addStateManagement(obj){ obj.settingsChange = settingsChange obj.reset = reset obj.clear = clear - obj.navigateTo = navigateTo + obj.updateGameState = updateGameState + obj.navigateToMove = navigateToMove + obj.navigateToGame = navigateToGame obj.playerColor = playerColor obj.fillArray = fillArray obj.brushes = brushes @@ -410,4 +429,4 @@ function addStateManagement(obj){ obj.highlightArrow = highlightArrow } -export {addStateManagement} \ No newline at end of file +export {addStateManagement} diff --git a/yarn.lock b/yarn.lock index 9bebf448..7e5a9789 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1839,7 +1839,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@7.12.1", "@babel/runtime@7.13.10", "@babel/runtime@7.9.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@7.12.1", "@babel/runtime@7.13.10", "@babel/runtime@7.9.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.13.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== @@ -2087,10 +2087,106 @@ foreachasync "^3.0.0" remedial "^1.0.7" +"@emotion/babel-plugin@^11.3.0": + version "11.7.2" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.7.2.tgz#fec75f38a6ab5b304b0601c74e2a5e77c95e5fa0" + integrity sha512-6mGSCWi9UzXut/ZAN6lGFu33wGR3SJisNl3c0tvlmb8XChH1b2SUvxvnOh7hvLpqyRdHHU9AiazV3Cwbk5SXKQ== + dependencies: + "@babel/helper-module-imports" "^7.12.13" + "@babel/plugin-syntax-jsx" "^7.12.13" + "@babel/runtime" "^7.13.10" + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.5" + "@emotion/serialize" "^1.0.2" + babel-plugin-macros "^2.6.1" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.0.13" + +"@emotion/cache@^11.7.1": + version "11.7.1" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539" + integrity sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A== + dependencies: + "@emotion/memoize" "^0.7.4" + "@emotion/sheet" "^1.1.0" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + stylis "4.0.13" + "@emotion/hash@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" +"@emotion/is-prop-valid@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.1.tgz#cbd843d409dfaad90f9404e7c0404c55eae8c134" + integrity sha512-bW1Tos67CZkOURLc0OalnfxtSXQJMrAMV0jZTVGJUPSOd4qgjF3+tTD5CwJM13PHA8cltGW1WGbbvV9NpvUZPw== + dependencies: + "@emotion/memoize" "^0.7.4" + +"@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" + integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== + +"@emotion/react@^11.7.1": + version "11.7.1" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.7.1.tgz#3f800ce9b20317c13e77b8489ac4a0b922b2fe07" + integrity sha512-DV2Xe3yhkF1yT4uAUoJcYL1AmrnO5SVsdfvu+fBuS7IbByDeTVx9+wFmvx9Idzv7/78+9Mgx2Hcmr7Fex3tIyw== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/cache" "^11.7.1" + "@emotion/serialize" "^1.0.2" + "@emotion/sheet" "^1.1.0" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965" + integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A== + dependencies: + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.4" + "@emotion/unitless" "^0.7.5" + "@emotion/utils" "^1.0.0" + csstype "^3.0.2" + +"@emotion/sheet@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2" + integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g== + +"@emotion/styled@^11.6.0": + version "11.6.0" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.6.0.tgz#9230d1a7bcb2ebf83c6a579f4c80e0664132d81d" + integrity sha512-mxVtVyIOTmCAkFbwIp+nCjTXJNgcz4VWkOYQro87jE2QBTydnkiYusMrRGFtzuruiGK4dDaNORk4gH049iiQuw== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/babel-plugin" "^11.3.0" + "@emotion/is-prop-valid" "^1.1.1" + "@emotion/serialize" "^1.0.2" + "@emotion/utils" "^1.0.0" + +"@emotion/unitless@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + +"@emotion/utils@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af" + integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA== + +"@emotion/weak-memoize@^0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + "@fortawesome/fontawesome-common-types@^0.2.35": version "0.2.35" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.35.tgz#01dd3d054da07a00b764d78748df20daf2b317e9" @@ -2420,6 +2516,95 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" +"@mui/base@5.0.0-alpha.62": + version "5.0.0-alpha.62" + resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.62.tgz#d86ff07f33d7b49ebb545f29e184750c0270c3f8" + integrity sha512-ItmdSZwHKQbLbAsS3sWguR7OHqYqh2cYWahoVmHb13Kc6bMdmVUTY4x57IlDSU712B0yuA0Q/gPTq7xADKnFow== + dependencies: + "@babel/runtime" "^7.16.3" + "@emotion/is-prop-valid" "^1.1.1" + "@mui/utils" "^5.2.3" + "@popperjs/core" "^2.4.4" + clsx "^1.1.1" + prop-types "^15.7.2" + react-is "^17.0.2" + +"@mui/material@^5.2.6": + version "5.2.6" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.2.6.tgz#fda165759281fe7d6f0ec2744a255f283e17b7aa" + integrity sha512-yF2bRqyJMo6bYXT7TPA9IU/XLaXHi47Xvmj8duQa5ha3bCpFMXLfGoZcAUl6ZDjjGEz1nCFS+c1qx219xD/aeQ== + dependencies: + "@babel/runtime" "^7.16.3" + "@mui/base" "5.0.0-alpha.62" + "@mui/system" "^5.2.6" + "@mui/types" "^7.1.0" + "@mui/utils" "^5.2.3" + "@types/react-transition-group" "^4.4.4" + clsx "^1.1.1" + csstype "^3.0.10" + hoist-non-react-statics "^3.3.2" + prop-types "^15.7.2" + react-is "^17.0.2" + react-transition-group "^4.4.2" + +"@mui/private-theming@^5.2.3": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.2.3.tgz#6d4e7d8309adc932b444fdd091caec339c430be4" + integrity sha512-Lc1Cmu8lSsYZiXADi9PBb17Ho82ZbseHQujUFAcp6bCJ5x/d+87JYCIpCBMagPu/isRlFCwbziuXPmz7WOzJPQ== + dependencies: + "@babel/runtime" "^7.16.3" + "@mui/utils" "^5.2.3" + prop-types "^15.7.2" + +"@mui/styled-engine@^5.2.6": + version "5.2.6" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.2.6.tgz#eac4a98b05b17190c2155b31b0e36338b3fb09f2" + integrity sha512-bqAhli8eGS6v2qxivy2/4K0Ag8o//jsu1G2G6QcieFiT6y7oIF/nd/6Tvw6OSm3roOTifVQWNKwkt1yFWhGS+w== + dependencies: + "@babel/runtime" "^7.16.3" + "@emotion/cache" "^11.7.1" + prop-types "^15.7.2" + +"@mui/system@^5.2.6": + version "5.2.6" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.2.6.tgz#153b534223caae254a98162eef06d6ab48ecff34" + integrity sha512-PZ7bmpWOLikWgqn2zWv9/Xa7lxnRBOmfjoMH7c/IVYJs78W3971brXJ3xV9MEWWQcoqiYQeXzUJaNf4rFbKCBA== + dependencies: + "@babel/runtime" "^7.16.3" + "@mui/private-theming" "^5.2.3" + "@mui/styled-engine" "^5.2.6" + "@mui/types" "^7.1.0" + "@mui/utils" "^5.2.3" + clsx "^1.1.1" + csstype "^3.0.10" + prop-types "^15.7.2" + +"@mui/types@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.0.tgz#5ed928c5a41cfbf9a4be82ea3bbdc47bcc9610d5" + integrity sha512-Hh7ALdq/GjfIwLvqH3XftuY3bcKhupktTm+S6qRIDGOtPtRuq2L21VWzOK4p7kblirK0XgGVH5BLwa6u8z/6QQ== + +"@mui/utils@^5.2.3": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.2.3.tgz#994f3a500679804483732596fcfa531e59c56445" + integrity sha512-sQujlajIS0zQKcGIS6tZR0L1R+ib26B6UtuEn+cZqwKHsPo3feuS+SkdscYBdcCdMbrZs4gj8WIJHl2z6tbSzQ== + dependencies: + "@babel/runtime" "^7.16.3" + "@types/prop-types" "^15.7.4" + "@types/react-is" "^16.7.1 || ^17.0.0" + prop-types "^15.7.2" + react-is "^17.0.2" + +"@mui/x-data-grid@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@mui/x-data-grid/-/x-data-grid-5.2.1.tgz#5c60e62e35d01507c9ec3b670d20a1cd408d312a" + integrity sha512-d1kNZnISZy74uz9mDitSRqdOPLTyqPXue/BQJV42evZ2aGbEIo5WhrGrQUhz/wgfigGaeybphWo8b8+efnVjGw== + dependencies: + "@mui/utils" "^5.2.3" + clsx "^1.1.1" + prop-types "^15.7.2" + reselect "^4.1.5" + "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents": version "2.1.8-no-fsevents" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.tgz#da7c3996b8e6e19ebd14d82eaced2313e7769f9b" @@ -2459,6 +2644,11 @@ "@nodelib/fs.scandir" "2.1.4" fastq "^1.6.0" +"@popperjs/core@^2.4.4": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.0.tgz#6734f8ebc106a0860dff7f92bf90df193f0935d7" + integrity sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -2709,16 +2899,35 @@ version "15.7.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" +"@types/prop-types@^15.7.4": + version "15.7.4" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" + integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + "@types/q@^1.5.1": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" +"@types/react-is@^16.7.1 || ^17.0.0": + version "17.0.3" + resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a" + integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw== + dependencies: + "@types/react" "*" + "@types/react-transition-group@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d" dependencies: "@types/react" "*" +"@types/react-transition-group@^4.4.4": + version "4.4.4" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e" + integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug== + dependencies: + "@types/react" "*" + "@types/react@*": version "16.9.43" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.43.tgz#c287f23f6189666ee3bebc2eb8d0f84bcb6cdb6b" @@ -3404,9 +3613,10 @@ babel-plugin-jest-hoist@^24.9.0: dependencies: "@types/babel__traverse" "^7.0.6" -babel-plugin-macros@2.8.0: +babel-plugin-macros@2.8.0, babel-plugin-macros@^2.6.1: version "2.8.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" + integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== dependencies: "@babel/runtime" "^7.7.2" cosmiconfig "^6.0.0" @@ -4016,9 +4226,10 @@ chess.js@^0.12.0: resolved "https://registry.yarnpkg.com/chess.js/-/chess.js-0.12.0.tgz#ffca1773b93a4aa044891de81286e264ca229b00" integrity sha512-eF1xf4j88r/AwIRxqV1FXZbVgMt4yUx05xrL+ZMqUrY+snFrUxGVHs0VgEd3AvngujDE9th7XZqqCwEKEQ7mWQ== -chessground@^7.6.12: +chessground@7.7.2, chessground@^7.6.12: version "7.7.2" resolved "https://registry.yarnpkg.com/chessground/-/chessground-7.7.2.tgz#bded8da6fb582ed256e882dde3b09bdcec4709f1" + integrity sha512-vJMu4tHf6vgzeqZNEZPsHUEy8Z6duZUY6A8j84qq7yC7cs+QQKThQoJEIOXu1rtj3AobRoitPNnSelybplmW0g== chokidar@^2.1.8: version "2.1.8" @@ -4164,7 +4375,7 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" -clsx@^1.0.2, clsx@^1.0.4: +clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" @@ -4360,6 +4571,13 @@ convert-source-map@^0.3.3: version "0.3.5" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" +convert-source-map@^1.5.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -4719,6 +4937,11 @@ csstype@^2.2.0, csstype@^2.5.2, csstype@^2.6.7: version "2.6.11" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.11.tgz#452f4d024149ecf260a852b025e36562a253ffc5" +csstype@^3.0.10: + version "3.0.10" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" + integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== + csstype@^3.0.2: version "3.0.7" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b" @@ -5315,6 +5538,11 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escodegen@^1.11.0, escodegen@^1.8.1, escodegen@^1.9.1: version "1.14.3" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" @@ -5903,6 +6131,11 @@ find-cache-dir@^3.2.0: make-dir "^3.0.2" pkg-dir "^4.1.0" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + find-up@4.1.0, find-up@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -6418,9 +6651,10 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== dependencies: react-is "^16.7.0" @@ -10119,7 +10353,7 @@ react-app-polyfill@^1.0.6: regenerator-runtime "^0.13.3" whatwg-fetch "^3.0.0" -react-chessground@^1.0.0: +react-chessground@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/react-chessground/-/react-chessground-1.5.0.tgz#fdc03c100b7d87053dc23e7015002261a3369103" integrity sha512-wwlfGHa41yvWa295dEgrzbvAR5GutYAYUd7I5Z86RfhSKNg1/vVF0L4LQ6YGdS5DUZoN2MHQzMECli3b0ELE/g== @@ -10241,7 +10475,7 @@ react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" -"react-is@^16.8.0 || ^17.0.0": +"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== @@ -10397,6 +10631,16 @@ react-transition-group@^4.4.0: loose-envify "^1.4.0" prop-types "^15.6.2" +react-transition-group@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" + integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^16.12.0: version "16.14.0" resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" @@ -10679,6 +10923,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.5.tgz#852c361247198da6756d07d9296c2b51eddb79f6" + integrity sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -11234,7 +11483,7 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, sourc version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" -source-map@^0.5.0, source-map@^0.5.6: +source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -11583,6 +11832,11 @@ stylehacks@^4.0.0: postcss "^7.0.0" postcss-selector-parser "^3.0.0" +stylis@4.0.13: + version "4.0.13" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91" + integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag== + superagent@^5.2.1: version "5.3.1" resolved "https://registry.yarnpkg.com/superagent/-/superagent-5.3.1.tgz#d62f3234d76b8138c1320e90fa83dc1850ccabf1" @@ -12194,9 +12448,9 @@ validate-npm-package-license@^3.0.1: spdx-expression-parse "^3.0.0" validator@^13.5.2: - version "13.5.2" - resolved "https://registry.yarnpkg.com/validator/-/validator-13.5.2.tgz#c97ae63ed4224999fb6f42c91eaca9567fe69a46" - integrity sha512-mD45p0rvHVBlY2Zuy3F3ESIe1h5X58GPfAtslBjY7EtTqGquZTj+VX/J4RnHWN8FKq0C9WRVt1oWAcytWRuYLQ== + version "13.7.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" + integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== vary@~1.1.2: version "1.1.2"