add prediction area & control area

This commit is contained in:
Songyi Huang 2021-04-25 20:48:34 -07:00
parent 1593b1235a
commit 3a878122d9
3 changed files with 240 additions and 47 deletions

View File

@ -101,6 +101,12 @@
box-sizing: border-box; box-sizing: border-box;
width: 80px; width: 80px;
} }
.MuiFormControlLabel-label {
display: table-cell;
line-height: 51px;
vertical-align: middle;
}
} }
.doudizhu-view-container { .doudizhu-view-container {

View File

@ -143,43 +143,41 @@ class DoudizhuGameBoard extends React.Component {
<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>
{this.props.gamePlayable ? {this.props.gamePlayable ? (
(<> <>
<Button <Button
onClick={() => { onClick={() => {
this.props.handleMainPlayerAct('deselect'); this.props.handleMainPlayerAct('deselect');
}} }}
style={{ marginRight: '2em' }} style={{ marginRight: '2em' }}
variant="contained" variant="contained"
color="primary" color="primary"
> >
Deselect Deselect
</Button> </Button>
<Button <Button
disabled={this.props.isPassDisabled} disabled={this.props.isPassDisabled}
onClick={() => { onClick={() => {
this.props.handleMainPlayerAct('pass'); this.props.handleMainPlayerAct('pass');
}} }}
style={{ marginRight: '2em' }} style={{ marginRight: '2em' }}
variant="contained" variant="contained"
color="primary" color="primary"
> >
Pass Pass
</Button> </Button>
<Button <Button
disabled={!this.props.selectedCards || this.props.selectedCards.length === 0} disabled={!this.props.selectedCards || this.props.selectedCards.length === 0}
onClick={() => { onClick={() => {
this.props.handleMainPlayerAct('play'); this.props.handleMainPlayerAct('play');
}} }}
variant="contained" variant="contained"
color="primary" color="primary"
> >
Play Play
</Button> </Button>
</>) </>
: ) : undefined}
undefined}
</div> </div>
); );
} else { } else {
@ -195,7 +193,12 @@ class DoudizhuGameBoard extends React.Component {
} }
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
if (prevProps.turn !== this.props.turn && this.props.turn !== 0 && this.props.gameStatus === 'playing') { if (
this.props.runNewTurn &&
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);
} }

View File

@ -4,7 +4,12 @@ import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent'; 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';
import Divider from '@material-ui/core/Divider';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormGroup from '@material-ui/core/FormGroup';
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
import Switch from '@material-ui/core/Switch';
import NotInterestedIcon from '@material-ui/icons/NotInterested';
import axios from 'axios'; import axios from 'axios';
import { Layout, Message } from 'element-react'; import { Layout, Message } from 'element-react';
import qs from 'query-string'; import qs from 'query-string';
@ -13,11 +18,13 @@ import '../../assets/doudizhu.scss';
import { DoudizhuGameBoard } from '../../components/GameBoard'; import { DoudizhuGameBoard } from '../../components/GameBoard';
import { import {
card2SuiteAndRank, card2SuiteAndRank,
computeHandCardsWidth,
deepCopy, deepCopy,
fullDoudizhuDeck, fullDoudizhuDeck,
isDoudizhuBomb, isDoudizhuBomb,
shuffleArray, shuffleArray,
sortDoudizhuCards, sortDoudizhuCards,
translateCardData,
} from '../../utils'; } from '../../utils';
import { douzeroDemoUrl } from '../../utils/config'; import { douzeroDemoUrl } from '../../utils/config';
@ -66,6 +73,9 @@ function PvEDoudizhuDemoView() {
}); });
const [selectedCards, setSelectedCards] = useState([]); // user selected hand card const [selectedCards, setSelectedCards] = useState([]); // user selected hand card
const [isPassDisabled, setIsPassDisabled] = useState(true); const [isPassDisabled, setIsPassDisabled] = useState(true);
const [predictionRes, setPredictionRes] = useState({ prediction: [], hands: [] });
const [hideRivalHand, setHideRivalHand] = useState(false);
const [hidePredictionArea, setHidePredictionArea] = useState(false);
const cardArr2DouzeroFormat = (cards) => { const cardArr2DouzeroFormat = (cards) => {
return cards return cards
@ -272,20 +282,41 @@ function PvEDoudizhuDemoView() {
rival_move, rival_move,
}; };
const apiRes = await axios.post(`${douzeroDemoUrl}/legal`, qs.stringify(requestBody)); const apiRes = await axios.post(`${douzeroDemoUrl}/legal`, qs.stringify(requestBody));
if (apiRes.data.legal_action === '') proceedNextTurn([]); if (apiRes.data.legal_action === '') {
else if (apiRes.data.legal_action.split(',').length === 1) proceedNextTurn([]);
setPredictionRes({
prediction: [['', 'Only Choice']],
hands: gameState.hands[gameState.currentPlayer].slice(),
});
} else if (apiRes.data.legal_action.split(',').length === 1) {
proceedNextTurn(apiRes.data.legal_action.split('')); proceedNextTurn(apiRes.data.legal_action.split(''));
else { setPredictionRes({
prediction: [[apiRes.data.legal_action, 'Only Choice']],
hands: gameState.hands[gameState.currentPlayer].slice(),
});
} else {
Message({ Message({
message: 'Error receiving prediction result, please try refresh the page', message: 'Error receiving prediction result, please try refresh the page',
type: 'error', type: 'error',
showClose: true, showClose: true,
}); });
} }
} else {
Message({
message: `Error: ${apiRes.data.message}`,
type: 'error',
showClose: true,
});
} }
} else { } else {
let bestAction = ''; let bestAction = '';
if (data.result && Object.keys(data.result).length > 0) { if (data.result && Object.keys(data.result).length > 0) {
setPredictionRes({
prediction: Object.entries(data.result).sort((a, b) => {
return Number(b[1]) - Number(a[1]);
}),
hands: gameState.hands[gameState.currentPlayer].slice(),
});
if (Object.keys(data.result).length === 1) bestAction = Object.keys(data.result)[0]; if (Object.keys(data.result).length === 1) bestAction = Object.keys(data.result)[0];
else { else {
bestAction = Object.keys(data.result)[0]; bestAction = Object.keys(data.result)[0];
@ -309,6 +340,14 @@ function PvEDoudizhuDemoView() {
} }
}; };
const toggleHideRivalHand = () => {
setHideRivalHand(!hideRivalHand);
};
const toggleHidePredictionArea = () => {
setHidePredictionArea(!hidePredictionArea);
};
const handleSelectedCards = (cards) => { const handleSelectedCards = (cards) => {
let newSelectedCards = selectedCards.slice(); let newSelectedCards = selectedCards.slice();
cards.forEach((card) => { cards.forEach((card) => {
@ -416,14 +455,16 @@ function PvEDoudizhuDemoView() {
}; };
useEffect(() => { useEffect(() => {
gameStateTimer(); if (gameStatus === 'playing') gameStateTimer();
}, [considerationTime]); }, [considerationTime]);
useEffect(() => { useEffect(() => {
if (gameState.currentPlayer && gameStatus === 'playing') { if (gameState.currentPlayer !== null && gameStatus === 'playing') {
// 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) {
requestApiPlay(); requestApiPlay();
} else {
setPredictionRes({ prediction: [], hands: [] });
} }
} }
}, [gameState.currentPlayer]); }, [gameState.currentPlayer]);
@ -432,8 +473,6 @@ function PvEDoudizhuDemoView() {
if (gameStatus === 'playing') startGame(); if (gameStatus === 'playing') startGame();
}, [gameStatus]); }, [gameStatus]);
const runNewTurn = () => {};
const handleMainPlayerAct = (type) => { const handleMainPlayerAct = (type) => {
switch (type) { switch (type) {
case 'play': { case 'play': {
@ -472,6 +511,82 @@ function PvEDoudizhuDemoView() {
} }
}; };
const computePredictionCards = (cards, hands) => {
let computedCards = [];
if (cards.length > 0) {
hands.forEach((card) => {
const { rank } = card2SuiteAndRank(card);
const idx = cards.indexOf(rank);
if (idx >= 0) {
cards.splice(idx, 1);
computedCards.push(card);
}
});
} else {
computedCards = 'pass';
}
if (computedCards === 'pass') {
return (
<div className={'non-card ' + toggleFade}>
<span>Pass</span>
</div>
);
} else {
return (
<div className={'unselectable playingCards loose ' + toggleFade}>
<ul className="hand" style={{ width: computeHandCardsWidth(computedCards.length, 10) }}>
{computedCards.map((card) => {
const [rankClass, suitClass, rankText, suitText] = translateCardData(card);
return (
<li key={`handCard-${card}`}>
<label className={`card ${rankClass} ${suitClass}`} href="/#">
<span className="rank">{rankText}</span>
<span className="suit">{suitText}</span>
</label>
</li>
);
})}
</ul>
</div>
);
}
};
const computeProbabilityItem = (idx) => {
if (gameStatus !== 'ready') {
if (hidePredictionArea) {
return (
<div className={'playing'}>
<div className={'non-card'}>
<span>{'Hidden'}</span>
</div>
</div>
);
}
return (
<div className={'playing'}>
<div className="probability-move">
{predictionRes.prediction.length > idx ? (
computePredictionCards(predictionRes.prediction[idx][0].split(''), predictionRes.hands)
) : (
<NotInterestedIcon fontSize="large" />
)}
</div>
{predictionRes.prediction.length > idx ? (
<div className={'non-card'}>
<span>{`Expected Score: ${predictionRes.prediction[idx][1]}`}</span>
</div>
) : (
''
)}
</div>
);
} else {
return <span className={'waiting'}>Waiting...</span>;
}
};
return ( return (
<div> <div>
<Dialog <Dialog
@ -498,7 +613,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
showCardBack={true} showCardBack={hideRivalHand}
handleSelectRole={handleSelectRole} handleSelectRole={handleSelectRole}
isPassDisabled={isPassDisabled} isPassDisabled={isPassDisabled}
gamePlayable={true} gamePlayable={true}
@ -511,7 +626,6 @@ function PvEDoudizhuDemoView() {
currentPlayer={gameState.currentPlayer} currentPlayer={gameState.currentPlayer}
considerationTime={considerationTime} considerationTime={considerationTime}
turn={gameState.turn} turn={gameState.turn}
runNewTurn={(prevTurn) => runNewTurn(prevTurn)}
toggleFade={toggleFade} toggleFade={toggleFade}
gameStatus={gameStatus} gameStatus={gameStatus}
handleMainPlayerAct={handleMainPlayerAct} handleMainPlayerAct={handleMainPlayerAct}
@ -520,7 +634,77 @@ function PvEDoudizhuDemoView() {
</Paper> </Paper>
</div> </div>
</Layout.Col> </Layout.Col>
<Layout.Col span="7" style={{ height: '100%' }}>
<Paper className={'doudizhu-probability-paper'} elevation={3}>
<div className={'probability-player'}>
{playerInfo.length > 0 && gameState.currentPlayer !== null ? (
<span>
Current Player: {gameState.currentPlayer}
<br />
{playerInfo[gameState.currentPlayer].role}
</span>
) : (
<span>Waiting...</span>
)}
</div>
<Divider />
<div className={'probability-table'}>
<div className={'probability-item'}>{computeProbabilityItem(0)}</div>
<div className={'probability-item'}>{computeProbabilityItem(1)}</div>
<div className={'probability-item'}>{computeProbabilityItem(2)}</div>
</div>
</Paper>
</Layout.Col>
</Layout.Row> </Layout.Row>
<div className="game-controller">
<Paper className={'game-controller-paper'} elevation={3}>
<Layout.Row style={{ height: '51px' }}>
<Layout.Col span="6" style={{ height: '51px', lineHeight: '48px' }}>
<FormGroup style={{ height: '100%' }}>
<FormControlLabel
style={{ textAlign: 'center', height: '100%', display: 'table' }}
class="switch-control"
control={<Switch checked={!hideRivalHand} onChange={toggleHideRivalHand} />}
label="Show Rival Cards"
/>
</FormGroup>
</Layout.Col>
<Layout.Col span="1" style={{ height: '100%', width: '1px' }}>
<Divider orientation="vertical" />
</Layout.Col>
<Layout.Col span="6" style={{ height: '51px', lineHeight: '48px' }}>
<FormGroup sty282718 le={{ height: '100%' }}>
<FormControlLabel
style={{ textAlign: 'center', height: '100%', display: 'table' }}
class="switch-control"
control={
<Switch checked={!hidePredictionArea} onChange={toggleHidePredictionArea} />
}
label="Show Prediction Area"
/>
</FormGroup>
</Layout.Col>
<Layout.Col span="1" style={{ height: '100%', width: '1px' }}>
<Divider orientation="vertical" />
</Layout.Col>
<Layout.Col
span="6"
style={{ height: '51px', lineHeight: '51px', marginLeft: '-1px', marginRight: '-1px' }}
>
<div style={{ textAlign: 'center' }}>{`Turn ${gameState.turn}`}</div>
</Layout.Col>
<Layout.Col span="1" style={{ height: '100%', width: '1px' }}>
<Divider orientation="vertical" />
</Layout.Col>
<Layout.Col
span="6"
style={{ height: '51px', lineHeight: '51px', marginLeft: '-1px', marginRight: '-1px' }}
>
<div style={{ textAlign: 'center' }}>{`Game Status: ${gameStatus}`}</div>
</Layout.Col>
</Layout.Row>
</Paper>
</div>
</div> </div>
</div> </div>
); );