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

View File

@ -1,6 +1,6 @@
import Paper from '@material-ui/core/Paper';
import axios from 'axios';
import { Layout } from 'element-react';
import { Layout, Message } from 'element-react';
import qs from 'query-string';
import React, { useEffect, useState } from 'react';
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',
];
const initConsiderationTime = 2000;
const considerationTimeDeduction = 200;
const mainPlayerId = 0;
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,
@ -48,6 +49,7 @@ let lastMoveLandlordUp = [];
let playedCardsLandlord = [];
let playedCardsLandlordDown = [];
let playedCardsLandlordUp = [];
let legalActions = { turn: -1, actions: [] };
function PvEDoudizhuDemoView() {
const [considerationTime, setConsiderationTime] = useState(initConsiderationTime);
@ -60,6 +62,7 @@ function PvEDoudizhuDemoView() {
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(' ');
@ -82,14 +85,42 @@ function PvEDoudizhuDemoView() {
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');
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];
let newHand;
let newLatestAction = [];
if (playingCard.length === 0) {
@ -123,7 +154,6 @@ function PvEDoudizhuDemoView() {
}
// update value records for douzero
// debugger;
const newHistoryRecord = newLatestAction === 'pass' ? [] : newLatestAction;
switch (playerInfo[gameState.currentPlayer].douzeroPlayerPosition) {
case 0:
@ -153,18 +183,12 @@ function PvEDoudizhuDemoView() {
}, 200);
if (gameStateTimeout) {
clearTimeout(gameStateTimeout);
setConsiderationTime(initConsiderationTime);
}
setConsiderationTime(initConsiderationTime);
};
const requestApiPlay = async () => {
// mock delayed API play
// await timeout(1200);
// const apiRes = [
// card2SuiteAndRank(
// gameState.hands[gameState.currentPlayer][gameState.hands[gameState.currentPlayer].length - 1],
// ).rank,
// ];
// 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 =
@ -217,10 +241,37 @@ function PvEDoudizhuDemoView() {
const apiRes = await axios.post(`${douzeroDemoUrl}/predict`, qs.stringify(requestBody));
console.log(apiRes.data);
const data = apiRes.data;
if (data.status !== 0) {
if (data.status === -1) {
// todo: check if no legal action can be made
proceedNextTurn([]);
// 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));
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);
} else {
@ -242,7 +293,11 @@ function PvEDoudizhuDemoView() {
proceedNextTurn(bestAction.split(''));
}
} 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 current player is not user, request for API player
if (gameState.currentPlayer !== mainPlayerId) {
// debugger;
requestApiPlay();
}
}
}, [gameState.currentPlayer]);
const runNewTurn = () => {
// gameStateTimer();
};
const runNewTurn = () => {};
const handleMainPlayerAct = (type) => {
switch (type) {
case 'play': {
proceedNextTurn(selectedCards, false);
// 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': {
@ -330,6 +402,7 @@ function PvEDoudizhuDemoView() {
<div style={{ height: '100%' }}>
<Paper className={'doudizhu-gameboard-paper'} elevation={3}>
<DoudizhuGameBoard
isPassDisabled={isPassDisabled}
gamePlayable={true}
playerInfo={playerInfo}
hands={gameState.hands}