Merge pull request #8 from datamllab/feature/pve-demo

Feature/pve demo
This commit is contained in:
Sony Huang 2021-04-22 22:02:19 -07:00 committed by GitHub
commit 8be57eef48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 910 additions and 197 deletions

15
.eslintrc Normal file
View File

@ -0,0 +1,15 @@
{
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier",
"prettier/react"
],
"rules": {
"no-redeclare": "off",
"react/display-name": "off",
"react/prop-types": "off",
"react/react-in-jsx-scope": "off"
}
}

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
# Folders
dist/
node_modules/
src/public/lib/

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"tabWidth": 4
}

View File

@ -1,28 +1,33 @@
import React from 'react'; import React from 'react';
import { BrowserRouter as Router, Route, Redirect } from "react-router-dom"; import { BrowserRouter as Router, Route, Redirect } from "react-router-dom";
import LeaderBoard from './view/LeaderBoard'; import LeaderBoard from './view/LeaderBoard';
import { DoudizhuGameView, LeducHoldemGameView } from './view/GameView'; import { DoudizhuReplayView, LeducHoldemReplayView } from './view/ReplayView';
import { PvEDoudizhuDemoView } from './view/PvEView';
import Navbar from "./components/Navbar"; import Navbar from "./components/Navbar";
import {makeStyles} from "@material-ui/core/styles";
const useStyles = makeStyles(() => ({ const navbarSubtitleMap = {
navBar: { "/leaderboard": "",
zIndex: 1002 "/replay/doudizhu": "Doudizhu",
} "/replay/leduc-holdem": "Leduc Hold'em",
})); "/pve/doudizhu-demo": "Doudizhu PvE Demo"
};
function App() { function App() {
const classes = useStyles(); // todo: add 404 page
return ( return (
<Router> <Router>
<Navbar className={classes.navBar} gameName={""}/> <Navbar subtitleMap={navbarSubtitleMap}/>
<div style={{marginTop: '75px'}}> <div style={{marginTop: '75px'}}>
<Route exact path="/"> <Route exact path="/">
<Redirect to="/leaderboard?type=game&name=leduc-holdem" /> {/* for test use */}
{/* <Redirect to="/leaderboard?type=game&name=leduc-holdem" /> */}
<Redirect to="/pve/doudizhu-demo" />
</Route> </Route>
<Route path="/leaderboard" component={LeaderBoard} /> <Route path="/leaderboard" component={LeaderBoard} />
<Route path="/replay/doudizhu" component={DoudizhuGameView} /> <Route path="/replay/doudizhu" component={DoudizhuReplayView} />
<Route path="/replay/leduc-holdem" component={LeducHoldemGameView} /> <Route path="/replay/leduc-holdem" component={LeducHoldemReplayView} />
<Route path="/pve/doudizhu-demo" component={PvEDoudizhuDemoView} />
</div> </div>
</Router> </Router>
); );

View File

@ -37,8 +37,8 @@
.playingCards .card { .playingCards .card {
display: inline-block; display: inline-block;
width: 3.3em; width: 2.5em;
height: 4.6em; height: 3.5em;
border: 1px solid #666; border: 1px solid #666;
border-radius: .3em; border-radius: .3em;
-moz-border-radius: .3em; -moz-border-radius: .3em;
@ -47,7 +47,7 @@
padding: .25em; padding: .25em;
margin: 0 .5em .5em 0; margin: 0 .5em .5em 0;
text-align: center; text-align: center;
font-size: 1.2em; /* @change: adjust this value to make bigger or smaller cards */ font-size: 1.5em; /* @change: adjust this value to make bigger or smaller cards */
font-weight: normal; font-weight: normal;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
position: relative; position: relative;
@ -60,8 +60,28 @@
.playingCards a.card { .playingCards a.card {
text-decoration: none; text-decoration: none;
} }
.playingCards.selectable label.card {
cursor: pointer;
}
/* override orignal hover style */
.playingCards.selectable label.card:hover {
bottom: 0;
}
.playingCards.selectable label.card:active::after {
background-color: rgba(23, 146, 210, 0.5);
content: '';
position: absolute;
left: 0;
top: 0;
width: 3em;
height: 4em;
}
/* selected and hover state */ /* selected and hover state */
.playingCards a.card:hover, .playingCards a.card:active, .playingCards a.card:hover, .playingCards a.card:active, .playingCards.selectable label.card.selected,
.playingCards label.card:hover, .playingCards label.card:hover,
.playingCards strong .card { .playingCards strong .card {
bottom: 1em; bottom: 1em;

View File

@ -27,6 +27,16 @@
opacity: 0; opacity: 0;
} }
.main-player-action-wrapper {
display: flex;
align-items: center;
button {
font-weight: bolder;
border-radius: 40px;
}
}
.timer { .timer {
visibility: visible; visibility: visible;
transition: visibility 0s, opacity 0.2s, transform 0.3s; transition: visibility 0s, opacity 0.2s, transform 0.3s;
@ -35,7 +45,7 @@
height: 60px; height: 60px;
.timer-text { .timer-text {
color: #303133; color: #303133;
margin-top: 5px; transform: translateY(5px);
font-size: 23px; font-size: 23px;
font-weight: bold; font-weight: bold;
text-shadow: 0 2px 2px #909399; text-shadow: 0 2px 2px #909399;

View File

@ -1,82 +1,84 @@
import Avatar from '@material-ui/core/Avatar';
import Button from '@material-ui/core/Button';
import Chip from '@material-ui/core/Chip';
import React from 'react'; import React from 'react';
import { translateCardData, millisecond2Second, computeHandCardsWidth } from '../../utils'
import '../../assets/doudizhu.scss'; import '../../assets/doudizhu.scss';
import Landlord_wName from '../../assets/images/Portrait/Landlord_wName.png'; import Landlord_wName from '../../assets/images/Portrait/Landlord_wName.png';
import Peasant_wName from '../../assets/images/Portrait/Peasant_wName.png'; import Peasant_wName from '../../assets/images/Portrait/Peasant_wName.png';
import PlaceHolderPlayer from '../../assets/images/Portrait/Player.png'; import PlaceHolderPlayer from '../../assets/images/Portrait/Player.png';
import { computeHandCardsWidth, millisecond2Second, translateCardData } from '../../utils';
import Chip from '@material-ui/core/Chip';
import Avatar from '@material-ui/core/Avatar';
class DoudizhuGameBoard extends React.Component { class DoudizhuGameBoard extends React.Component {
computePlayerPortrait(playerId, playerIdx){ computePlayerPortrait(playerId, playerIdx) {
if(this.props.playerInfo.length > 0){ if (this.props.playerInfo.length > 0) {
return this.props.playerInfo[playerIdx].role === "landlord" ? return this.props.playerInfo[playerIdx].role === 'landlord' ? (
<div> <div>
<img src={Landlord_wName} alt={"Landlord"} height="70%" width="70%" /> <img src={Landlord_wName} alt={'Landlord'} height="70%" width="70%" />
<Chip <Chip avatar={<Avatar>ID</Avatar>} label={playerId} clickable color="primary" />
avatar={<Avatar>ID</Avatar>}
label={playerId}
clickable
color="primary"
/>
</div> </div>
: ) : (
<div> <div>
<img src={Peasant_wName} alt={"Peasant"} height="70%" width="70%" /> <img src={Peasant_wName} alt={'Peasant'} height="70%" width="70%" />
<Chip <Chip avatar={<Avatar>ID</Avatar>} label={playerId} clickable color="primary" />
avatar={<Avatar>ID</Avatar>}
label={playerId}
clickable
color="primary"
/>
</div> </div>
}else );
} else
return ( return (
<div> <div>
<img src={PlaceHolderPlayer} alt={"Player"} height="70%" width="70%" /> <img src={PlaceHolderPlayer} alt={'Player'} height="70%" width="70%" />
<Chip <Chip avatar={<Avatar>ID</Avatar>} label={playerId} clickable color="primary" />
avatar={<Avatar>ID</Avatar>}
label={playerId}
clickable
color="primary"
/>
</div> </div>
) );
} }
computeSingleLineHand(cards, fadeClassName="") { computeSingleLineHand(cards, fadeClassName = '', cardSelectable = false) {
if(cards === "pass"){ if (cards === 'pass') {
return <div className="non-card"><span>PASS</span></div>
}else{
return ( return (
<div className={"playingCards unselectable loose "+fadeClassName}> <div className="non-card">
<ul className="hand" style={{width: computeHandCardsWidth(cards.length, 12)}}> <span>PASS</span>
{cards.map(card=>{ </div>
);
} else {
return (
<div
className={`playingCards loose ${fadeClassName} ${
this.props.gamePlayable && cardSelectable ? 'selectable' : 'unselectable'
}`}
>
<ul className="hand" style={{ width: computeHandCardsWidth(cards.length, 12) }}>
{cards.map((card) => {
const [rankClass, suitClass, rankText, suitText] = translateCardData(card); const [rankClass, suitClass, rankText, suitText] = translateCardData(card);
let selected = false;
if (this.props.gamePlayable && cardSelectable) {
selected = this.props.selectedCards.indexOf(card) >= 0;
}
// todo: right click and move to select multiple cards
return ( return (
<li key={`handCard-${card}`}> <li key={`handCard-${card}`}>
<a className={`card ${rankClass} ${suitClass}`} href="/#"> <label
onClick={() => this.props.handleSelectedCards([card])}
className={`card ${rankClass} ${suitClass} ${selected ? 'selected' : ''}`}
>
<span className="rank">{rankText}</span> <span className="rank">{rankText}</span>
<span className="suit">{suitText}</span> <span className="suit">{suitText}</span>
</a> </label>
</li> </li>
); );
})} })}
</ul> </ul>
</div> </div>
) );
} }
} }
computeSideHand(cards) { computeSideHand(cards) {
let upCards; let upCards;
let downCards = []; let downCards = [];
if(cards.length > 10){ if (cards.length > 10) {
upCards = cards.slice(0, 10); upCards = cards.slice(0, 10);
downCards = cards.slice(10, ); downCards = cards.slice(10);
}else{ } else {
upCards = cards; upCards = cards;
} }
return ( return (
@ -84,11 +86,11 @@ class DoudizhuGameBoard extends React.Component {
<div className="player-hand-up"> <div className="player-hand-up">
<div className="playingCards unselectable loose"> <div className="playingCards unselectable loose">
<ul className="hand"> <ul className="hand">
{upCards.map(card => { {upCards.map((card) => {
const [rankClass, suitClass, rankText, suitText] = translateCardData(card); const [rankClass, suitClass, rankText, suitText] = translateCardData(card);
return ( return (
<li key={`handCard-${card}`}> <li key={`handCard-${card}`}>
<a className={`card ${rankClass} ${suitClass}`} href="/#"> <a className={`card ${rankClass} ${suitClass}`} href="javascript:void(0);">
<span className="rank">{rankText}</span> <span className="rank">{rankText}</span>
<span className="suit">{suitText}</span> <span className="suit">{suitText}</span>
</a> </a>
@ -101,11 +103,11 @@ class DoudizhuGameBoard extends React.Component {
<div className="player-hand-down"> <div className="player-hand-down">
<div className="playingCards unselectable loose"> <div className="playingCards unselectable loose">
<ul className="hand"> <ul className="hand">
{downCards.map(card => { {downCards.map((card) => {
const [rankClass, suitClass, rankText, suitText] = translateCardData(card); const [rankClass, suitClass, rankText, suitText] = translateCardData(card);
return ( return (
<li key={`handCard-${card}`}> <li key={`handCard-${card}`}>
<a className={`card ${rankClass} ${suitClass}`} href="/#"> <a className={`card ${rankClass} ${suitClass}`} href="javascript:void(0);">
<span className="rank">{rankText}</span> <span className="rank">{rankText}</span>
<span className="suit">{suitText}</span> <span className="suit">{suitText}</span>
</a> </a>
@ -116,28 +118,69 @@ class DoudizhuGameBoard extends React.Component {
</div> </div>
</div> </div>
</div> </div>
) );
} }
playerDecisionArea(playerIdx){ playerDecisionArea(playerIdx) {
let fadeClassName = ""; let fadeClassName = '';
if(this.props.toggleFade === "fade-out" && (playerIdx+2)%3 === this.props.currentPlayer) if (this.props.toggleFade === 'fade-out' && (playerIdx + 2) % 3 === this.props.currentPlayer)
fadeClassName = "fade-out"; fadeClassName = 'fade-out';
else if(this.props.toggleFade === "fade-in" && (playerIdx+1)%3 === this.props.currentPlayer) else if (this.props.toggleFade === 'fade-in' && (playerIdx + 1) % 3 === this.props.currentPlayer)
fadeClassName = "scale-fade-in"; fadeClassName = 'scale-fade-in';
if(this.props.currentPlayer === playerIdx){ if (this.props.currentPlayer === playerIdx) {
if (this.props.mainPlayerId === this.props.playerInfo[this.props.currentPlayer].id) {
return ( return (
<div className={"timer "+fadeClassName}> <div className={'main-player-action-wrapper'}>
<div style={{ marginRight: '2em' }} className={'timer ' + fadeClassName}>
<div className="timer-text">{millisecond2Second(this.props.considerationTime)}</div> <div className="timer-text">{millisecond2Second(this.props.considerationTime)}</div>
</div> </div>
) <Button
}else{ onClick={() => {
return this.computeSingleLineHand(this.props.latestAction[playerIdx], fadeClassName) this.props.handleMainPlayerAct('deselect');
}}
style={{ marginRight: '2em' }}
variant="contained"
color="primary"
>
Deselect
</Button>
<Button
disabled={this.props.isPassDisabled}
onClick={() => {
this.props.handleMainPlayerAct('pass');
}}
style={{ marginRight: '2em' }}
variant="contained"
color="primary"
>
Pass
</Button>
<Button
disabled={this.props.selectedCards.length === 0}
onClick={() => {
this.props.handleMainPlayerAct('play');
}}
variant="contained"
color="primary"
>
Play
</Button>
</div>
);
} else {
return (
<div className={'timer ' + fadeClassName}>
<div className="timer-text">{millisecond2Second(this.props.considerationTime)}</div>
</div>
);
}
} else {
return this.computeSingleLineHand(this.props.latestAction[playerIdx], fadeClassName);
} }
} }
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
if(prevProps.turn !== this.props.turn && this.props.turn !== 0 && this.props.gameStatus === "playing"){ if (prevProps.turn !== this.props.turn && this.props.turn !== 0 && this.props.gameStatus === 'playing') {
// new turn starts // new turn starts
this.props.runNewTurn(prevProps); this.props.runNewTurn(prevProps);
} }
@ -146,59 +189,65 @@ class DoudizhuGameBoard extends React.Component {
render() { render() {
// compute the id as well as index in list for every player // compute the id as well as index in list for every player
const bottomId = this.props.mainPlayerId; const bottomId = this.props.mainPlayerId;
let found = this.props.playerInfo.find(element=>{ let found = this.props.playerInfo.find((element) => {
return element.id === bottomId; return element.id === bottomId;
}); });
const bottomIdx = found ? found.index : -1; const bottomIdx = found ? found.index : -1;
const rightIdx = bottomIdx >= 0 ? (bottomIdx+1)%3 : -1; const rightIdx = bottomIdx >= 0 ? (bottomIdx + 1) % 3 : -1;
const leftIdx = rightIdx >= 0 ? (rightIdx+1)%3 : -1; const leftIdx = rightIdx >= 0 ? (rightIdx + 1) % 3 : -1;
let rightId = -1; let rightId = -1;
let leftId = -1; let leftId = -1;
if(rightIdx >= 0 && leftIdx >= 0){ if (rightIdx >= 0 && leftIdx >= 0) {
found = this.props.playerInfo.find(element=>{ found = this.props.playerInfo.find((element) => {
return element.index === rightIdx; return element.index === rightIdx;
}); });
if(found) if (found) rightId = found.id;
rightId = found.id; found = this.props.playerInfo.find((element) => {
found = this.props.playerInfo.find(element=>{
return element.index === leftIdx; return element.index === leftIdx;
}); });
if(found) if (found) leftId = found.id;
leftId = found.id;
} }
return ( return (
<div className="doudizhu-wrapper" style={{}}> <div className="doudizhu-wrapper" style={{}}>
<div id={"left-player"}> <div id={'left-player'}>
<div className="player-main-area"> <div className="player-main-area">
<div className="player-info"> <div className="player-info">{this.computePlayerPortrait(leftId, leftIdx)}</div>
{this.computePlayerPortrait(leftId, leftIdx)} {leftIdx >= 0 ? (
this.computeSideHand(this.props.hands[leftIdx])
) : (
<div className="player-hand-placeholder">
<span>Waiting...</span>
</div> </div>
{leftIdx >= 0 ? this.computeSideHand(this.props.hands[leftIdx]) : <div className="player-hand-placeholder"><span>Waiting...</span></div>} )}
</div> </div>
<div className="played-card-area"> <div className="played-card-area">{leftIdx >= 0 ? this.playerDecisionArea(leftIdx) : ''}</div>
{leftIdx >= 0 ? this.playerDecisionArea(leftIdx) : ""}
</div> </div>
</div> <div id={'right-player'}>
<div id={"right-player"}>
<div className="player-main-area"> <div className="player-main-area">
<div className="player-info"> <div className="player-info">{this.computePlayerPortrait(rightId, rightIdx)}</div>
{this.computePlayerPortrait(rightId, rightIdx)} {rightIdx >= 0 ? (
this.computeSideHand(this.props.hands[rightIdx])
) : (
<div className="player-hand-placeholder">
<span>Waiting...</span>
</div> </div>
{rightIdx >= 0 ? this.computeSideHand(this.props.hands[rightIdx]) : <div className="player-hand-placeholder"><span>Waiting...</span></div>} )}
</div> </div>
<div className="played-card-area"> <div className="played-card-area">{rightIdx >= 0 ? this.playerDecisionArea(rightIdx) : ''}</div>
{rightIdx >= 0 ? this.playerDecisionArea(rightIdx) : ""}
</div>
</div>
<div id={"bottom-player"}>
<div className="played-card-area">
{bottomIdx >= 0 ? this.playerDecisionArea(bottomIdx) : ""}
</div> </div>
<div id={'bottom-player'}>
<div className="played-card-area">{bottomIdx >= 0 ? this.playerDecisionArea(bottomIdx) : ''}</div>
<div className="player-main-area"> <div className="player-main-area">
<div className="player-info"> <div className="player-info">{this.computePlayerPortrait(bottomId, bottomIdx)}</div>
{this.computePlayerPortrait(bottomId, bottomIdx)} {bottomIdx >= 0 ? (
<div className="player-hand">
{this.computeSingleLineHand(this.props.hands[bottomIdx], '', true)}
</div> </div>
{bottomIdx >= 0 ? <div className="player-hand">{this.computeSingleLineHand(this.props.hands[bottomIdx])}</div> : <div className="player-hand-placeholder"><span>Waiting...</span></div>} ) : (
<div className="player-hand-placeholder">
<span>Waiting...</span>
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,39 +1,37 @@
import React from "react"; import React, { useEffect, useState } from "react";
import { Link } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import logo_white from "../assets/images/logo_white.png"; import logo_white from "../assets/images/logo_white.png";
import GitHubIcon from "@material-ui/icons/GitHub"; import GitHubIcon from "@material-ui/icons/GitHub";
import AppBar from "@material-ui/core/AppBar"; import AppBar from "@material-ui/core/AppBar";
import axios from 'axios'; import axios from 'axios';
class Navbar extends React.Component { function Navbar({subtitleMap}) {
constructor(props) { const [stars, setStars] = useState('...');
super(props); let location = useLocation();
console.log(location.pathname, subtitleMap);
const subtitle = subtitleMap[location.pathname];
this.state = {stars: '...'}; useEffect(() => {
}
componentDidMount() {
axios.get("https://api.github.com/repos/datamllab/rlcard") axios.get("https://api.github.com/repos/datamllab/rlcard")
.then(res=>{ .then(res=>{
this.setState({stars: res.data.stargazers_count}); setStars(res.data.stargazers_count);
}); });
} }, [])
render() {
return ( return (
<AppBar position="fixed" className={"header-bar-wrapper"}> <AppBar position="fixed" className={"header-bar-wrapper"}>
<div className={"header-bar"}> <div className={"header-bar"}>
<Link to="/leaderboard"><img src={logo_white} alt={"Logo"} height="65px" /></Link> <Link to="/leaderboard"><img src={logo_white} alt={"Logo"} height="65px" /></Link>
<div className={"title unselectable"}><div className={"title-text"}>Showdown<span className={"subtitle"}>{this.props.gameName === '' ? '' : '/ ' + this.props.gameName}</span></div></div> <div className={"title unselectable"}><div className={"title-text"}>Showdown<span className={"subtitle"}>{subtitle ? '/ ' + subtitle : ''}</span></div></div>
<div className={"stretch"} /> <div className={"stretch"} />
<div className={"github-info"} onClick={()=>{window.location.href = 'https://github.com/datamllab/rlcard'}}> <div className={"github-info"} onClick={()=>{window.location.href = 'https://github.com/datamllab/rlcard'}}>
<div className={"github-icon"}><GitHubIcon /></div> <div className={"github-icon"}><GitHubIcon /></div>
<div className={"github-text"}>Github<br /><span>{this.state.stars} stars</span></div> <div className={"github-text"}>Github<br /><span>{stars} stars</span></div>
</div> </div>
</div> </div>
</AppBar> </AppBar>
) )
}
} }
export default Navbar; export default Navbar;

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import './assets/index.scss'; import './assets/index.scss';
import App from './App'; import App from './App';

View File

@ -1,3 +1,4 @@
const apiUrl = 'http://127.0.0.1:8000'; const apiUrl = 'http://127.0.0.1:8000';
const douzeroDemoUrl = 'http://127.0.0.1:5000';
export {apiUrl}; export { apiUrl, douzeroDemoUrl };

View File

@ -1,66 +1,73 @@
const suitMap = new Map( const suitMap = new Map([
[["H", "hearts"], ["D", "diams"], ["S", "spades"], ["C", "clubs"]] ['H', 'hearts'],
); ['D', 'diams'],
['S', 'spades'],
['C', 'clubs'],
]);
const suitMapSymbol = new Map( const suitMapSymbol = new Map([
[["H", "\u2665"], ["D", "\u2666"], ["S", "\u2660"], ["C", "\u2663"]] ['H', '\u2665'],
); ['D', '\u2666'],
['S', '\u2660'],
['C', '\u2663'],
]);
export function removeCards(cards, hands){ // remove cards from hands, return the remained hands export function removeCards(cards, hands) {
// remove cards from hands, return the remained hands
let remainedHands = deepCopy(hands); let remainedHands = deepCopy(hands);
// if the player's action is pass then return the copy of original hands // if the player's action is pass then return the copy of original hands
if(cards === "pass"){ if (cards === 'pass') {
return remainedHands; return remainedHands;
} }
let misMatch = false; let misMatch = false;
cards.forEach(card => { cards.forEach((card) => {
let foundIdx = remainedHands.findIndex(element => {return element === card;}); let foundIdx = remainedHands.findIndex((element) => {
if(foundIdx > -1){ return element === card;
});
if (foundIdx > -1) {
remainedHands.splice(foundIdx, 1); remainedHands.splice(foundIdx, 1);
}else { } else {
misMatch = true; misMatch = true;
} }
}); });
if(misMatch) if (misMatch) return false;
return false; else return remainedHands;
else
return remainedHands;
} }
export function doubleRaf(callback){ export function doubleRaf(callback) {
// secure all the animation got rendered before callback function gets executed // secure all the animation got rendered before callback function gets executed
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(callback) requestAnimationFrame(callback);
}) });
} }
export function deepCopy(toCopy){ export function deepCopy(toCopy) {
return JSON.parse(JSON.stringify(toCopy)); return JSON.parse(JSON.stringify(toCopy));
} }
export function translateCardData(card) { export function translateCardData(card) {
let rankClass; let rankClass;
let suitClass = ""; let suitClass = '';
let rankText; let rankText;
let suitText = ""; let suitText = '';
// translate rank // translate rank
if(card === "RJ"){ if (card === 'RJ') {
rankClass = "big"; rankClass = 'big';
rankText = "+"; rankText = '+';
suitClass = "joker"; suitClass = 'joker';
suitText = "Joker"; suitText = 'Joker';
}else if(card === "BJ"){ } else if (card === 'BJ') {
rankClass = "little"; rankClass = 'little';
rankText = "-"; rankText = '-';
suitClass = "joker"; suitClass = 'joker';
suitText = "Joker"; suitText = 'Joker';
}else{ } else {
rankClass = card.charAt(1) === "T" ? `10` : card.charAt(1).toLowerCase(); rankClass = card.charAt(1) === 'T' ? `10` : card.charAt(1).toLowerCase();
rankClass = `rank-${rankClass}`; rankClass = `rank-${rankClass}`;
rankText = card.charAt(1) === "T" ? `10` : card.charAt(1); rankText = card.charAt(1) === 'T' ? `10` : card.charAt(1);
} }
// translate suitClass // translate suitClass
if(card !== "RJ" && card !== "BJ"){ if (card !== 'RJ' && card !== 'BJ') {
suitClass = suitMap.get(card.charAt(0)); suitClass = suitMap.get(card.charAt(0));
suitText = suitMapSymbol.get(card.charAt(0)); suitText = suitMapSymbol.get(card.charAt(0));
} }
@ -68,14 +75,15 @@ export function translateCardData(card) {
return [rankClass, suitClass, rankText, suitText]; return [rankClass, suitClass, rankText, suitText];
} }
export function millisecond2Second(t){ export function millisecond2Second(t) {
return Math.ceil(t/1000); return Math.ceil(t / 1000);
} }
export function debounce(func, wait, immediate) { export function debounce(func, wait, immediate) {
let timeout; let timeout;
return function() { return function () {
const context = this, args = arguments; const context = this,
args = arguments;
const later = function () { const later = function () {
timeout = null; timeout = null;
if (!immediate) func.apply(context, args); if (!immediate) func.apply(context, args);
@ -88,7 +96,157 @@ export function debounce(func, wait, immediate) {
} }
export function computeHandCardsWidth(num, emWidth) { export function computeHandCardsWidth(num, emWidth) {
if(num === 0) if (num === 0) return 0;
return 0; return (num - 1) * 1.1 * emWidth + 4.3 * emWidth * 1.2 + 2;
return (num-1)*1.1*emWidth + 4.3*emWidth*1.2 + 2; }
export function card2SuiteAndRank(card) {
if (card === 'BJ') {
return { suite: null, rank: 'X' };
} else if (card === 'RJ') {
return { suite: null, rank: 'D' };
} else {
return { suite: card[0], rank: card[1] };
}
}
export const fullDoudizhuDeck = [
'RJ',
'BJ',
'S2',
'C2',
'H2',
'D2',
'SA',
'CA',
'HA',
'DA',
'SK',
'CK',
'HK',
'DK',
'SQ',
'CQ',
'HQ',
'DQ',
'SJ',
'CJ',
'HJ',
'DJ',
'ST',
'CT',
'HT',
'DT',
'S9',
'C9',
'H9',
'D9',
'S8',
'C8',
'H8',
'D8',
'S7',
'C7',
'H7',
'D7',
'S6',
'C6',
'H6',
'D6',
'S5',
'C5',
'H5',
'D5',
'S4',
'C4',
'H4',
'D4',
'S3',
'C3',
'H3',
'D3',
];
export const fullDoudizhuDeckIndex = {
RJ: 54,
BJ: 53,
S2: 52,
C2: 51,
H2: 50,
D2: 49,
SA: 48,
CA: 47,
HA: 46,
DA: 45,
SK: 44,
CK: 43,
HK: 42,
DK: 41,
SQ: 40,
CQ: 39,
HQ: 38,
DQ: 37,
SJ: 36,
CJ: 35,
HJ: 34,
DJ: 33,
ST: 32,
CT: 31,
HT: 30,
DT: 29,
S9: 28,
C9: 27,
H9: 26,
D9: 25,
S8: 24,
C8: 23,
H8: 22,
D8: 21,
S7: 20,
C7: 19,
H7: 18,
D7: 17,
S6: 16,
C6: 15,
H6: 14,
D6: 13,
S5: 12,
C5: 11,
H5: 10,
D5: 9,
S4: 8,
C4: 7,
H4: 6,
D4: 5,
S3: 4,
C3: 3,
H3: 2,
D3: 1,
};
export function sortDoudizhuCards(cards, ascending = false) {
const cardsCopy = cards.slice();
return cardsCopy.sort((a, b) => {
return ascending
? fullDoudizhuDeckIndex[a] - fullDoudizhuDeckIndex[b]
: fullDoudizhuDeckIndex[b] - fullDoudizhuDeckIndex[a];
});
}
export function isDoudizhuBomb(cards) {
if (cards.length === 2) return (cards[0] === 'RJ' && cards[1] === 'BJ') || (cards[0] === 'BJ' && cards[1] === 'RJ');
if (cards.length === 4)
return cards[0][1] === cards[1][1] && cards[0][1] === cards[2][1] && cards[0][1] === cards[3][1];
return false;
}
export function shuffleArray(inputArray) {
let array = inputArray.slice();
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
} }

View File

@ -1,4 +0,0 @@
import DoudizhuGameView from "./DoudizhuGameView";
import LeducHoldemGameView from "./LeducHoldemGameView";
export {DoudizhuGameView, LeducHoldemGameView};

View File

@ -62,7 +62,14 @@ function LeaderBoard () {
fetchModelData(); fetchModelData();
}, [reloadMenu]); }, [reloadMenu]);
const { type, name } = qs.parse(window.location.search); let { type, name } = qs.parse(window.location.search);
// default value
if (!type) {
type = "game";
}
if (!name) {
name = "leduc-holdem";
}
let requestUrl = `${apiUrl}/tournament/`; let requestUrl = `${apiUrl}/tournament/`;
if (type === 'game') { if (type === 'game') {
requestUrl += `query_agent_payoff?name=${name}&elements_every_page=${rowsPerPage}&page_index=${page}` requestUrl += `query_agent_payoff?name=${name}&elements_every_page=${rowsPerPage}&page_index=${page}`

View File

@ -0,0 +1,440 @@
import Paper from '@material-ui/core/Paper';
import axios from 'axios';
import { Layout, Message } from 'element-react';
import qs from 'query-string';
import React, { useEffect, useState } from 'react';
import { DoudizhuGameBoard } from '../../components/GameBoard';
import {
card2SuiteAndRank,
deepCopy,
fullDoudizhuDeck,
isDoudizhuBomb,
shuffleArray,
sortDoudizhuCards,
} from '../../utils';
import { douzeroDemoUrl } from '../../utils/config';
const shuffledDoudizhuDeck = shuffleArray(fullDoudizhuDeck.slice());
const threeLandlordCards = shuffleArray(sortDoudizhuCards(shuffledDoudizhuDeck.slice(0, 3)));
const initConsiderationTime = 30000;
const considerationTimeDeduction = 1000;
const apiPlayDelay = 3000;
const mainPlayerId = 0; // index of main player (for the sake of simplify code logic)
const playerInfo = [
{
id: 0,
index: 0,
role: 'peasant',
douzeroPlayerPosition: 1,
},
{
id: 1,
index: 1,
role: 'peasant',
douzeroPlayerPosition: 2,
},
{
id: 2,
index: 2,
role: 'landlord',
douzeroPlayerPosition: 0,
},
];
let initHands = [
shuffledDoudizhuDeck.slice(3, 20),
shuffledDoudizhuDeck.slice(20, 37),
shuffledDoudizhuDeck.slice(37, 54),
];
console.log('init hands', initHands);
console.log('three landlord card', threeLandlordCards);
console.log('player info', playerInfo);
const landlordIdx = playerInfo.find((player) => player.role === 'landlord').index;
initHands[landlordIdx] = initHands[landlordIdx].concat(threeLandlordCards.slice());
let gameStateTimeout = null;
let gameHistory = [];
let bombNum = 0;
let lastMoveLandlord = [];
let lastMoveLandlordDown = [];
let lastMoveLandlordUp = [];
let playedCardsLandlord = [];
let playedCardsLandlordDown = [];
let playedCardsLandlordUp = [];
let legalActions = { turn: -1, actions: [] };
function PvEDoudizhuDemoView() {
const [considerationTime, setConsiderationTime] = useState(initConsiderationTime);
const [toggleFade, setToggleFade] = useState('');
const [gameStatus, setGameStatus] = useState('ready'); // "ready", "playing", "paused", "over"
const [gameState, setGameState] = useState({
hands: [[], [], []],
latestAction: [[], [], []],
currentPlayer: null, // index of current player
turn: 0,
});
const [selectedCards, setSelectedCards] = useState([]); // user selected hand card
const [isPassDisabled, setIsPassDisabled] = useState(true);
const cardStr2Arr = (cardStr) => {
return cardStr === 'pass' || cardStr === '' ? 'pass' : cardStr.split(' ');
};
const cardArr2DouzeroFormat = (cards) => {
return cards
.map((card) => {
if (card === 'RJ') return 'D';
if (card === 'BJ') return 'X';
return card[1];
})
.join('');
};
// todo: generate inital player / hand states
// for test use
function timeout(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const proceedNextTurn = async (playingCard, rankOnly = true) => {
// if next player is user, get legal actions
if ((gameState.currentPlayer + 1) % 3 === mainPlayerId) {
const player_hand_cards = cardArr2DouzeroFormat(gameState.hands[mainPlayerId].slice().reverse());
let rival_move = '';
if (playingCard.length === 0) {
rival_move = cardArr2DouzeroFormat(sortDoudizhuCards(gameHistory[gameHistory.length - 1], true));
} else {
rival_move = rankOnly ? playingCard.join('') : cardArr2DouzeroFormat(playingCard);
}
const requestBody = {
player_hand_cards,
rival_move,
};
const apiRes = await axios.post(`${douzeroDemoUrl}/legal`, qs.stringify(requestBody));
const data = apiRes.data;
legalActions = {
turn: gameState.turn + 1,
actions: data.legal_action.split(','),
};
setIsPassDisabled(playingCard.length === 0 && gameHistory[gameHistory.length - 1].length === 0);
}
// delay play for api player
if (gameState.currentPlayer !== mainPlayerId && considerationTime > apiPlayDelay) {
await timeout(apiPlayDelay);
}
setToggleFade('fade-out');
let newGameState = deepCopy(gameState);
// take played card out from hand, and generate playing cards with suite
const currentHand = newGameState.hands[gameState.currentPlayer];
let newHand;
let newLatestAction = [];
if (playingCard.length === 0) {
newHand = currentHand;
newLatestAction = 'pass';
} else if (rankOnly) {
newHand = currentHand.filter((card) => {
if (playingCard.length === 0) return true;
const { rank } = card2SuiteAndRank(card);
const idx = playingCard.indexOf(rank);
if (idx >= 0) {
playingCard.splice(idx, 1);
newLatestAction.push(card);
return false;
}
return true;
});
} else {
newLatestAction = playingCard.slice();
newHand = currentHand.filter((card) => {
if (playingCard.length === 0) return true;
const idx = playingCard.indexOf(card);
if (idx >= 0) {
playingCard.splice(idx, 1);
return false;
}
return true;
});
}
// update value records for douzero
const newHistoryRecord = sortDoudizhuCards(newLatestAction === 'pass' ? [] : newLatestAction, true);
switch (playerInfo[gameState.currentPlayer].douzeroPlayerPosition) {
case 0:
lastMoveLandlord = newHistoryRecord;
playedCardsLandlord = playedCardsLandlord.concat(newHistoryRecord);
break;
case 1:
lastMoveLandlordDown = newHistoryRecord;
playedCardsLandlordDown = playedCardsLandlordDown.concat(newHistoryRecord);
break;
case 2:
lastMoveLandlordUp = newHistoryRecord;
playedCardsLandlordUp = playedCardsLandlordUp.concat(newHistoryRecord);
break;
}
gameHistory.push(newHistoryRecord);
if (isDoudizhuBomb(newHistoryRecord)) bombNum++;
newGameState.latestAction[gameState.currentPlayer] = newLatestAction;
newGameState.hands[gameState.currentPlayer] = newHand;
newGameState.currentPlayer = (newGameState.currentPlayer + 1) % 3;
newGameState.turn++;
setGameState(newGameState);
setToggleFade('fade-in');
setTimeout(() => {
setToggleFade('');
}, 200);
if (gameStateTimeout) {
clearTimeout(gameStateTimeout);
}
setConsiderationTime(initConsiderationTime);
};
const requestApiPlay = async () => {
// gather information for api request
const player_position = playerInfo[gameState.currentPlayer].douzeroPlayerPosition;
const player_hand_cards = cardArr2DouzeroFormat(gameState.hands[gameState.currentPlayer].slice().reverse());
const num_cards_left_landlord =
gameState.hands[playerInfo.find((player) => player.douzeroPlayerPosition === 0).index].length;
const num_cards_left_landlord_down =
gameState.hands[playerInfo.find((player) => player.douzeroPlayerPosition === 1).index].length;
const num_cards_left_landlord_up =
gameState.hands[playerInfo.find((player) => player.douzeroPlayerPosition === 2).index].length;
const three_landlord_cards = cardArr2DouzeroFormat(threeLandlordCards.slice().reverse());
const card_play_action_seq = gameHistory
.map((cards) => {
return cardArr2DouzeroFormat(cards);
})
.join(',');
const other_hand_cards = cardArr2DouzeroFormat(
sortDoudizhuCards(
gameState.hands[(gameState.currentPlayer + 1) % 3].concat(
gameState.hands[(gameState.currentPlayer + 2) % 3],
),
true,
),
);
const last_move_landlord = cardArr2DouzeroFormat(lastMoveLandlord.slice().reverse());
const last_move_landlord_down = cardArr2DouzeroFormat(lastMoveLandlordDown.slice().reverse());
const last_move_landlord_up = cardArr2DouzeroFormat(lastMoveLandlordUp.slice().reverse());
const bomb_num = bombNum;
const played_cards_landlord = cardArr2DouzeroFormat(playedCardsLandlord);
const played_cards_landlord_down = cardArr2DouzeroFormat(playedCardsLandlordDown);
const played_cards_landlord_up = cardArr2DouzeroFormat(playedCardsLandlordUp);
const requestBody = {
player_position,
player_hand_cards,
num_cards_left_landlord,
num_cards_left_landlord_down,
num_cards_left_landlord_up,
three_landlord_cards,
card_play_action_seq,
other_hand_cards,
last_move_landlord,
last_move_landlord_down,
last_move_landlord_up,
bomb_num,
played_cards_landlord,
played_cards_landlord_down,
played_cards_landlord_up,
};
try {
const apiRes = await axios.post(`${douzeroDemoUrl}/predict`, qs.stringify(requestBody));
const data = apiRes.data;
if (data.status !== 0) {
if (data.status === -1) {
// check if no legal action can be made
const player_hand_cards = cardArr2DouzeroFormat(
gameState.hands[gameState.currentPlayer].slice().reverse(),
);
let rival_move = '';
if (gameHistory[gameHistory.length - 1].length > 0) {
rival_move = cardArr2DouzeroFormat(
sortDoudizhuCards(gameHistory[gameHistory.length - 1], true),
);
} else if (gameHistory.length >= 2 && gameHistory[gameHistory.length - 2].length > 0) {
rival_move = cardArr2DouzeroFormat(
sortDoudizhuCards(gameHistory[gameHistory.length - 2], true),
);
}
const requestBody = {
player_hand_cards,
rival_move,
};
const apiRes = await axios.post(`${douzeroDemoUrl}/legal`, qs.stringify(requestBody));
if (apiRes.data.legal_action === '') proceedNextTurn([]);
else {
Message({
message: 'Error receiving prediction result, please try refresh the page',
type: 'error',
showClose: true,
});
}
}
} else {
let bestAction = '';
if (data.result && Object.keys(data.result).length > 0) {
if (Object.keys(data.result).length === 1) bestAction = Object.keys(data.result)[0];
else {
bestAction = Object.keys(data.result)[0];
let bestConfidence = Number(data.result[Object.keys(data.result)[0]]);
for (let i = 1; i < Object.keys(data.result).length; i++) {
if (Number(data.result[Object.keys(data.result)[i]]) > bestConfidence) {
bestAction = Object.keys(data.result)[i];
bestConfidence = Number(data.result[Object.keys(data.result)[i]]);
}
}
}
}
proceedNextTurn(bestAction.split(''));
}
} catch (err) {
Message({
message: 'Error receiving prediction result, please try refresh the page',
type: 'error',
showClose: true,
});
}
};
const handleSelectedCards = (cards) => {
let newSelectedCards = selectedCards.slice();
cards.forEach((card) => {
if (newSelectedCards.indexOf(card) >= 0) {
newSelectedCards.splice(newSelectedCards.indexOf(card), 1);
} else {
newSelectedCards.push(card);
}
});
setSelectedCards(newSelectedCards);
};
const gameStateTimer = () => {
gameStateTimeout = setTimeout(() => {
let currentConsiderationTime = considerationTime;
if (currentConsiderationTime > 0) {
currentConsiderationTime -= considerationTimeDeduction;
currentConsiderationTime = Math.max(currentConsiderationTime, 0);
setConsiderationTime(currentConsiderationTime);
} else {
// consideration time used up for current player
// if current player is controlled by user, play a random card
// todo
}
}, considerationTimeDeduction);
};
useEffect(() => {
gameStateTimer();
}, [considerationTime]);
// set init game state
useEffect(() => {
// start game
setGameStatus('playing');
const newGameState = deepCopy(gameState);
// find landord to be the first player
newGameState.currentPlayer = playerInfo.find((element) => element.role === 'landlord').index;
newGameState.hands = initHands.map((element) => sortDoudizhuCards(element));
setGameState(newGameState);
gameStateTimer();
}, []);
useEffect(() => {
if (gameState.currentPlayer) {
// if current player is not user, request for API player
if (gameState.currentPlayer !== mainPlayerId) {
requestApiPlay();
}
}
}, [gameState.currentPlayer]);
const runNewTurn = () => {};
const handleMainPlayerAct = (type) => {
switch (type) {
case 'play': {
// check if cards to play is in legal action list
if (gameState.turn === legalActions.turn) {
if (
legalActions.actions.indexOf(cardArr2DouzeroFormat(sortDoudizhuCards(selectedCards, true))) >= 0
) {
proceedNextTurn(selectedCards, false);
} else {
Message({
message: 'Selected cards are not legal action',
type: 'warning',
showClose: true,
});
setSelectedCards([]);
}
} else {
Message({
message: 'Legal Action not received or turn info inconsistant',
type: 'error',
showClose: true,
});
}
break;
}
case 'pass': {
proceedNextTurn([], false);
setSelectedCards([]);
break;
}
case 'deselect': {
setSelectedCards([]);
break;
}
}
};
return (
<div>
<div className={'doudizhu-view-container'}>
<Layout.Row style={{ height: '540px' }}>
<Layout.Col style={{ height: '100%' }} span="17">
<div style={{ height: '100%' }}>
<Paper className={'doudizhu-gameboard-paper'} elevation={3}>
<DoudizhuGameBoard
isPassDisabled={isPassDisabled}
gamePlayable={true}
playerInfo={playerInfo}
hands={gameState.hands}
selectedCards={selectedCards}
handleSelectedCards={handleSelectedCards}
latestAction={gameState.latestAction}
mainPlayerId={mainPlayerId}
currentPlayer={gameState.currentPlayer}
considerationTime={considerationTime}
turn={gameState.turn}
runNewTurn={(prevTurn) => runNewTurn(prevTurn)}
toggleFade={toggleFade}
gameStatus={gameStatus}
handleMainPlayerAct={handleMainPlayerAct}
/>
</Paper>
</div>
</Layout.Col>
</Layout.Row>
</div>
</div>
);
}
export default PvEDoudizhuDemoView;

View File

@ -0,0 +1,3 @@
import PvEDoudizhuDemoView from './PvEDoudizhuDemoView';
export {PvEDoudizhuDemoView};

View File

@ -2,7 +2,6 @@ import React from 'react';
import axios from 'axios'; import axios from 'axios';
import '../../assets/gameview.scss'; import '../../assets/gameview.scss';
import { DoudizhuGameBoard } from '../../components/GameBoard'; import { DoudizhuGameBoard } from '../../components/GameBoard';
import Navbar from "../../components/Navbar";
import {removeCards, doubleRaf, deepCopy, computeHandCardsWidth, translateCardData} from "../../utils"; import {removeCards, doubleRaf, deepCopy, computeHandCardsWidth, translateCardData} from "../../utils";
import { apiUrl } from "../../utils/config"; import { apiUrl } from "../../utils/config";
@ -25,7 +24,7 @@ import DialogActions from "@material-ui/core/DialogActions";
import Dialog from "@material-ui/core/Dialog"; import Dialog from "@material-ui/core/Dialog";
import qs from "query-string"; import qs from "query-string";
class DoudizhuGameView extends React.Component { class DoudizhuReplayView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -381,7 +380,6 @@ class DoudizhuGameView extends React.Component {
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<Navbar gameName={"Doudizhu"} />
<div className={"doudizhu-view-container"}> <div className={"doudizhu-view-container"}>
<Layout.Row style={{"height": "540px"}}> <Layout.Row style={{"height": "540px"}}>
<Layout.Col style={{"height": "100%"}} span="17"> <Layout.Col style={{"height": "100%"}} span="17">
@ -493,4 +491,4 @@ class DoudizhuGameView extends React.Component {
} }
} }
export default DoudizhuGameView; export default DoudizhuReplayView;

View File

@ -3,7 +3,6 @@ import axios from 'axios';
import qs from 'query-string'; import qs from 'query-string';
import '../../assets/gameview.scss'; import '../../assets/gameview.scss';
import {LeducHoldemGameBoard} from '../../components/GameBoard'; import {LeducHoldemGameBoard} from '../../components/GameBoard';
import Navbar from '../../components/Navbar';
import {deepCopy} from "../../utils"; import {deepCopy} from "../../utils";
import { apiUrl } from "../../utils/config"; import { apiUrl } from "../../utils/config";
@ -26,7 +25,7 @@ import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText'; import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle'; import DialogTitle from '@material-ui/core/DialogTitle';
class LeducHoldemGameView extends React.Component { class LeducHoldemReplayView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -398,7 +397,6 @@ class LeducHoldemGameView extends React.Component {
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<Navbar gameName="Leduc Hold'em" />
<div className={"leduc-view-container"}> <div className={"leduc-view-container"}>
<Layout.Row style={{"height": "540px"}}> <Layout.Row style={{"height": "540px"}}>
<Layout.Col style={{"height": "100%"}} span="17"> <Layout.Col style={{"height": "100%"}} span="17">
@ -513,4 +511,4 @@ class LeducHoldemGameView extends React.Component {
} }
} }
export default LeducHoldemGameView; export default LeducHoldemReplayView;

View File

@ -0,0 +1,4 @@
import DoudizhuReplayView from "./DoudizhuReplayView";
import LeducHoldemReplayView from "./LeducHoldemReplayView";
export {DoudizhuReplayView, LeducHoldemReplayView};