connect legal action api; add error messages for api request

This commit is contained in:
Songyi Huang 2021-04-21 21:44:22 -07:00
parent 14ce853ce9
commit 9e2e8dccf9
2 changed files with 229 additions and 127 deletions

View File

@ -1,60 +1,52 @@
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 Button from "@material-ui/core/Button";
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="", cardSelectable = false) { 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 loose ${fadeClassName} ${this.props.gamePlayable && cardSelectable ? 'selectable' : 'unselectable'}`}> <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; let selected = false;
if (this.props.gamePlayable && cardSelectable) { if (this.props.gamePlayable && cardSelectable) {
@ -64,7 +56,10 @@ class DoudizhuGameBoard extends React.Component {
// todo: right click and move to select multiple cards // todo: right click and move to select multiple cards
return ( return (
<li key={`handCard-${card}`}> <li key={`handCard-${card}`}>
<label onClick={() => this.props.handleSelectedCards([card])} className={`card ${rankClass} ${suitClass} ${selected ? 'selected' : ''}`}> <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>
</label> </label>
@ -73,17 +68,17 @@ class DoudizhuGameBoard extends React.Component {
})} })}
</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 (
@ -91,7 +86,7 @@ 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}`}>
@ -108,7 +103,7 @@ 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}`}>
@ -123,41 +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) { if (this.props.mainPlayerId === this.props.playerInfo[this.props.currentPlayer].id) {
return ( return (
<div className={"main-player-action-wrapper"}> <div className={'main-player-action-wrapper'}>
<div style={{marginRight: '2em'}} className={"timer "+fadeClassName}> <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 onClick={() => {this.props.handleMainPlayerAct('deselect')}} style={{marginRight: '2em'}} variant="contained" color="primary">Deselect</Button> <Button
<Button onClick={() => {this.props.handleMainPlayerAct('pass');}} style={{marginRight: '2em'}} variant="contained" color="primary">Pass</Button> onClick={() => {
<Button onClick={() => {this.props.handleMainPlayerAct('play');}} variant="contained" color="primary">Play</Button> 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> </div>
) );
} else { } else {
return ( return (
<div className={"timer "+fadeClassName}> <div className={'timer ' + fadeClassName}>
<div className="timer-text">{millisecond2Second(this.props.considerationTime)}</div> <div className="timer-text">{millisecond2Second(this.props.considerationTime)}</div>
</div> </div>
) );
} }
}else{ } else {
return this.computeSingleLineHand(this.props.latestAction[playerIdx], fadeClassName) 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);
} }
@ -166,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], '', true)}</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,6 +1,6 @@
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
import axios from 'axios'; import axios from 'axios';
import { Layout } from 'element-react'; import { Layout, Message } from 'element-react';
import qs from 'query-string'; import qs from 'query-string';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { DoudizhuGameBoard } from '../../components/GameBoard'; import { DoudizhuGameBoard } from '../../components/GameBoard';
@ -13,9 +13,10 @@ const initHands = [
'RJ BJ D2 SA SK CK SJ HJ DJ CT DT C9 S8 H8 C8 D7 D5 H3 S3 D3', 'RJ BJ D2 SA SK CK SJ HJ DJ CT DT C9 S8 H8 C8 D7 D5 H3 S3 D3',
]; ];
const initConsiderationTime = 2000; const initConsiderationTime = 30000;
const considerationTimeDeduction = 200; const considerationTimeDeduction = 1000;
const mainPlayerId = 0; const apiPlayDelay = 3000;
const mainPlayerId = 0; // index of main player (for the sake of simplify code logic)
const playerInfo = [ const playerInfo = [
{ {
id: 0, id: 0,
@ -48,6 +49,7 @@ let lastMoveLandlordUp = [];
let playedCardsLandlord = []; let playedCardsLandlord = [];
let playedCardsLandlordDown = []; let playedCardsLandlordDown = [];
let playedCardsLandlordUp = []; let playedCardsLandlordUp = [];
let legalActions = { turn: -1, actions: [] };
function PvEDoudizhuDemoView() { function PvEDoudizhuDemoView() {
const [considerationTime, setConsiderationTime] = useState(initConsiderationTime); const [considerationTime, setConsiderationTime] = useState(initConsiderationTime);
@ -60,6 +62,7 @@ function PvEDoudizhuDemoView() {
turn: 0, turn: 0,
}); });
const [selectedCards, setSelectedCards] = useState([]); // user selected hand card const [selectedCards, setSelectedCards] = useState([]); // user selected hand card
const [isPassDisabled, setIsPassDisabled] = useState(true);
const cardStr2Arr = (cardStr) => { const cardStr2Arr = (cardStr) => {
return cardStr === 'pass' || cardStr === '' ? 'pass' : cardStr.split(' '); return cardStr === 'pass' || cardStr === '' ? 'pass' : cardStr.split(' ');
@ -82,14 +85,42 @@ function PvEDoudizhuDemoView() {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
const proceedNextTurn = (playingCard, rankOnly = true) => { 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));
console.log('legal', apiRes);
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'); setToggleFade('fade-out');
let newGameState = deepCopy(gameState); let newGameState = deepCopy(gameState);
// todo: take played card out from hand, and generate playing cards with suite // take played card out from hand, and generate playing cards with suite
const currentHand = newGameState.hands[gameState.currentPlayer]; const currentHand = newGameState.hands[gameState.currentPlayer];
let newHand; let newHand;
let newLatestAction = []; let newLatestAction = [];
if (playingCard.length === 0) { if (playingCard.length === 0) {
@ -123,7 +154,6 @@ function PvEDoudizhuDemoView() {
} }
// update value records for douzero // update value records for douzero
// debugger;
const newHistoryRecord = newLatestAction === 'pass' ? [] : newLatestAction; const newHistoryRecord = newLatestAction === 'pass' ? [] : newLatestAction;
switch (playerInfo[gameState.currentPlayer].douzeroPlayerPosition) { switch (playerInfo[gameState.currentPlayer].douzeroPlayerPosition) {
case 0: case 0:
@ -153,18 +183,12 @@ function PvEDoudizhuDemoView() {
}, 200); }, 200);
if (gameStateTimeout) { if (gameStateTimeout) {
clearTimeout(gameStateTimeout); clearTimeout(gameStateTimeout);
setConsiderationTime(initConsiderationTime);
} }
setConsiderationTime(initConsiderationTime);
}; };
const requestApiPlay = async () => { const requestApiPlay = async () => {
// mock delayed API play // gather information for api request
// await timeout(1200);
// const apiRes = [
// card2SuiteAndRank(
// gameState.hands[gameState.currentPlayer][gameState.hands[gameState.currentPlayer].length - 1],
// ).rank,
// ];
const player_position = playerInfo[gameState.currentPlayer].douzeroPlayerPosition; const player_position = playerInfo[gameState.currentPlayer].douzeroPlayerPosition;
const player_hand_cards = cardArr2DouzeroFormat(gameState.hands[gameState.currentPlayer].slice().reverse()); const player_hand_cards = cardArr2DouzeroFormat(gameState.hands[gameState.currentPlayer].slice().reverse());
const num_cards_left_landlord = const num_cards_left_landlord =
@ -217,10 +241,37 @@ function PvEDoudizhuDemoView() {
const apiRes = await axios.post(`${douzeroDemoUrl}/predict`, qs.stringify(requestBody)); const apiRes = await axios.post(`${douzeroDemoUrl}/predict`, qs.stringify(requestBody));
console.log(apiRes.data); console.log(apiRes.data);
const data = apiRes.data; const data = apiRes.data;
if (data.status !== 0) { if (data.status !== 0) {
if (data.status === -1) { if (data.status === -1) {
// todo: check if no legal action can be made // check if no legal action can be made
proceedNextTurn([]); 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));
console.log('api player legal', apiRes);
if (apiRes.data.legal_action === '') proceedNextTurn([]);
else {
Message({
message: 'Error receiving prediction result, please try refresh the page',
type: 'error',
showClose: true,
});
}
} }
console.log(data.status, data.message); console.log(data.status, data.message);
} else { } else {
@ -242,7 +293,11 @@ function PvEDoudizhuDemoView() {
proceedNextTurn(bestAction.split('')); proceedNextTurn(bestAction.split(''));
} }
} catch (err) { } catch (err) {
console.log(err); Message({
message: 'Error receiving prediction result, please try refresh the page',
type: 'error',
showClose: true,
});
} }
}; };
@ -294,20 +349,37 @@ function PvEDoudizhuDemoView() {
if (gameState.currentPlayer) { if (gameState.currentPlayer) {
// if current player is not user, request for API player // if current player is not user, request for API player
if (gameState.currentPlayer !== mainPlayerId) { if (gameState.currentPlayer !== mainPlayerId) {
// debugger;
requestApiPlay(); requestApiPlay();
} }
} }
}, [gameState.currentPlayer]); }, [gameState.currentPlayer]);
const runNewTurn = () => { const runNewTurn = () => {};
// gameStateTimer();
};
const handleMainPlayerAct = (type) => { const handleMainPlayerAct = (type) => {
switch (type) { switch (type) {
case 'play': { 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); 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; break;
} }
case 'pass': { case 'pass': {
@ -330,6 +402,7 @@ function PvEDoudizhuDemoView() {
<div style={{ height: '100%' }}> <div style={{ height: '100%' }}>
<Paper className={'doudizhu-gameboard-paper'} elevation={3}> <Paper className={'doudizhu-gameboard-paper'} elevation={3}>
<DoudizhuGameBoard <DoudizhuGameBoard
isPassDisabled={isPassDisabled}
gamePlayable={true} gamePlayable={true}
playerInfo={playerInfo} playerInfo={playerInfo}
hands={gameState.hands} hands={gameState.hands}