format leduc

This commit is contained in:
Lei Pan 2020-02-18 12:23:29 -08:00
parent c1650e64a5
commit 92a133e9c8
2 changed files with 337 additions and 112 deletions

View File

@ -74,7 +74,7 @@ class DoudizhuGameView extends React.Component {
console.log("Cannot find cards in move from player's hand"); console.log("Cannot find cards in move from player's hand");
} }
gameInfo.considerationTime = this.initConsiderationTime; gameInfo.considerationTime = this.initConsiderationTime;
gameInfo.completedPercent = (this.state.gameInfo.turn + 1) * 100.0 / this.moveHistory.length; gameInfo.completedPercent += 100.0 / this.moveHistory.length;
}else { }else {
console.log("Mismatched current player index"); console.log("Mismatched current player index");
} }
@ -283,7 +283,7 @@ class DoudizhuGameView extends React.Component {
render(){ render(){
let sliderValueText = (value) => { let sliderValueText = (value) => {
return `${value}°C`; return value;
}; };
const gameSpeedMarks = [ const gameSpeedMarks = [
{ {

View File

@ -4,8 +4,20 @@ import '../assets/gameview.scss';
import {LeducHoldemGameBoard} from '../components/GameBoard'; import {LeducHoldemGameBoard} from '../components/GameBoard';
import {deepCopy} from "../utils"; import {deepCopy} from "../utils";
import { Button, Layout } from 'element-react'; import { Layout } from 'element-react';
import Slider from '@material-ui/core/Slider'; 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 { class LeducHoldemGameView extends React.Component {
constructor(props) { constructor(props) {
@ -17,8 +29,10 @@ class LeducHoldemGameView extends React.Component {
this.gameStateTimeout = null; this.gameStateTimeout = null;
this.apiUrl = window.g.apiUrl; this.apiUrl = window.g.apiUrl;
this.moveHistory = []; this.moveHistory = [];
this.moveHistoryTotalLength = null;
this.gameStateHistory = [[],[]];
this.initGameState = { this.initGameState = {
gameStatus: "ready", // "ready", "playing", "paused", "over"
playerInfo: [], playerInfo: [],
hands: [], hands: [],
latestAction: ["", ""], latestAction: ["", ""],
@ -28,142 +42,353 @@ class LeducHoldemGameView extends React.Component {
pot: [1, 1], pot: [1, 1],
publicCard: "", publicCard: "",
currentPlayer: null, currentPlayer: null,
considerationTime: this.initConsiderationTime considerationTime: this.initConsiderationTime,
completedPercent: 0,
}; };
this.state = { this.state = {
gameInfo: this.initGameState, gameInfo: this.initGameState,
gameStateLoop: null, gameStateLoop: null,
considerationTimeSetting: this.initConsiderationTime gameSpeed: 0
} }
} }
retrieveReplayData(){ generateNewState(){
// for test use // console.log(this.state.gameInfo.latestAction);
const replayId = 0; let gameInfo = deepCopy(this.state.gameInfo);
const turn = this.state.gameInfo.turn;
// reset game state if(turn >= this.moveHistory[this.state.gameInfo.round].length){
this.setState({gameInfo: this.initGameState}); if(this.state.gameInfo.round === 0){
// todo: if it's the first round, then reveal the public card and start the second round
axios.get(`${this.apiUrl}/replay/leduc_holdem/${replayId}`) gameInfo.turn = 0;
.then(res => { gameInfo.round = 1;
res = res.data; gameInfo.latestAction = ["", ""];
this.moveHistory = res.moveHistory; gameInfo.currentPlayer = this.moveHistory[1][0].playerIdx;
let gameInfo = deepCopy(this.state.gameInfo); gameInfo.considerationTime = this.initConsiderationTime;
gameInfo.hands = res.initHands; // gameInfo.completedPercent += 100.0 / this.moveHistoryTotalLength;
gameInfo.playerInfo = res.playerInfo; this.setState({ gameInfo: gameInfo });
gameInfo.currentPlayer = res.moveHistory[0][0].playerIdx; }else{
gameInfo.publicCard = res.publicCard; gameInfo.gameStatus = "over";
this.setState({gameInfo: gameInfo}, ()=>{ this.setState({ gameInfo: gameInfo });
this.startReplay(); setTimeout(()=>{
}); // TODO: show winner
}) alert("Game Ends!");
.catch(err=>{ }, 200);
console.log("err", err); return gameInfo;
}) }
} }else{
if(gameInfo.currentPlayer === this.moveHistory[gameInfo.round][gameInfo.turn].playerIdx){
startReplay(){ gameInfo.latestAction[gameInfo.currentPlayer] = this.moveHistory[gameInfo.round][gameInfo.turn].move;
if(this.gameStateTimeout){ switch (gameInfo.latestAction[gameInfo.currentPlayer]) {
window.clearTimeout(this.gameStateTimeout); case "Check":
this.gameStateTimeout = null; 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 // if current state is new to game state history, push it to the game state history array
this.gameStateTimer(); 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(){ gameStateTimer(){
this.gameStateTimeout = setTimeout(()=>{ this.gameStateTimeout = setTimeout(()=>{
let currentConsiderationTime = this.state.gameInfo.considerationTime; let currentConsiderationTime = this.state.gameInfo.considerationTime;
if(currentConsiderationTime > 0) { 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); let gameInfo = deepCopy(this.state.gameInfo);
gameInfo.considerationTime = currentConsiderationTime; gameInfo.considerationTime = currentConsiderationTime;
this.setState({gameInfo: gameInfo}); this.setState({gameInfo: gameInfo});
this.gameStateTimer(); this.gameStateTimer();
}else{ }else{
console.log(this.state.gameInfo.latestAction); let gameInfo = this.generateNewState();
const turn = this.state.gameInfo.turn; if(gameInfo.gameStatus == "over") return;
if(turn >= this.moveHistory[this.state.gameInfo.round].length){ this.gameStateTimer();
if(this.state.gameInfo.round === 0){ gameInfo.gameStatus = "playing";
// todo: if it's the first round, then reveal the public card and start the second round if(this.state.gameInfo.toggleFade === "fade-out") {
let gameInfo = deepCopy(this.state.gameInfo); gameInfo.toggleFade = "fade-in";
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");
}
} }
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); }, 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 <Button className={"status-button"} variant={"contained"} startIcon={<PlayArrowRoundedIcon />} color="primary" onClick={()=>{this.startReplay()}}>Start</Button>;
case "playing":
return <Button className={"status-button"} variant={"contained"} startIcon={<PauseCircleOutlineRoundedIcon />} color="secondary" onClick={()=>{this.pauseReplay()}}>Pause</Button>;
case "paused":
return <Button className={"status-button"} variant={"contained"} startIcon={<PlayArrowRoundedIcon />} color="primary" onClick={()=>{this.resumeReplay()}}>Resume</Button>;
case "over":
return <Button className={"status-button"} variant={"contained"} startIcon={<ReplayRoundedIcon />} color="primary" onClick={()=>{this.startReplay()}}>Restart</Button>;
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(){ 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 ( return (
<div> <div className={"leduc-view-container"}>
<div style={{width: "960px", height: "540px"}}> <Layout.Row style={{"height": "540px"}}>
<LeducHoldemGameBoard <Layout.Col style={{"height": "100%"}} span="17">
playerInfo={this.state.gameInfo.playerInfo} <div style={{"height": "100%"}}>
hands={this.state.gameInfo.hands} <Paper className={"leduc-gameboard-paper"} elevation={3}>
latestAction={this.state.gameInfo.latestAction} <LeducHoldemGameBoard
mainPlayerId={this.state.gameInfo.mainViewerId} playerInfo={this.state.gameInfo.playerInfo}
currentPlayer={this.state.gameInfo.currentPlayer} hands={this.state.gameInfo.hands}
considerationTime={this.state.gameInfo.considerationTime} latestAction={this.state.gameInfo.latestAction}
round={this.state.gameInfo.round} mainPlayerId={this.state.gameInfo.mainViewerId}
turn={this.state.gameInfo.turn} currentPlayer={this.state.gameInfo.currentPlayer}
pot={this.state.gameInfo.pot} considerationTime={this.state.gameInfo.considerationTime}
publicCard={this.state.gameInfo.publicCard} round={this.state.gameInfo.round}
/> turn={this.state.gameInfo.turn}
pot={this.state.gameInfo.pot}
publicCard={this.state.gameInfo.publicCard}
/>
</Paper>
</div>
</Layout.Col>
<Layout.Col span="7" style={{"height": "100%"}}>
<Paper className={"leduc-probability-paper"} elevation={3}>
<div className={"probability-player"}>
{
this.state.gameInfo.playerInfo.length > 0 ?
<span>Current Player: {this.state.gameInfo.currentPlayer}</span>
:
<span>Waiting...</span>
}
</div>
<Divider />
<div className={"probability-table"}>
<div className={"probability-item"}>
{/* {this.computeProbabilityItem(0)} */}
</div>
<div className={"probability-item"}>
{/* {this.computeProbabilityItem(1)} */}
</div>
<div className={"probability-item"}>
{/* {this.computeProbabilityItem(2)} */}
</div>
</div>
</Paper>
</Layout.Col>
</Layout.Row>
<div className="progress-bar">
<LinearProgress variant="determinate" value={this.state.gameInfo.completedPercent} />
</div> </div>
<div className="game-controller"> <div className="game-controller">
<Layout.Row> <Paper className={"game-controller-paper"} elevation={3}>
<Layout.Col span="24"> <Layout.Row style={{"height": "51px"}}>
<Button type="primary" onClick={()=>{this.retrieveReplayData()}}>Start</Button> <Layout.Col span="7" style={{"height": "51px", "lineHeight": "48px"}}>
</Layout.Col> <div>
</Layout.Row> <Button
variant="contained"
color="primary"
disabled={this.state.gameInfo.gameStatus !== "paused" || (this.state.gameInfo.round === 0 && this.state.gameInfo.turn === 0)}
onClick={()=>{this.go2PrevGameState()}}
>
<SkipPreviousIcon />
</Button>
{ this.gameStatusButton(this.state.gameInfo.gameStatus) }
<Button
variant="contained"
color="primary"
disabled={this.state.gameInfo.gameStatus !== "paused"}
onClick={()=>{this.go2NextGameState()}}
>
<SkipNextIcon />
</Button>
</div>
</Layout.Col>
<Layout.Col span="1" style={{"height": "100%", "width": "1px"}}>
<Divider orientation="vertical" />
</Layout.Col>
<Layout.Col span="3" style={{"height": "51px", "lineHeight": "51px", "marginLeft": "-1px", "marginRight": "-1px"}}>
<div style={{"textAlign": "center"}}>{`Turn: ${this.state.gameInfo.turn}`}</div>
</Layout.Col>
<Layout.Col span="1" style={{"height": "100%", "width": "1px"}}>
<Divider orientation="vertical" />
</Layout.Col>
<Layout.Col span="14">
<div>
<label className={"form-label-left"}>Game Speed</label>
<div style={{"marginLeft": "100px", "marginRight": "10px"}}>
<Slider
value={this.state.gameSpeed}
getAriaValueText={sliderValueText}
onChange={(e, newVal)=>{this.changeGameSpeed(newVal)}}
aria-labelledby="discrete-slider-custom"
step={1}
min={-3}
max={3}
track={false}
valueLabelDisplay="off"
marks={gameSpeedMarks}
/>
</div>
</div>
</Layout.Col>
</Layout.Row>
</Paper>
</div> </div>
</div> </div>
); );