diff --git a/package.json b/package.json index 17e7f4d..5eece45 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@material-ui/icons": "^4.9.1", "@material-ui/core": "^4.9.0", "element-react": "^1.4.34", "element-theme-default": "^1.4.13", diff --git a/server/package.json b/server/package.json index cba61ac..1fd906f 100644 --- a/server/package.json +++ b/server/package.json @@ -11,6 +11,7 @@ "author": "", "license": "ISC", "dependencies": { + "@material-ui/icons": "^4.9.1", "cors": "^2.8.5", "express": "^4.17.1", "nodemon": "^2.0.2", diff --git a/server/sample_data/sample_doudizhu.json b/server/sample_data/sample_doudizhu.json index 71585c6..cfa93f1 100644 --- a/server/sample_data/sample_doudizhu.json +++ b/server/sample_data/sample_doudizhu.json @@ -8,143 +8,469 @@ { "id": 0, "index": 0, - "role": "peasant" + "role": "peasant", + "agentInfo": { + "name": "random", + "winRate": 0.333 + } }, { "id": 1, "index": 1, - "role": "peasant" + "role": "peasant", + "agentInfo": { + "name": "DQN", + "winRate": 0.555 + } }, { "id": 2, "index": 2, - "role": "landlord" + "role": "landlord", + "agentInfo": { + "name": "CFR", + "winRate": 0.666 + } } ], "moveHistory": [ { "playerIdx": 2, - "move": "H3 S3 D3 D5" + "move": "H3 S3 D3 D5", + "probabilities": [ + { + "move": "H3 S3 D3 D5", + "probability": 0.9 + }, + { + "move": "H3", + "probability": 0.05 + }, + { + "move": "D5", + "probability": 0.02 + } + ] }, { "playerIdx": 0, - "move": "S9 H9 D9 S3" + "move": "S9 H9 D9 S3", + "probabilities": [ + { + "move": "S9 H9 D9 S3", + "probability": 0.9 + }, + { + "move": "HQ CQ DQ S3", + "probability": 0.05 + }, + { + "move": "HQ CQ DQ C3", + "probability": 0.02 + } + ] }, { "playerIdx": 1, - "move": "P" + "move": "P", + "probabilities": [ + { + "move": "P", + "probability": 1 + } + ] }, { "playerIdx": 2, - "move": "SJ HJ DJ D7" + "move": "SJ HJ DJ D7", + "probabilities": [ + { + "move": "SJ HJ DJ D7", + "probability": 0.9 + }, + { + "move": "SJ HJ DJ C9", + "probability": 0.05 + }, + { + "move": "BJ RJ", + "probability": 0.02 + } + ] }, { "playerIdx": 0, - "move": "HQ CQ DQ C7" + "move": "HQ CQ DQ C7", + "probabilities": [ + { + "move": "HQ CQ DQ C7", + "probability": 0.9 + }, + { + "move": "HQ CQ DQ CJ", + "probability": 0.05 + }, + { + "move": "P", + "probability": 0.02 + } + ] }, { "playerIdx": 1, - "move": "P" + "move": "P", + "probabilities": [ + { + "move": "P", + "probability": 1 + } + ] }, { "playerIdx": 2, - "move": "P" + "move": "P", + "probabilities": [ + { + "move": "P", + "probability": 1 + } + ] }, { "playerIdx": 0, - "move": "C4 D4" + "move": "C4 D4", + "probabilities": [ + { + "move": "HK DK", + "probability": 0.5 + }, + { + "move": "C4 D4", + "probability": 0.2 + }, + { + "move": "S2 H2", + "probability": 0.1 + } + ] }, { "playerIdx": 1, - "move": "ST HT" + "move": "ST HT", + "probabilities": [ + { + "move": "S6 D6", + "probability": 0.5 + }, + { + "move": "ST HT", + "probability": 0.2 + }, + { + "move": "HA CA", + "probability": 0.1 + } + ] }, { "playerIdx": 2, - "move": "SK CK" + "move": "SK CK", + "probabilities": [ + { + "move": "P", + "probability": 0.5 + }, + { + "move": "RJ BJ", + "probability": 0.3 + }, + { + "move": "SK CK", + "probability": 0.2 + } + ] }, { "playerIdx": 0, - "move": "S2 H2" + "move": "S2 H2", + "probabilities": [ + { + "move": "P", + "probability": 0.5 + }, + { + "move": "HK DK", + "probability": 0.5 + } + ] }, { "playerIdx": 1, - "move": "P" + "move": "P", + "probabilities": [ + { + "move": "P", + "probability": 1 + } + ] }, { "playerIdx": 2, - "move": "P" + "move": "P", + "probabilities": [ + { + "move": "P", + "probability": 1 + } + ] }, { "playerIdx": 0, - "move": "S6 H6" + "move": "S6 H6", + "probabilities": [ + { + "move": "S6 H6", + "probability": 0.6 + }, + { + "move": "HK DK", + "probability": 0.2 + }, + { + "move": "CJ", + "probability": 0.1 + } + ] }, { "playerIdx": 1, - "move": "P" + "move": "P", + "probabilities": [ + { + "move": "P", + "probability": 0.8 + }, + { + "move": "S7 H7", + "probability": 0.1 + }, + { + "move": "HA CA", + "probability": 0.05 + } + ] }, { "playerIdx": 2, - "move": "CT DT" + "move": "CT DT", + "probabilities": [ + { + "move": "P", + "probability": 0.5 + }, + { + "move": "S7 H7", + "probability": 0.1 + }, + { + "move": "HA CA", + "probability": 0.05 + } + ] }, { "playerIdx": 0, - "move": "HK DK" + "move": "HK DK", + "probabilities": [ + { + "move": "HK DK", + "probability": 0.9 + }, + { + "move": "P", + "probability": 0.1 + } + ] }, { "playerIdx": 1, - "move": "P" + "move": "P", + "probabilities": [ + { + "move": "P", + "probability": 0.9 + }, + { + "move": "HA CA", + "probability": 0.1 + } + ] }, { "playerIdx": 2, - "move": "RJ BJ" + "move": "RJ BJ", + "probabilities": [ + { + "move": "RJ BJ", + "probability": 0.9 + }, + { + "move": "P", + "probability": 0.1 + } + ] }, { "playerIdx": 0, - "move": "P" + "move": "P", + "probabilities": [ + { + "move": "P", + "probability": 1 + } + ] }, { "playerIdx": 1, - "move": "P" + "move": "P", + "probabilities": [ + { + "move": "P", + "probability": 1 + } + ] }, { "playerIdx": 2, - "move": "S8 H8 C8 C9" + "move": "S8 H8 C8 C9", + "probabilities": [ + { + "move": "S8 H8 C8 C9", + "probability": 0.7 + }, + { + "move": "SA", + "probability": 0.1 + }, + { + "move": "D2", + "probability": 0.1 + } + ] }, { "playerIdx": 0, - "move": "P" + "move": "P", + "probabilities": [ + { + "move": "P", + "probability": 1 + } + ] }, { "playerIdx": 1, - "move": "HA CA DA H5" + "move": "HA CA DA H5", + "probabilities": [ + { + "move": "HA CA DA H5", + "probability": 0.5 + }, + { + "move": "P", + "probability": 0.1 + }, + { + "move": "HA CA DA D6", + "probability": 0.1 + } + ] }, { "playerIdx": 2, - "move": "P" + "move": "P", + "probabilities": [ + { + "move": "P", + "probability": 1 + } + ] }, { "playerIdx": 0, - "move": "P" + "move": "P", + "probabilities": [ + { + "move": "P", + "probability": 1 + } + ] }, { "playerIdx": 1, - "move": "SQ" + "move": "SQ", + "probabilities": [ + { + "move": "SQ", + "probability": 0.5 + }, + { + "move": "S4 H4", + "probability": 0.2 + }, + { + "move": "D6", + "probability": 0.1 + } + ] }, { "playerIdx": 2, - "move": "D2" + "move": "D2", + "probabilities": [ + { + "move": "D2", + "probability": 0.5 + }, + { + "move": "SA", + "probability": 0.3 + }, + { + "move": "P", + "probability": 0.2 + } + ] }, { "playerIdx": 0, - "move": "P" + "move": "P", + "probabilities": [ + { + "move": "P", + "probability": 1 + } + ] }, { "playerIdx": 1, - "move": "P" + "move": "P", + "probabilities": [ + { + "move": "P", + "probability": 1 + } + ] }, { "playerIdx": 2, - "move": "SA" + "move": "SA", + "probabilities": [ + { + "move": "SA", + "probability": 1 + } + ] } ] } \ No newline at end of file diff --git a/src/utils/index.js b/src/utils/index.js index e8a126c..fc6212a 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -74,4 +74,17 @@ export function millisecond2Second(t){ return Math.ceil(t/1000); } -export { suitMap, suitMapSymbol }; \ No newline at end of file +export function debounce(func, wait, immediate) { + let timeout; + return function() { + const context = this, args = arguments; + const later = function () { + timeout = null; + if (!immediate) func.apply(context, args); + }; + const callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; +} \ No newline at end of file diff --git a/src/view/DoudizhuGameView.js b/src/view/DoudizhuGameView.js index 4287a73..1d5be15 100644 --- a/src/view/DoudizhuGameView.js +++ b/src/view/DoudizhuGameView.js @@ -2,21 +2,26 @@ import React from 'react'; import '../assets/gameview.scss'; import { DoudizhuGameBoard } from '../components/GameBoard'; import webSocket from "socket.io-client"; -import { removeCards, doubleRaf, deepCopy } from "../utils"; +import { removeCards, doubleRaf, deepCopy, debounce } from "../utils"; -import { Button, Layout } from 'element-react'; +import { Layout } from 'element-react'; import Slider from '@material-ui/core/Slider'; +import Button from '@material-ui/core/Button'; +import PlayArrowRoundedIcon from '@material-ui/icons/PlayArrowRounded'; +import PauseCircleOutlineRoundedIcon from '@material-ui/icons/PauseCircleOutlineRounded'; +import ReplayRoundedIcon from '@material-ui/icons/ReplayRounded'; class DoudizhuGameView extends React.Component { constructor(props) { super(props); const mainViewerId = 0; // Id of the player at the bottom of screen - this.initConsiderationTime = 1000; + this.initConsiderationTime = 2000; this.considerationTimeDeduction = 100; this.gameStateTimeout = null; this.initGameState = { + gameStatus: "ready", // "ready", "playing", "paused", "over" playerInfo: [], hands: [], latestAction: [[], [], []], @@ -30,15 +35,21 @@ class DoudizhuGameView extends React.Component { ws: null, gameInfo: this.initGameState, gameStateLoop: null, - considerationTimeSetting: this.initConsiderationTime + gameSpeed: 0 }; } gameStateTimer() { this.gameStateTimeout = setTimeout(()=>{ let currentConsiderationTime = this.state.gameInfo.considerationTime; + // for test use + // console.log(currentConsiderationTime); + // if(currentConsiderationTime === 1000){ + // debugger; + // } if(currentConsiderationTime > 0) { - currentConsiderationTime -= this.considerationTimeDeduction; + currentConsiderationTime -= this.considerationTimeDeduction * Math.pow(2, this.state.gameSpeed); + currentConsiderationTime = currentConsiderationTime < 0 ? 0 : currentConsiderationTime; let gameInfo = deepCopy(this.state.gameInfo); gameInfo.considerationTime = currentConsiderationTime; this.setState({gameInfo: gameInfo}); @@ -53,7 +64,7 @@ class DoudizhuGameView extends React.Component { this.setState({gameInfo: gameInfo}); this.state.ws.emit("getMessage", gameStateReq); } - }, this.considerationTimeDeduction); + }, 100); } startReplay() { @@ -61,7 +72,11 @@ class DoudizhuGameView extends React.Component { const replayReq = {type: 0}; this.state.ws.emit("getMessage", replayReq); // init game state - this.setState({gameInfo: this.initGameState}); + let initGameState = deepCopy(this.initGameState); + // set game status to playing + initGameState.gameStatus = "playing"; + this.setState({gameInfo: initGameState}); + if(this.gameStateTimeout){ window.clearTimeout(this.gameStateTimeout); this.gameStateTimeout = null; @@ -104,7 +119,7 @@ class DoudizhuGameView extends React.Component { }else{ console.log("Cannot find cards in move from player's hand"); } - gameInfo.considerationTime = this.state.considerationTimeSetting; + gameInfo.considerationTime = this.initConsiderationTime; this.setState({gameInfo: gameInfo}); }else{ console.log("Mismatched game turn or current player index", message); @@ -127,6 +142,9 @@ class DoudizhuGameView extends React.Component { return element.index === prevTurn.currentPlayer; }); if(winner){ + let gameInfo = deepCopy(this.state.gameInfo); + gameInfo.gameStatus = "over"; + this.setState({ gameInfo: gameInfo }); if(winner.role === "landlord") alert("Landlord Wins"); else @@ -139,8 +157,79 @@ class DoudizhuGameView extends React.Component { this.gameStateTimer(); } + pauseReplay(){ + if(this.gameStateTimeout){ + window.clearTimeout(this.gameStateTimeout); + this.gameStateTimeout = null; + } + let gameInfo = deepCopy(this.state.gameInfo); + gameInfo.gameStatus = "paused"; + this.setState({ gameInfo: gameInfo }); + } + + resumeReplay(){ + this.gameStateTimer(); + let gameInfo = deepCopy(this.state.gameInfo); + gameInfo.gameStatus = "playing"; + this.setState({ gameInfo: gameInfo }); + } + + changeGameSpeed(newVal){ + console.log('wdnmd'); + this.setState({gameSpeed: newVal}); + } + + gameStatusButton(status){ + switch (status) { + case "ready": + return } color="primary" onClick={()=>{this.startReplay()}}>Start Replay; + case "playing": + return } color="secondary" onClick={()=>{this.pauseReplay()}}>Pause; + case "paused": + return } color="primary" onClick={()=>{this.resumeReplay()}}>Resume; + case "over": + return } color="primary" onClick={()=>{this.startReplay()}}>Resume; + default: + alert(`undefined game status: ${status}`); + } + return ; + } + render(){ - // todo: reset game state timer when considerationTimeSetting changes + let sliderValueText = (value) => { + return `${value}°C`; + }; + const gameSpeedMarks = [ + { + value: -3, + label: 'x0.125', + }, + { + value: -2, + label: 'x0.25', + }, + { + value: -1, + label: 'x0.5', + }, + { + value: 0, + label: 'x1', + }, + { + value: 1, + label: 'x2', + }, + { + value: 2, + label: 'x4', + }, + { + value: 3, + label: 'x8', + } + ]; + return (