diff --git a/src/view/DoudizhuGameView.js b/src/view/DoudizhuGameView.js index 5be4ebf..8930c62 100644 --- a/src/view/DoudizhuGameView.js +++ b/src/view/DoudizhuGameView.js @@ -74,7 +74,7 @@ class DoudizhuGameView extends React.Component { console.log("Cannot find cards in move from player's hand"); } gameInfo.considerationTime = this.initConsiderationTime; - gameInfo.completedPercent = (this.state.gameInfo.turn + 1) * 100.0 / this.moveHistory.length; + gameInfo.completedPercent += 100.0 / this.moveHistory.length; }else { console.log("Mismatched current player index"); } @@ -283,7 +283,7 @@ class DoudizhuGameView extends React.Component { render(){ let sliderValueText = (value) => { - return `${value}°C`; + return value; }; const gameSpeedMarks = [ { diff --git a/src/view/LeducHoldemGameView.js b/src/view/LeducHoldemGameView.js index 81691eb..8fc33b4 100644 --- a/src/view/LeducHoldemGameView.js +++ b/src/view/LeducHoldemGameView.js @@ -4,8 +4,20 @@ import '../assets/gameview.scss'; import {LeducHoldemGameBoard} from '../components/GameBoard'; import {deepCopy} 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 Paper from '@material-ui/core/Paper'; +import Divider from '@material-ui/core/Divider'; +import LinearProgress from '@material-ui/core/LinearProgress'; +import ButtonGroup from '@material-ui/core/ButtonGroup'; +import PlayArrowRoundedIcon from '@material-ui/icons/PlayArrowRounded'; +import PauseCircleOutlineRoundedIcon from '@material-ui/icons/PauseCircleOutlineRounded'; +import ReplayRoundedIcon from '@material-ui/icons/ReplayRounded'; +import NotInterestedIcon from '@material-ui/icons/NotInterested'; +import PlayArrowIcon from '@material-ui/icons/PlayArrow'; +import SkipNextIcon from '@material-ui/icons/SkipNext'; +import SkipPreviousIcon from '@material-ui/icons/SkipPrevious'; class LeducHoldemGameView extends React.Component { constructor(props) { @@ -17,8 +29,10 @@ class LeducHoldemGameView extends React.Component { this.gameStateTimeout = null; this.apiUrl = window.g.apiUrl; this.moveHistory = []; - + this.moveHistoryTotalLength = null; + this.gameStateHistory = [[],[]]; this.initGameState = { + gameStatus: "ready", // "ready", "playing", "paused", "over" playerInfo: [], hands: [], latestAction: ["", ""], @@ -28,142 +42,353 @@ class LeducHoldemGameView extends React.Component { pot: [1, 1], publicCard: "", currentPlayer: null, - considerationTime: this.initConsiderationTime + considerationTime: this.initConsiderationTime, + completedPercent: 0, }; this.state = { gameInfo: this.initGameState, gameStateLoop: null, - considerationTimeSetting: this.initConsiderationTime + gameSpeed: 0 } } - retrieveReplayData(){ - // for test use - const replayId = 0; - - // reset game state - this.setState({gameInfo: this.initGameState}); - - axios.get(`${this.apiUrl}/replay/leduc_holdem/${replayId}`) - .then(res => { - res = res.data; - this.moveHistory = res.moveHistory; - let gameInfo = deepCopy(this.state.gameInfo); - gameInfo.hands = res.initHands; - gameInfo.playerInfo = res.playerInfo; - gameInfo.currentPlayer = res.moveHistory[0][0].playerIdx; - gameInfo.publicCard = res.publicCard; - this.setState({gameInfo: gameInfo}, ()=>{ - this.startReplay(); - }); - }) - .catch(err=>{ - console.log("err", err); - }) - } - - startReplay(){ - if(this.gameStateTimeout){ - window.clearTimeout(this.gameStateTimeout); - this.gameStateTimeout = null; + generateNewState(){ + // console.log(this.state.gameInfo.latestAction); + let gameInfo = deepCopy(this.state.gameInfo); + const turn = this.state.gameInfo.turn; + if(turn >= this.moveHistory[this.state.gameInfo.round].length){ + if(this.state.gameInfo.round === 0){ + // todo: if it's the first round, then reveal the public card and start the second round + gameInfo.turn = 0; + gameInfo.round = 1; + gameInfo.latestAction = ["", ""]; + gameInfo.currentPlayer = this.moveHistory[1][0].playerIdx; + gameInfo.considerationTime = this.initConsiderationTime; + // gameInfo.completedPercent += 100.0 / this.moveHistoryTotalLength; + this.setState({ gameInfo: gameInfo }); + }else{ + gameInfo.gameStatus = "over"; + this.setState({ gameInfo: gameInfo }); + setTimeout(()=>{ + // TODO: show winner + alert("Game Ends!"); + }, 200); + return gameInfo; + } + }else{ + if(gameInfo.currentPlayer === this.moveHistory[gameInfo.round][gameInfo.turn].playerIdx){ + gameInfo.latestAction[gameInfo.currentPlayer] = this.moveHistory[gameInfo.round][gameInfo.turn].move; + switch (gameInfo.latestAction[gameInfo.currentPlayer]) { + case "Check": + break; + case "Raise": + gameInfo.pot[gameInfo.currentPlayer] += (gameInfo.round+1) * 2; + break; + case "Call": + // the upstream player must have bet more + if(gameInfo.pot[(gameInfo.currentPlayer+2-1)%2] > gameInfo.pot[gameInfo.currentPlayer]){ + gameInfo.pot[gameInfo.currentPlayer] = gameInfo.pot[(gameInfo.currentPlayer+2-1)%2]; + }else{ + console.log("Current player choose call but has bet more or equal to the upstream player"); + } + break; + case "Fold": + // if one player folds, game ends + const foldedFound = gameInfo.playerInfo.find(element=>{return element.index === gameInfo.currentPlayer}); + const foldedId = foldedFound ? foldedFound.id : -1; + const winnerFound = gameInfo.playerInfo.find(element=>{return element.index === (gameInfo.currentPlayer+1)%2}); + const winnerId = winnerFound ? winnerFound.id : -1; + gameInfo.gameStatus = "over"; + this.setState({ gameInfo: gameInfo }); + setTimeout(()=>{ + alert(`Player ${foldedId} folded, player ${winnerId} wins!`); + }, 200); + return gameInfo; + default: + console.log("Error in player's latest action"); + } + gameInfo.turn++; + gameInfo.currentPlayer = (gameInfo.currentPlayer+1)%2; + gameInfo.considerationTime = this.initConsiderationTime; + gameInfo.completedPercent += 100.0 / this.moveHistoryTotalLength; + this.setState({ gameInfo: gameInfo }); + }else{ + console.log("Mismatch in current player & move history"); + } } - // loop to update game state - this.gameStateTimer(); + // if current state is new to game state history, push it to the game state history array + if(gameInfo.turn === this.gameStateHistory[gameInfo.round].length){ + this.gameStateHistory[gameInfo.round].push(gameInfo); + }else{ + console.log("inconsistent game state history length and turn number"); + } + return gameInfo; } gameStateTimer(){ this.gameStateTimeout = setTimeout(()=>{ let currentConsiderationTime = this.state.gameInfo.considerationTime; if(currentConsiderationTime > 0) { - currentConsiderationTime -= this.considerationTimeDeduction; + currentConsiderationTime -= this.considerationTimeDeduction * Math.pow(2, this.state.gameSpeed); + currentConsiderationTime = currentConsiderationTime < 0 ? 0 : currentConsiderationTime; + if(currentConsiderationTime === 0 && this.state.gameSpeed < 2){ + let gameInfo = deepCopy(this.state.gameInfo); + gameInfo.toggleFade = "fade-out"; + this.setState({gameInfo: gameInfo}); + } let gameInfo = deepCopy(this.state.gameInfo); gameInfo.considerationTime = currentConsiderationTime; this.setState({gameInfo: gameInfo}); this.gameStateTimer(); }else{ - console.log(this.state.gameInfo.latestAction); - const turn = this.state.gameInfo.turn; - if(turn >= this.moveHistory[this.state.gameInfo.round].length){ - if(this.state.gameInfo.round === 0){ - // todo: if it's the first round, then reveal the public card and start the second round - let gameInfo = deepCopy(this.state.gameInfo); - gameInfo.turn = 0; - gameInfo.round = 1; - gameInfo.latestAction = ["", ""]; - gameInfo.currentPlayer = this.moveHistory[1][0].playerIdx; - gameInfo.considerationTime = this.state.considerationTimeSetting; - this.setState({gameInfo: gameInfo}, ()=>{ - this.gameStateTimer(); - }); - }else{ - alert('Game Ends!'); - } - }else{ - let gameInfo = deepCopy(this.state.gameInfo); - if(gameInfo.currentPlayer === this.moveHistory[gameInfo.round][gameInfo.turn].playerIdx){ - gameInfo.latestAction[gameInfo.currentPlayer] = this.moveHistory[gameInfo.round][gameInfo.turn].move; - switch (gameInfo.latestAction[gameInfo.currentPlayer]) { - case "Check": - break; - case "Raise": - gameInfo.pot[gameInfo.currentPlayer] += (gameInfo.round+1) * 2; - break; - case "Call": - // the upstream player must have bet more - if(gameInfo.pot[(gameInfo.currentPlayer+2-1)%2] > gameInfo.pot[gameInfo.currentPlayer]){ - gameInfo.pot[gameInfo.currentPlayer] = gameInfo.pot[(gameInfo.currentPlayer+2-1)%2]; - }else{ - console.log("Current player choose call but has bet more or equal to the upstream player"); - } - break; - case "Fold": - // if one player folds, game ends - const foldedFound = gameInfo.playerInfo.find(element=>{return element.index === gameInfo.currentPlayer}); - const foldedId = foldedFound ? foldedFound.id : -1; - const winnerFound = gameInfo.playerInfo.find(element=>{return element.index === (gameInfo.currentPlayer+1)%2}); - const winnerId = winnerFound ? winnerFound.id : -1; - alert(`Player ${foldedId} folded, player ${winnerId} wins!`); - return ; - default: - console.log("Error in player's latest action"); - } - gameInfo.turn++; - gameInfo.currentPlayer = (gameInfo.currentPlayer+1)%2; - gameInfo.considerationTime = this.state.considerationTimeSetting; - this.setState({gameInfo: gameInfo}, ()=>{ - this.gameStateTimer(); - }); - }else{ - console.log("Mismatch in current player & move history"); - } + let gameInfo = this.generateNewState(); + if(gameInfo.gameStatus == "over") return; + this.gameStateTimer(); + gameInfo.gameStatus = "playing"; + if(this.state.gameInfo.toggleFade === "fade-out") { + gameInfo.toggleFade = "fade-in"; } + this.setState({gameInfo: gameInfo}, ()=>{ + // toggle fade in + if(this.state.gameInfo.toggleFade !== ""){ + setTimeout(()=>{ + let gameInfo = deepCopy(this.state.gameInfo); + gameInfo.toggleFade = ""; + this.setState({gameInfo: gameInfo}); + }, 200); + } + }); } }, this.considerationTimeDeduction); } + startReplay() { + // for test use + const replayId = 0; + + axios.get(`${this.apiUrl}/replay/leduc_holdem/${replayId}`) + .then(res => { + res = res.data; + // init replay info + this.moveHistory = res.moveHistory; + this.moveHistoryTotalLength = this.moveHistory.reduce((count, round) => count + round.length, 0); + let gameInfo = deepCopy(this.initGameState); + gameInfo.gameStatus = "playing"; + gameInfo.playerInfo = res.playerInfo; + gameInfo.hands = res.initHands; + gameInfo.currentPlayer = res.moveHistory[0][0].playerIdx; + gameInfo.publicCard = res.publicCard; + this.gameStateHistory[gameInfo.round].push(gameInfo); + this.setState({gameInfo: gameInfo}, ()=>{ + if(this.gameStateTimeout){ + window.clearTimeout(this.gameStateTimeout); + this.gameStateTimeout = null; + } + // loop to update game state + 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){ + this.setState({gameSpeed: newVal}); + } + + gameStatusButton(status){ + switch (status) { + case "ready": + return ; + case "playing": + return ; + case "paused": + return ; + case "over": + return ; + default: + alert(`undefined game status: ${status}`); + } + } + + go2PrevGameState() { + let gameInfo = null; + if(this.state.gameInfo.turn === 0 && this.state.gameInfo.round !== 0){ + let prevRound = this.gameStateHistory[this.state.gameInfo.round-1]; + gameInfo = deepCopy(prevRound[prevRound.length-1]); + }else{ + gameInfo = deepCopy(this.gameStateHistory[this.state.gameInfo.round][this.state.gameInfo.turn - 1]); + } + gameInfo.gameStatus = "paused"; + gameInfo.toggleFade = ""; + this.setState({gameInfo: gameInfo}); + } + + go2NextGameState() { + let gameInfo = this.generateNewState(); + if(gameInfo.gameStatus === "over") { + this.setState({gameInfo: gameInfo}); + }else{ + gameInfo.gameStatus = "paused"; + gameInfo.toggleFade = ""; + this.setState({gameInfo: gameInfo}); + } + } + render(){ + let sliderValueText = (value) => { + return value; + }; + 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 ( -
-
- +
+ + +
+ + + +
+
+ + +
+ { + this.state.gameInfo.playerInfo.length > 0 ? + Current Player: {this.state.gameInfo.currentPlayer} + : + Waiting... + } +
+ +
+
+ {/* {this.computeProbabilityItem(0)} */} +
+
+ {/* {this.computeProbabilityItem(1)} */} +
+
+ {/* {this.computeProbabilityItem(2)} */} +
+
+
+
+
+
+
- - - - - + + + +
+ + { this.gameStatusButton(this.state.gameInfo.gameStatus) } + +
+
+ + + + +
{`Turn: ${this.state.gameInfo.turn}`}
+
+ + + + +
+ +
+ {this.changeGameSpeed(newVal)}} + aria-labelledby="discrete-slider-custom" + step={1} + min={-3} + max={3} + track={false} + valueLabelDisplay="off" + marks={gameSpeedMarks} + /> +
+
+
+
+
);