commit
8be57eef48
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Folders
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
src/public/lib/
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 4
|
||||||
|
}
|
29
src/App.js
29
src/App.js
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
import DoudizhuGameView from "./DoudizhuGameView";
|
|
||||||
import LeducHoldemGameView from "./LeducHoldemGameView";
|
|
||||||
|
|
||||||
export {DoudizhuGameView, LeducHoldemGameView};
|
|
|
@ -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}`
|
||||||
|
|
|
@ -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;
|
|
@ -0,0 +1,3 @@
|
||||||
|
import PvEDoudizhuDemoView from './PvEDoudizhuDemoView';
|
||||||
|
|
||||||
|
export {PvEDoudizhuDemoView};
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,4 @@
|
||||||
|
import DoudizhuReplayView from "./DoudizhuReplayView";
|
||||||
|
import LeducHoldemReplayView from "./LeducHoldemReplayView";
|
||||||
|
|
||||||
|
export {DoudizhuReplayView, LeducHoldemReplayView};
|
Loading…
Reference in New Issue