From 66639d1aa862da15b0e3294144a4d3ad8ceb8546 Mon Sep 17 00:00:00 2001 From: Songyi Huang Date: Tue, 13 Apr 2021 21:07:43 -0700 Subject: [PATCH 01/14] init PVE Doudizhu view file --- src/App.js | 9 ++++++++- src/view/PVEView/PVEDoudizhuDemoView.js | 15 +++++++++++++++ src/view/PVEView/index.js | 3 +++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/view/PVEView/PVEDoudizhuDemoView.js create mode 100644 src/view/PVEView/index.js diff --git a/src/App.js b/src/App.js index 9d0520e..200f935 100644 --- a/src/App.js +++ b/src/App.js @@ -2,6 +2,7 @@ import React from 'react'; import { BrowserRouter as Router, Route, Redirect } from "react-router-dom"; import LeaderBoard from './view/LeaderBoard'; import { DoudizhuGameView, LeducHoldemGameView } from './view/GameView'; +import { PVEDoudizhuDemoView } from './view/PVEView'; import Navbar from "./components/Navbar"; import {makeStyles} from "@material-ui/core/styles"; @@ -13,16 +14,22 @@ const useStyles = makeStyles(() => ({ function App() { const classes = useStyles(); + + // todo: add 404 page + return (
- + {/* for test use */} + {/* */} + +
); diff --git a/src/view/PVEView/PVEDoudizhuDemoView.js b/src/view/PVEView/PVEDoudizhuDemoView.js new file mode 100644 index 0000000..1968f81 --- /dev/null +++ b/src/view/PVEView/PVEDoudizhuDemoView.js @@ -0,0 +1,15 @@ +import React from 'react'; + +class PVEDoudizhuDemoView extends React.Component { + constructor(props) { + super(props); + } + + render() { + return ( +
Under Development...
+ ); + } +} + +export default PVEDoudizhuDemoView; \ No newline at end of file diff --git a/src/view/PVEView/index.js b/src/view/PVEView/index.js new file mode 100644 index 0000000..c2b1b61 --- /dev/null +++ b/src/view/PVEView/index.js @@ -0,0 +1,3 @@ +import PVEDoudizhuDemoView from './PVEDoudizhuDemoView'; + +export {PVEDoudizhuDemoView}; \ No newline at end of file From fa35a343550c86058e104d63ca87f978b3443cd2 Mon Sep 17 00:00:00 2001 From: Songyi Huang Date: Tue, 13 Apr 2021 22:23:58 -0700 Subject: [PATCH 02/14] refactor navbar to functional component --- src/App.js | 28 +++++++-------- src/components/Navbar.js | 46 ++++++++++++------------- src/index.js | 1 + src/view/PVEView/PVEDoudizhuDemoView.js | 11 ++++-- src/view/PVEView/index.js | 4 +-- 5 files changed, 46 insertions(+), 44 deletions(-) diff --git a/src/App.js b/src/App.js index 200f935..ca79191 100644 --- a/src/App.js +++ b/src/App.js @@ -1,35 +1,33 @@ import React from 'react'; import { BrowserRouter as Router, Route, Redirect } from "react-router-dom"; import LeaderBoard from './view/LeaderBoard'; -import { DoudizhuGameView, LeducHoldemGameView } from './view/GameView'; -import { PVEDoudizhuDemoView } from './view/PVEView'; +import { DoudizhuReplayView, LeducHoldemReplayView } from './view/ReplayView'; +import { PvEDoudizhuDemoView } from './view/PvEView'; import Navbar from "./components/Navbar"; -import {makeStyles} from "@material-ui/core/styles"; -const useStyles = makeStyles(() => ({ - navBar: { - zIndex: 1002 - } -})); +const navbarSubtitleMap = { + "/leaderboard": "", + "/replay/doudizhu": "Doudizhu", + "/replay/leduc-holdem": "Leduc Hold'em", + "/pve/doudizhu-demo": "Doudizhu PvE Demo" +}; function App() { - const classes = useStyles(); - // todo: add 404 page return ( - +
{/* for test use */} {/* */} - + - - - + + +
); diff --git a/src/components/Navbar.js b/src/components/Navbar.js index e4f21aa..dc791c2 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -1,39 +1,37 @@ -import React from "react"; -import { Link } from 'react-router-dom'; +import React, { useEffect, useState } from "react"; +import { Link, useLocation } from 'react-router-dom'; import logo_white from "../assets/images/logo_white.png"; import GitHubIcon from "@material-ui/icons/GitHub"; import AppBar from "@material-ui/core/AppBar"; import axios from 'axios'; -class Navbar extends React.Component { - constructor(props) { - super(props); +function Navbar({subtitleMap}) { + const [stars, setStars] = useState('...'); + let location = useLocation(); + console.log(location.pathname, subtitleMap); + const subtitle = subtitleMap[location.pathname]; - this.state = {stars: '...'}; - } - - componentDidMount() { + useEffect(() => { axios.get("https://api.github.com/repos/datamllab/rlcard") .then(res=>{ - this.setState({stars: res.data.stargazers_count}); + setStars(res.data.stargazers_count); }); - } + }, []) - render() { - return ( - -
- {"Logo"} -
Showdown{this.props.gameName === '' ? '' : '/ ' + this.props.gameName}
-
-
{window.location.href = 'https://github.com/datamllab/rlcard'}}> -
-
Github
{this.state.stars} stars
-
+ return ( + +
+ {"Logo"} +
Showdown{subtitle ? '/ ' + subtitle : ''}
+
+
{window.location.href = 'https://github.com/datamllab/rlcard'}}> +
+
Github
{stars} stars
- +
+ ) - } + } export default Navbar; diff --git a/src/index.js b/src/index.js index 8f9c359..1d01335 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; + import './assets/index.scss'; import App from './App'; diff --git a/src/view/PVEView/PVEDoudizhuDemoView.js b/src/view/PVEView/PVEDoudizhuDemoView.js index 1968f81..9e772a2 100644 --- a/src/view/PVEView/PVEDoudizhuDemoView.js +++ b/src/view/PVEView/PVEDoudizhuDemoView.js @@ -1,15 +1,20 @@ import React from 'react'; +import Navbar from "../../components/Navbar"; +import { DoudizhuGameBoard } from '../../components/GameBoard'; -class PVEDoudizhuDemoView extends React.Component { + +class PvEDoudizhuDemoView extends React.Component { constructor(props) { super(props); } render() { return ( -
Under Development...
+
+
Under Development...
+
); } } -export default PVEDoudizhuDemoView; \ No newline at end of file +export default PvEDoudizhuDemoView; \ No newline at end of file diff --git a/src/view/PVEView/index.js b/src/view/PVEView/index.js index c2b1b61..5d89759 100644 --- a/src/view/PVEView/index.js +++ b/src/view/PVEView/index.js @@ -1,3 +1,3 @@ -import PVEDoudizhuDemoView from './PVEDoudizhuDemoView'; +import PvEDoudizhuDemoView from './PvEDoudizhuDemoView'; -export {PVEDoudizhuDemoView}; \ No newline at end of file +export {PvEDoudizhuDemoView}; \ No newline at end of file From ba2ec6e3c61b13fd98a88b5e86679bd6dafa5b9a Mon Sep 17 00:00:00 2001 From: Songyi Huang Date: Tue, 13 Apr 2021 23:01:21 -0700 Subject: [PATCH 03/14] refactor --- src/view/GameView/index.js | 4 ---- .../DoudizhuReplayView.js} | 6 ++---- .../LeducHoldemReplayView.js} | 6 ++---- src/view/ReplayView/index.js | 4 ++++ 4 files changed, 8 insertions(+), 12 deletions(-) delete mode 100644 src/view/GameView/index.js rename src/view/{GameView/DoudizhuGameView.js => ReplayView/DoudizhuReplayView.js} (99%) rename src/view/{GameView/LeducHoldemGameView.js => ReplayView/LeducHoldemReplayView.js} (99%) create mode 100644 src/view/ReplayView/index.js diff --git a/src/view/GameView/index.js b/src/view/GameView/index.js deleted file mode 100644 index a64e709..0000000 --- a/src/view/GameView/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import DoudizhuGameView from "./DoudizhuGameView"; -import LeducHoldemGameView from "./LeducHoldemGameView"; - -export {DoudizhuGameView, LeducHoldemGameView}; diff --git a/src/view/GameView/DoudizhuGameView.js b/src/view/ReplayView/DoudizhuReplayView.js similarity index 99% rename from src/view/GameView/DoudizhuGameView.js rename to src/view/ReplayView/DoudizhuReplayView.js index 13b15a6..00ed55c 100644 --- a/src/view/GameView/DoudizhuGameView.js +++ b/src/view/ReplayView/DoudizhuReplayView.js @@ -2,7 +2,6 @@ import React from 'react'; import axios from 'axios'; import '../../assets/gameview.scss'; import { DoudizhuGameBoard } from '../../components/GameBoard'; -import Navbar from "../../components/Navbar"; import {removeCards, doubleRaf, deepCopy, computeHandCardsWidth, translateCardData} from "../../utils"; import { apiUrl } from "../../utils/config"; @@ -25,7 +24,7 @@ import DialogActions from "@material-ui/core/DialogActions"; import Dialog from "@material-ui/core/Dialog"; import qs from "query-string"; -class DoudizhuGameView extends React.Component { +class DoudizhuReplayView extends React.Component { constructor(props) { super(props); @@ -381,7 +380,6 @@ class DoudizhuGameView extends React.Component { -
@@ -493,4 +491,4 @@ class DoudizhuGameView extends React.Component { } } -export default DoudizhuGameView; +export default DoudizhuReplayView; diff --git a/src/view/GameView/LeducHoldemGameView.js b/src/view/ReplayView/LeducHoldemReplayView.js similarity index 99% rename from src/view/GameView/LeducHoldemGameView.js rename to src/view/ReplayView/LeducHoldemReplayView.js index cf77741..fb60116 100644 --- a/src/view/GameView/LeducHoldemGameView.js +++ b/src/view/ReplayView/LeducHoldemReplayView.js @@ -3,7 +3,6 @@ import axios from 'axios'; import qs from 'query-string'; import '../../assets/gameview.scss'; import {LeducHoldemGameBoard} from '../../components/GameBoard'; -import Navbar from '../../components/Navbar'; import {deepCopy} from "../../utils"; import { apiUrl } from "../../utils/config"; @@ -26,7 +25,7 @@ import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; -class LeducHoldemGameView extends React.Component { +class LeducHoldemReplayView extends React.Component { constructor(props) { super(props); @@ -398,7 +397,6 @@ class LeducHoldemGameView extends React.Component { -
@@ -513,4 +511,4 @@ class LeducHoldemGameView extends React.Component { } } -export default LeducHoldemGameView; +export default LeducHoldemReplayView; diff --git a/src/view/ReplayView/index.js b/src/view/ReplayView/index.js new file mode 100644 index 0000000..0cd02f6 --- /dev/null +++ b/src/view/ReplayView/index.js @@ -0,0 +1,4 @@ +import DoudizhuReplayView from "./DoudizhuReplayView"; +import LeducHoldemReplayView from "./LeducHoldemReplayView"; + +export {DoudizhuReplayView, LeducHoldemReplayView}; From 549482d3eba3de36315cbefcf6b7c4296d2bb089 Mon Sep 17 00:00:00 2001 From: Songyi Huang Date: Wed, 14 Apr 2021 23:04:21 -0700 Subject: [PATCH 04/14] add config for eslint, prettier; implementing doudizhu demo... --- .eslintrc | 15 ++++ .prettierignore | 4 ++ .prettierrc | 6 ++ src/utils/index.js | 93 +++++++++++++----------- src/view/PVEView/PVEDoudizhuDemoView.js | 95 +++++++++++++++++++++---- 5 files changed, 157 insertions(+), 56 deletions(-) create mode 100644 .eslintrc create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..e6014d8 --- /dev/null +++ b/.eslintrc @@ -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" + } +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..4a28355 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +# Folders +dist/ +node_modules/ +src/public/lib/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ade7841 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 120, + "tabWidth": 4 +} diff --git a/src/utils/index.js b/src/utils/index.js index 16ab065..e20dc29 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,66 +1,73 @@ -const suitMap = new Map( - [["H", "hearts"], ["D", "diams"], ["S", "spades"], ["C", "clubs"]] -); +const suitMap = new Map([ + ['H', 'hearts'], + ['D', 'diams'], + ['S', 'spades'], + ['C', 'clubs'], +]); -const suitMapSymbol = new Map( - [["H", "\u2665"], ["D", "\u2666"], ["S", "\u2660"], ["C", "\u2663"]] -); +const suitMapSymbol = new Map([ + ['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); // if the player's action is pass then return the copy of original hands - if(cards === "pass"){ + if (cards === 'pass') { return remainedHands; } let misMatch = false; - cards.forEach(card => { - let foundIdx = remainedHands.findIndex(element => {return element === card;}); - if(foundIdx > -1){ + cards.forEach((card) => { + let foundIdx = remainedHands.findIndex((element) => { + return element === card; + }); + if (foundIdx > -1) { remainedHands.splice(foundIdx, 1); - }else { + } else { misMatch = true; } }); - if(misMatch) - return false; - else - return remainedHands; + if (misMatch) return false; + else return remainedHands; } -export function doubleRaf(callback){ +export function doubleRaf(callback) { // secure all the animation got rendered before callback function gets executed requestAnimationFrame(() => { - requestAnimationFrame(callback) - }) + requestAnimationFrame(callback); + }); } -export function deepCopy(toCopy){ +export function deepCopy(toCopy) { return JSON.parse(JSON.stringify(toCopy)); } export function translateCardData(card) { let rankClass; - let suitClass = ""; + let suitClass = ''; let rankText; - let suitText = ""; + let suitText = ''; // translate rank - if(card === "RJ"){ - rankClass = "big"; - rankText = "+"; - suitClass = "joker"; - suitText = "Joker"; - }else if(card === "BJ"){ - rankClass = "little"; - rankText = "-"; - suitClass = "joker"; - suitText = "Joker"; - }else{ - rankClass = card.charAt(1) === "T" ? `10` : card.charAt(1).toLowerCase(); + if (card === 'RJ') { + rankClass = 'big'; + rankText = '+'; + suitClass = 'joker'; + suitText = 'Joker'; + } else if (card === 'BJ') { + rankClass = 'little'; + rankText = '-'; + suitClass = 'joker'; + suitText = 'Joker'; + } else { + rankClass = card.charAt(1) === 'T' ? `10` : card.charAt(1).toLowerCase(); rankClass = `rank-${rankClass}`; - rankText = card.charAt(1) === "T" ? `10` : card.charAt(1); + rankText = card.charAt(1) === 'T' ? `10` : card.charAt(1); } // translate suitClass - if(card !== "RJ" && card !== "BJ"){ + if (card !== 'RJ' && card !== 'BJ') { suitClass = suitMap.get(card.charAt(0)); suitText = suitMapSymbol.get(card.charAt(0)); } @@ -68,14 +75,15 @@ export function translateCardData(card) { return [rankClass, suitClass, rankText, suitText]; } -export function millisecond2Second(t){ - return Math.ceil(t/1000); +export function millisecond2Second(t) { + return Math.ceil(t / 1000); } export function debounce(func, wait, immediate) { let timeout; - return function() { - const context = this, args = arguments; + return function () { + const context = this, + args = arguments; const later = function () { timeout = null; if (!immediate) func.apply(context, args); @@ -88,7 +96,6 @@ export function debounce(func, wait, immediate) { } export function computeHandCardsWidth(num, emWidth) { - if(num === 0) - return 0; - return (num-1)*1.1*emWidth + 4.3*emWidth*1.2 + 2; + if (num === 0) return 0; + return (num - 1) * 1.1 * emWidth + 4.3 * emWidth * 1.2 + 2; } diff --git a/src/view/PVEView/PVEDoudizhuDemoView.js b/src/view/PVEView/PVEDoudizhuDemoView.js index 9e772a2..75b9047 100644 --- a/src/view/PVEView/PVEDoudizhuDemoView.js +++ b/src/view/PVEView/PVEDoudizhuDemoView.js @@ -1,20 +1,89 @@ -import React from 'react'; -import Navbar from "../../components/Navbar"; +import Paper from '@material-ui/core/Paper'; +import { Layout } from 'element-react'; +import React, { useEffect, useState } from 'react'; import { DoudizhuGameBoard } from '../../components/GameBoard'; +// for test use +const generatedPlayerInfo = [ + { + id: 0, + index: 0, + role: 'peasant', + }, + { + id: 1, + index: 1, + role: 'peasant', + }, + { + id: 2, + index: 2, + role: 'landlord', + }, +]; +const initHands = [ + 'S2 H2 HK DK HQ CQ DQ CJ S9 H9 D9 C7 S6 H6 C4 D4 S3', + 'C2 HA CA DA SQ ST HT D8 S7 H7 C6 D6 S5 H5 C5 S4 H4', + 'RJ BJ D2 SA SK CK SJ HJ DJ CT DT C9 S8 H8 C8 D7 D5 H3 S3 D3', +]; -class PvEDoudizhuDemoView extends React.Component { - constructor(props) { - super(props); - } +function PvEDoudizhuDemoView() { + const initConsiderationTime = 2000; - render() { - return ( -
-
Under Development...
+ const [playerInfo, setPlayerInfo] = useState([]); + const [hands, setHands] = useState([]); + const [latestAction, setLatestAction] = useState([[], [], []]); + const mainPlayerId = 0; + const [currentPlayer, setCurrentPlayer] = useState(null); + const [considerationTime, setConsiderationTime] = useState(initConsiderationTime); + const [turn, setTurn] = useState(0); + const [toggleFade, setToggleFade] = useState(''); + const [gameStatus, setGameStatus] = useState('ready'); // "ready", "playing", "paused", "over" + + const cardStr2Arr = (cardStr) => { + return cardStr === 'pass' || cardStr === '' ? 'pass' : cardStr.split(' '); + }; + + // todo: generate inital player / hand states + + // set init game state + useEffect(() => { + setPlayerInfo(generatedPlayerInfo); + setHands(initHands.map((element) => cardStr2Arr(element))); + }, []); + + // start game + + const runNewTurn = () => { + // gameStateTimer(); + }; + + return ( +
+
+ + +
+ + runNewTurn(prevTurn)} + toggleFade={toggleFade} + gameStatus={gameStatus} + /> + +
+
+
- ); - } +
+ ); } -export default PvEDoudizhuDemoView; \ No newline at end of file +export default PvEDoudizhuDemoView; From 71a26334f073a0da14ea724461644e48dea7bc63 Mon Sep 17 00:00:00 2001 From: Songyi Huang Date: Thu, 15 Apr 2021 23:39:50 -0700 Subject: [PATCH 05/14] implementing PVE doudizhu game logic... --- src/view/PVEView/PVEDoudizhuDemoView.js | 120 ++++++++++++++++++------ 1 file changed, 89 insertions(+), 31 deletions(-) diff --git a/src/view/PVEView/PVEDoudizhuDemoView.js b/src/view/PVEView/PVEDoudizhuDemoView.js index 75b9047..0d7f255 100644 --- a/src/view/PVEView/PVEDoudizhuDemoView.js +++ b/src/view/PVEView/PVEDoudizhuDemoView.js @@ -2,25 +2,8 @@ import Paper from '@material-ui/core/Paper'; import { Layout } from 'element-react'; import React, { useEffect, useState } from 'react'; import { DoudizhuGameBoard } from '../../components/GameBoard'; +import { deepCopy } from '../../utils'; -// for test use -const generatedPlayerInfo = [ - { - id: 0, - index: 0, - role: 'peasant', - }, - { - id: 1, - index: 1, - role: 'peasant', - }, - { - id: 2, - index: 2, - role: 'landlord', - }, -]; const initHands = [ 'S2 H2 HK DK HQ CQ DQ CJ S9 H9 D9 C7 S6 H6 C4 D4 S3', 'C2 HA CA DA SQ ST HT D8 S7 H7 C6 D6 S5 H5 C5 S4 H4', @@ -29,30 +12,105 @@ const initHands = [ function PvEDoudizhuDemoView() { const initConsiderationTime = 2000; - - const [playerInfo, setPlayerInfo] = useState([]); - const [hands, setHands] = useState([]); - const [latestAction, setLatestAction] = useState([[], [], []]); + const considerationTimeDeduction = 200; const mainPlayerId = 0; - const [currentPlayer, setCurrentPlayer] = useState(null); + + let gameStateTimeout = null; const [considerationTime, setConsiderationTime] = useState(initConsiderationTime); - const [turn, setTurn] = useState(0); const [toggleFade, setToggleFade] = useState(''); const [gameStatus, setGameStatus] = useState('ready'); // "ready", "playing", "paused", "over" + const [gameState, setGameState] = useState({ + hands: [[], [], []], + latestAction: [[], [], []], + currentPlayer: null, + turn: 0, + }); const cardStr2Arr = (cardStr) => { return cardStr === 'pass' || cardStr === '' ? 'pass' : cardStr.split(' '); }; // todo: generate inital player / hand states + // for test use + const playerInfo = [ + { + id: 0, + index: 0, + role: 'peasant', + }, + { + id: 1, + index: 1, + role: 'peasant', + }, + { + id: 2, + index: 2, + role: 'landlord', + }, + ]; + + function timeout(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + const proceedNextTurn = () => { + // todo + }; + + const requestApiPlay = async () => { + // mock delayed API play + await timeout(250); + const apiRes = gameState.hands[gameState.currentPlayer][gameState.hands[gameState.currentPlayer].length - 1]; + console.log('mock api res', apiRes); + }; + + const gameStateTimer = () => { + gameStateTimeout = setTimeout(() => { + let currentConsiderationTime = considerationTime; + if (currentConsiderationTime > 0) { + currentConsiderationTime -= considerationTimeDeduction; + currentConsiderationTime = Math.max(currentConsiderationTime, 0); + if (currentConsiderationTime === 0) { + // consideration time used up for current player + // if current player is controlled by user, play a random card + // todo + } + setConsiderationTime(currentConsiderationTime); + } else { + // consideration time used up for current player + // if current player is controlled by user, play a random card + // todo + } + }, considerationTimeDeduction); + }; + + useEffect(() => { + console.log(considerationTime); + gameStateTimer(); + }, [considerationTime]); // set init game state useEffect(() => { - setPlayerInfo(generatedPlayerInfo); - setHands(initHands.map((element) => cardStr2Arr(element))); + // 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) => cardStr2Arr(element)); + setGameState(newGameState); + gameStateTimer(); }, []); - // start game + useEffect(() => { + if (gameState.currentPlayer) { + // if current player is not user, request for API player + if (gameState.currentPlayer !== mainPlayerId) { + requestApiPlay(); + } + } + }, [gameState.currentPlayer]); const runNewTurn = () => { // gameStateTimer(); @@ -67,12 +125,12 @@ function PvEDoudizhuDemoView() { runNewTurn(prevTurn)} toggleFade={toggleFade} gameStatus={gameStatus} From bce52cf9489caa02250a103c0685a6d3dbf36002 Mon Sep 17 00:00:00 2001 From: Songyi Huang Date: Sat, 17 Apr 2021 21:29:04 -0700 Subject: [PATCH 06/14] implementing doudidzhu demo... --- src/utils/index.js | 10 +++++++ src/view/PVEView/PVEDoudizhuDemoView.js | 35 ++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/utils/index.js b/src/utils/index.js index e20dc29..a04d085 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -99,3 +99,13 @@ export function computeHandCardsWidth(num, emWidth) { if (num === 0) return 0; return (num - 1) * 1.1 * emWidth + 4.3 * emWidth * 1.2 + 2; } + +export function card2SuiteAndRank(card) { + if (card === 'BJ') { + return {rank: null, suite: 'X'}; + } else if (card === 'RJ') { + return {rank: null, suite: 'D'}; + } else { + return {rank: card[0], suite: card[1]}; + } +} \ No newline at end of file diff --git a/src/view/PVEView/PVEDoudizhuDemoView.js b/src/view/PVEView/PVEDoudizhuDemoView.js index 0d7f255..e451ce1 100644 --- a/src/view/PVEView/PVEDoudizhuDemoView.js +++ b/src/view/PVEView/PVEDoudizhuDemoView.js @@ -2,7 +2,7 @@ import Paper from '@material-ui/core/Paper'; import { Layout } from 'element-react'; import React, { useEffect, useState } from 'react'; import { DoudizhuGameBoard } from '../../components/GameBoard'; -import { deepCopy } from '../../utils'; +import { deepCopy, card2SuiteAndRank } from '../../utils'; const initHands = [ 'S2 H2 HK DK HQ CQ DQ CJ S9 H9 D9 C7 S6 H6 C4 D4 S3', @@ -22,7 +22,7 @@ function PvEDoudizhuDemoView() { const [gameState, setGameState] = useState({ hands: [[], [], []], latestAction: [[], [], []], - currentPlayer: null, + currentPlayer: null, // index of current player turn: 0, }); @@ -54,8 +54,35 @@ function PvEDoudizhuDemoView() { return new Promise((resolve) => setTimeout(resolve, ms)); } - const proceedNextTurn = () => { - // todo + const proceedNextTurn = (playingCard, rankOnly = true) => { + let newGameState = deepCopy(gameState); + + // todo: take played card out from hand, and generate playing cards with suite + const currentHand = newGameState.hands[gameState.currentPlayer]; + + let newHand = []; + let newLatestAction = [] + 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 { + // todo: proceed player's action + } + + newGameState.latestAction[gameState.currentPlayer] = newLatestAction; + newGameState.hands[gameState.currentPlayer] = newHand; + }; const requestApiPlay = async () => { From 426d90cf51d5f0f7f441b04a4b18665e73517f5f Mon Sep 17 00:00:00 2001 From: Songyi Huang Date: Sun, 18 Apr 2021 19:04:57 -0700 Subject: [PATCH 07/14] implemented user select hand functionality; implemented remove played card functionality --- src/assets/cards.css | 7 +++- src/components/GameBoard/DoudizhuGameBoard.js | 10 ++++- src/utils/index.js | 6 +-- src/view/LeaderBoard.js | 9 ++++- .../PvEDoudizhuDemoView.js} | 38 +++++++++++++++---- src/view/{PVEView => PvEView}/index.js | 0 6 files changed, 56 insertions(+), 14 deletions(-) rename src/view/{PVEView/PVEDoudizhuDemoView.js => PvEView/PvEDoudizhuDemoView.js} (81%) rename src/view/{PVEView => PvEView}/index.js (100%) diff --git a/src/assets/cards.css b/src/assets/cards.css index a6f4b75..bbed0fd 100644 --- a/src/assets/cards.css +++ b/src/assets/cards.css @@ -60,8 +60,13 @@ .playingCards a.card { text-decoration: none; } + +.playingCards.selectable a.card { + cursor: pointer; +} + /* selected and hover state */ -.playingCards a.card:hover, .playingCards a.card:active, +.playingCards a.card:hover, .playingCards a.card:active, .playingCards a.card.selected, .playingCards label.card:hover, .playingCards strong .card { bottom: 1em; diff --git a/src/components/GameBoard/DoudizhuGameBoard.js b/src/components/GameBoard/DoudizhuGameBoard.js index 2cf7dcc..0cc68f0 100644 --- a/src/components/GameBoard/DoudizhuGameBoard.js +++ b/src/components/GameBoard/DoudizhuGameBoard.js @@ -51,13 +51,19 @@ class DoudizhuGameBoard extends React.Component { return
PASS
}else{ return ( -
+
    {cards.map(card=>{ const [rankClass, suitClass, rankText, suitText] = translateCardData(card); + let selected = false; + if (this.props.handSelectable) { + selected = this.props.selectedCards.indexOf(card) >= 0; + } + + // todo: right click and move to select multiple cards return (
  • - + this.props.handleSelectedCards([card])} className={`card ${rankClass} ${suitClass} ${selected ? 'selected' : ''}`}> {rankText} {suitText} diff --git a/src/utils/index.js b/src/utils/index.js index a04d085..c4b577c 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -102,10 +102,10 @@ export function computeHandCardsWidth(num, emWidth) { export function card2SuiteAndRank(card) { if (card === 'BJ') { - return {rank: null, suite: 'X'}; + return {suite: null, rank: 'X'}; } else if (card === 'RJ') { - return {rank: null, suite: 'D'}; + return {suite: null, rank: 'D'}; } else { - return {rank: card[0], suite: card[1]}; + return {suite: card[0], rank: card[1]}; } } \ No newline at end of file diff --git a/src/view/LeaderBoard.js b/src/view/LeaderBoard.js index 5556f99..3caa184 100644 --- a/src/view/LeaderBoard.js +++ b/src/view/LeaderBoard.js @@ -62,7 +62,14 @@ function LeaderBoard () { fetchModelData(); }, [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/`; if (type === 'game') { requestUrl += `query_agent_payoff?name=${name}&elements_every_page=${rowsPerPage}&page_index=${page}` diff --git a/src/view/PVEView/PVEDoudizhuDemoView.js b/src/view/PvEView/PvEDoudizhuDemoView.js similarity index 81% rename from src/view/PVEView/PVEDoudizhuDemoView.js rename to src/view/PvEView/PvEDoudizhuDemoView.js index e451ce1..996c04a 100644 --- a/src/view/PVEView/PVEDoudizhuDemoView.js +++ b/src/view/PvEView/PvEDoudizhuDemoView.js @@ -10,12 +10,13 @@ const initHands = [ 'RJ BJ D2 SA SK CK SJ HJ DJ CT DT C9 S8 H8 C8 D7 D5 H3 S3 D3', ]; +let gameStateTimeout = null; + function PvEDoudizhuDemoView() { const initConsiderationTime = 2000; const considerationTimeDeduction = 200; const mainPlayerId = 0; - let gameStateTimeout = null; const [considerationTime, setConsiderationTime] = useState(initConsiderationTime); const [toggleFade, setToggleFade] = useState(''); const [gameStatus, setGameStatus] = useState('ready'); // "ready", "playing", "paused", "over" @@ -25,6 +26,7 @@ function PvEDoudizhuDemoView() { currentPlayer: null, // index of current player turn: 0, }); + const [selectedCards, setSelectedCards] = useState([]); // user selected hand card const cardStr2Arr = (cardStr) => { return cardStr === 'pass' || cardStr === '' ? 'pass' : cardStr.split(' '); @@ -67,9 +69,9 @@ function PvEDoudizhuDemoView() { if (playingCard.length === 0) return true; - const [_, rank] = card2SuiteAndRank(card); + const { rank } = card2SuiteAndRank(card); const idx = playingCard.indexOf(rank); - if (idx > 0) { + if (idx >= 0) { playingCard.splice(idx, 1); newLatestAction.push(card); return false; @@ -82,15 +84,35 @@ function PvEDoudizhuDemoView() { newGameState.latestAction[gameState.currentPlayer] = newLatestAction; newGameState.hands[gameState.currentPlayer] = newHand; + newGameState.currentPlayer = (newGameState.currentPlayer + 1) % 3; + newGameState.turn++; + setGameState(newGameState); + if (gameStateTimeout) { + clearTimeout(gameStateTimeout); + setConsiderationTime(initConsiderationTime); + } }; const requestApiPlay = async () => { // mock delayed API play - await timeout(250); - const apiRes = gameState.hands[gameState.currentPlayer][gameState.hands[gameState.currentPlayer].length - 1]; - console.log('mock api res', apiRes); + await timeout(1200); + const apiRes = [card2SuiteAndRank(gameState.hands[gameState.currentPlayer][gameState.hands[gameState.currentPlayer].length - 1]).rank]; + console.log('mock api res', apiRes, gameStateTimeout); + proceedNextTurn(apiRes); }; + + 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(() => { @@ -113,7 +135,6 @@ function PvEDoudizhuDemoView() { }; useEffect(() => { - console.log(considerationTime); gameStateTimer(); }, [considerationTime]); @@ -151,8 +172,11 @@ function PvEDoudizhuDemoView() {
    Date: Mon, 19 Apr 2021 21:41:15 -0700 Subject: [PATCH 08/14] add user play button & handler functionality; adjust card style --- src/assets/cards.css | 21 +++++++++++++--- src/assets/doudizhu.scss | 7 +++++- src/components/GameBoard/DoudizhuGameBoard.js | 24 +++++++++++++++---- src/view/PvEView/PvEDoudizhuDemoView.js | 24 +++++++++++++++++-- 4 files changed, 65 insertions(+), 11 deletions(-) diff --git a/src/assets/cards.css b/src/assets/cards.css index bbed0fd..6c64e8e 100644 --- a/src/assets/cards.css +++ b/src/assets/cards.css @@ -37,8 +37,8 @@ .playingCards .card { display: inline-block; - width: 3.3em; - height: 4.6em; + width: 2.5em; + height: 3.5em; border: 1px solid #666; border-radius: .3em; -moz-border-radius: .3em; @@ -47,7 +47,7 @@ padding: .25em; margin: 0 .5em .5em 0; 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-family: Arial, sans-serif; position: relative; @@ -65,6 +65,21 @@ cursor: pointer; } +/* override orignal hover style */ +.playingCards.selectable a.card:hover { + bottom: 0; +} + +.playingCards.selectable a.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 */ .playingCards a.card:hover, .playingCards a.card:active, .playingCards a.card.selected, .playingCards label.card:hover, diff --git a/src/assets/doudizhu.scss b/src/assets/doudizhu.scss index f20b1e2..296d816 100644 --- a/src/assets/doudizhu.scss +++ b/src/assets/doudizhu.scss @@ -26,6 +26,11 @@ transition: visibility 0.1s, opacity 0.05s; opacity: 0; } + + .main-player-action-wrapper { + display: flex; + align-items: center; + } .timer { visibility: visible; @@ -35,7 +40,7 @@ height: 60px; .timer-text { color: #303133; - margin-top: 5px; + transform: translateY(5px); font-size: 23px; font-weight: bold; text-shadow: 0 2px 2px #909399; diff --git a/src/components/GameBoard/DoudizhuGameBoard.js b/src/components/GameBoard/DoudizhuGameBoard.js index 0cc68f0..7b536db 100644 --- a/src/components/GameBoard/DoudizhuGameBoard.js +++ b/src/components/GameBoard/DoudizhuGameBoard.js @@ -6,6 +6,7 @@ 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'; @@ -132,12 +133,25 @@ class DoudizhuGameBoard extends React.Component { else if(this.props.toggleFade === "fade-in" && (playerIdx+1)%3 === this.props.currentPlayer) fadeClassName = "scale-fade-in"; if(this.props.currentPlayer === playerIdx){ - return ( -
    -
    {millisecond2Second(this.props.considerationTime)}
    -
    - ) + if (this.props.mainPlayerId === this.props.playerInfo[this.props.currentPlayer].id) { + return ( +
    +
    +
    {millisecond2Second(this.props.considerationTime)}
    +
    + +
    + ) + } else { + return ( +
    +
    {millisecond2Second(this.props.considerationTime)}
    +
    + ) + } }else{ + if (playerIdx === this.props.mainPlayerId) + console.log(this.props.latestAction[playerIdx]); return this.computeSingleLineHand(this.props.latestAction[playerIdx], fadeClassName) } } diff --git a/src/view/PvEView/PvEDoudizhuDemoView.js b/src/view/PvEView/PvEDoudizhuDemoView.js index 996c04a..1fd7976 100644 --- a/src/view/PvEView/PvEDoudizhuDemoView.js +++ b/src/view/PvEView/PvEDoudizhuDemoView.js @@ -62,7 +62,7 @@ function PvEDoudizhuDemoView() { // todo: take played card out from hand, and generate playing cards with suite const currentHand = newGameState.hands[gameState.currentPlayer]; - let newHand = []; + let newHand; let newLatestAction = [] if (rankOnly) { newHand = currentHand.filter(card => { @@ -79,7 +79,18 @@ function PvEDoudizhuDemoView() { return true; }); } else { - // todo: proceed player's action + 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; + }); } newGameState.latestAction[gameState.currentPlayer] = newLatestAction; @@ -163,6 +174,14 @@ function PvEDoudizhuDemoView() { const runNewTurn = () => { // gameStateTimer(); }; + + const handleMainPlayerAct = (type) => { + switch(type) { + case 'play': { + proceedNextTurn(selectedCards, false); + } + } + } return (
    @@ -185,6 +204,7 @@ function PvEDoudizhuDemoView() { runNewTurn={(prevTurn) => runNewTurn(prevTurn)} toggleFade={toggleFade} gameStatus={gameStatus} + handleMainPlayerAct={handleMainPlayerAct} />
    From 77912648fb06140a95ace23985b72f90b30f68c6 Mon Sep 17 00:00:00 2001 From: Songyi Huang Date: Mon, 19 Apr 2021 22:33:19 -0700 Subject: [PATCH 09/14] add animation of playing card in doudizhu demo --- src/assets/cards.css | 8 ++--- src/components/GameBoard/DoudizhuGameBoard.js | 22 ++++++------- src/view/PvEView/PvEDoudizhuDemoView.js | 33 +++++++++++++------ 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/assets/cards.css b/src/assets/cards.css index 6c64e8e..339cb64 100644 --- a/src/assets/cards.css +++ b/src/assets/cards.css @@ -61,16 +61,16 @@ text-decoration: none; } -.playingCards.selectable a.card { +.playingCards.selectable label.card { cursor: pointer; } /* override orignal hover style */ -.playingCards.selectable a.card:hover { +.playingCards.selectable label.card:hover { bottom: 0; } -.playingCards.selectable a.card:active::after { +.playingCards.selectable label.card:active::after { background-color: rgba(23, 146, 210, 0.5); content: ''; position: absolute; @@ -81,7 +81,7 @@ } /* selected and hover state */ -.playingCards a.card:hover, .playingCards a.card:active, .playingCards a.card.selected, +.playingCards a.card:hover, .playingCards a.card:active, .playingCards.selectable label.card.selected, .playingCards label.card:hover, .playingCards strong .card { bottom: 1em; diff --git a/src/components/GameBoard/DoudizhuGameBoard.js b/src/components/GameBoard/DoudizhuGameBoard.js index 7b536db..8e42abe 100644 --- a/src/components/GameBoard/DoudizhuGameBoard.js +++ b/src/components/GameBoard/DoudizhuGameBoard.js @@ -47,27 +47,27 @@ class DoudizhuGameBoard extends React.Component { ) } - computeSingleLineHand(cards, fadeClassName="") { + computeSingleLineHand(cards, fadeClassName="", cardSelectable = false) { if(cards === "pass"){ return
    PASS
    }else{ return ( -
    +
    ) } else { @@ -150,8 +152,6 @@ class DoudizhuGameBoard extends React.Component { ) } }else{ - if (playerIdx === this.props.mainPlayerId) - console.log(this.props.latestAction[playerIdx]); return this.computeSingleLineHand(this.props.latestAction[playerIdx], fadeClassName) } } @@ -218,7 +218,7 @@ class DoudizhuGameBoard extends React.Component {
    {this.computePlayerPortrait(bottomId, bottomIdx)}
    - {bottomIdx >= 0 ?
    {this.computeSingleLineHand(this.props.hands[bottomIdx])}
    :
    Waiting...
    } + {bottomIdx >= 0 ?
    {this.computeSingleLineHand(this.props.hands[bottomIdx], '', true)}
    :
    Waiting...
    }
diff --git a/src/view/PvEView/PvEDoudizhuDemoView.js b/src/view/PvEView/PvEDoudizhuDemoView.js index 1fd7976..4e40014 100644 --- a/src/view/PvEView/PvEDoudizhuDemoView.js +++ b/src/view/PvEView/PvEDoudizhuDemoView.js @@ -57,14 +57,19 @@ function PvEDoudizhuDemoView() { } const proceedNextTurn = (playingCard, rankOnly = true) => { + setToggleFade('fade-out'); + let newGameState = deepCopy(gameState); // todo: take played card out from hand, and generate playing cards with suite const currentHand = newGameState.hands[gameState.currentPlayer]; let newHand; - let newLatestAction = [] - if (rankOnly) { + let newLatestAction = []; + if (playingCard.length === 0) { + newHand = currentHand; + newLatestAction = 'pass'; + } else if (rankOnly) { newHand = currentHand.filter(card => { if (playingCard.length === 0) return true; @@ -92,13 +97,16 @@ function PvEDoudizhuDemoView() { return true; }); } - + 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); @@ -131,11 +139,6 @@ function PvEDoudizhuDemoView() { if (currentConsiderationTime > 0) { currentConsiderationTime -= considerationTimeDeduction; currentConsiderationTime = Math.max(currentConsiderationTime, 0); - if (currentConsiderationTime === 0) { - // consideration time used up for current player - // if current player is controlled by user, play a random card - // todo - } setConsiderationTime(currentConsiderationTime); } else { // consideration time used up for current player @@ -179,6 +182,16 @@ function PvEDoudizhuDemoView() { switch(type) { case 'play': { proceedNextTurn(selectedCards, false); + break; + } + case 'pass': { + proceedNextTurn([], false); + setSelectedCards([]); + break; + } + case 'deselect': { + setSelectedCards([]); + break; } } } @@ -191,7 +204,7 @@ function PvEDoudizhuDemoView() {
Date: Mon, 19 Apr 2021 22:43:32 -0700 Subject: [PATCH 10/14] adjust style of doudizhu player action button --- src/assets/doudizhu.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/assets/doudizhu.scss b/src/assets/doudizhu.scss index 296d816..4adb9c4 100644 --- a/src/assets/doudizhu.scss +++ b/src/assets/doudizhu.scss @@ -30,6 +30,11 @@ .main-player-action-wrapper { display: flex; align-items: center; + + button { + font-weight: bolder; + border-radius: 40px; + } } .timer { From 14ce853ce933dcad919de5951d6af9fb4ebcc6d3 Mon Sep 17 00:00:00 2001 From: Songyi Huang Date: Tue, 20 Apr 2021 23:30:27 -0700 Subject: [PATCH 11/14] add logic for request api play --- src/utils/config.js | 3 +- src/utils/index.js | 138 ++++++++++++++- src/view/PvEView/PvEDoudizhuDemoView.js | 222 +++++++++++++++++++----- 3 files changed, 310 insertions(+), 53 deletions(-) diff --git a/src/utils/config.js b/src/utils/config.js index 40d5cf3..117b76b 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -1,3 +1,4 @@ const apiUrl = 'http://127.0.0.1:8000'; +const douzeroDemoUrl = 'http://127.0.0.1:5000'; -export {apiUrl}; +export { apiUrl, douzeroDemoUrl }; diff --git a/src/utils/index.js b/src/utils/index.js index c4b577c..90ecb6b 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -102,10 +102,140 @@ export function computeHandCardsWidth(num, emWidth) { export function card2SuiteAndRank(card) { if (card === 'BJ') { - return {suite: null, rank: 'X'}; + return { suite: null, rank: 'X' }; } else if (card === 'RJ') { - return {suite: null, rank: 'D'}; + return { suite: null, rank: 'D' }; } else { - return {suite: card[0], rank: card[1]}; + return { suite: card[0], rank: card[1] }; } -} \ No newline at end of file +} + +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; +} diff --git a/src/view/PvEView/PvEDoudizhuDemoView.js b/src/view/PvEView/PvEDoudizhuDemoView.js index 4e40014..597aa18 100644 --- a/src/view/PvEView/PvEDoudizhuDemoView.js +++ b/src/view/PvEView/PvEDoudizhuDemoView.js @@ -1,8 +1,11 @@ import Paper from '@material-ui/core/Paper'; +import axios from 'axios'; import { Layout } from 'element-react'; +import qs from 'query-string'; import React, { useEffect, useState } from 'react'; import { DoudizhuGameBoard } from '../../components/GameBoard'; -import { deepCopy, card2SuiteAndRank } from '../../utils'; +import { card2SuiteAndRank, deepCopy, isDoudizhuBomb, sortDoudizhuCards } from '../../utils'; +import { douzeroDemoUrl } from '../../utils/config'; const initHands = [ 'S2 H2 HK DK HQ CQ DQ CJ S9 H9 D9 C7 S6 H6 C4 D4 S3', @@ -10,47 +13,70 @@ 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 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, + }, +]; +const threeLandlordCards = ['RJ', 'BJ', 'D2']; + let gameStateTimeout = null; -function PvEDoudizhuDemoView() { - const initConsiderationTime = 2000; - const considerationTimeDeduction = 200; - const mainPlayerId = 0; +let gameHistory = []; +let bombNum = 0; +let lastMoveLandlord = []; +let lastMoveLandlordDown = []; +let lastMoveLandlordUp = []; +let playedCardsLandlord = []; +let playedCardsLandlordDown = []; +let playedCardsLandlordUp = []; +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 + currentPlayer: null, // index of current player turn: 0, }); - const [selectedCards, setSelectedCards] = useState([]); // user selected hand card + const [selectedCards, setSelectedCards] = useState([]); // user selected hand card 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 - const playerInfo = [ - { - id: 0, - index: 0, - role: 'peasant', - }, - { - id: 1, - index: 1, - role: 'peasant', - }, - { - id: 2, - index: 2, - role: 'landlord', - }, - ]; function timeout(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -58,22 +84,21 @@ function PvEDoudizhuDemoView() { const proceedNextTurn = (playingCard, rankOnly = true) => { setToggleFade('fade-out'); - + let newGameState = deepCopy(gameState); - - // todo: take played card out from hand, and generate playing cards with suite + + // todo: 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; - + newHand = currentHand.filter((card) => { + if (playingCard.length === 0) return true; + const { rank } = card2SuiteAndRank(card); const idx = playingCard.indexOf(rank); if (idx >= 0) { @@ -85,9 +110,8 @@ function PvEDoudizhuDemoView() { }); } else { newLatestAction = playingCard.slice(); - newHand = currentHand.filter(card => { - if (playingCard.length === 0) - return true; + newHand = currentHand.filter((card) => { + if (playingCard.length === 0) return true; const idx = playingCard.indexOf(card); if (idx >= 0) { @@ -98,13 +122,33 @@ function PvEDoudizhuDemoView() { }); } + // update value records for douzero + // debugger; + const newHistoryRecord = newLatestAction === 'pass' ? [] : newLatestAction; + 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(()=> { + setTimeout(() => { setToggleFade(''); }, 200); if (gameStateTimeout) { @@ -115,23 +159,104 @@ function PvEDoudizhuDemoView() { const requestApiPlay = async () => { // mock delayed API play - await timeout(1200); - const apiRes = [card2SuiteAndRank(gameState.hands[gameState.currentPlayer][gameState.hands[gameState.currentPlayer].length - 1]).rank]; - console.log('mock api res', apiRes, gameStateTimeout); - proceedNextTurn(apiRes); + // 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_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)); + 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([]); + } + console.log(data.status, data.message); + } else { + console.log('api res', data, gameStateTimeout); + 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) { + console.log(err); + } }; - + const handleSelectedCards = (cards) => { let newSelectedCards = selectedCards.slice(); - cards.forEach(card => { + cards.forEach((card) => { if (newSelectedCards.indexOf(card) >= 0) { newSelectedCards.splice(newSelectedCards.indexOf(card), 1); } else { - newSelectedCards.push(card); + newSelectedCards.push(card); } }); setSelectedCards(newSelectedCards); - } + }; const gameStateTimer = () => { gameStateTimeout = setTimeout(() => { @@ -160,7 +285,7 @@ function PvEDoudizhuDemoView() { 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) => cardStr2Arr(element)); + newGameState.hands = initHands.map((element) => sortDoudizhuCards(cardStr2Arr(element))); setGameState(newGameState); gameStateTimer(); }, []); @@ -169,6 +294,7 @@ function PvEDoudizhuDemoView() { if (gameState.currentPlayer) { // if current player is not user, request for API player if (gameState.currentPlayer !== mainPlayerId) { + // debugger; requestApiPlay(); } } @@ -177,9 +303,9 @@ function PvEDoudizhuDemoView() { const runNewTurn = () => { // gameStateTimer(); }; - + const handleMainPlayerAct = (type) => { - switch(type) { + switch (type) { case 'play': { proceedNextTurn(selectedCards, false); break; @@ -194,7 +320,7 @@ function PvEDoudizhuDemoView() { break; } } - } + }; return (
From 9e2e8dccf917ef180d416672ebeedab90bb99e56 Mon Sep 17 00:00:00 2001 From: Songyi Huang Date: Wed, 21 Apr 2021 21:44:22 -0700 Subject: [PATCH 12/14] connect legal action api; add error messages for api request --- src/components/GameBoard/DoudizhuGameBoard.js | 235 ++++++++++-------- src/view/PvEView/PvEDoudizhuDemoView.js | 121 +++++++-- 2 files changed, 229 insertions(+), 127 deletions(-) diff --git a/src/components/GameBoard/DoudizhuGameBoard.js b/src/components/GameBoard/DoudizhuGameBoard.js index 8e42abe..09742e2 100644 --- a/src/components/GameBoard/DoudizhuGameBoard.js +++ b/src/components/GameBoard/DoudizhuGameBoard.js @@ -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' ? (
- {"Landlord"} - ID} - label={playerId} - clickable - color="primary" - /> + {'Landlord'} + ID} label={playerId} clickable color="primary" />
- : + ) : (
- {"Peasant"} - ID} - label={playerId} - clickable - color="primary" - /> + {'Peasant'} + ID} label={playerId} clickable color="primary" />
- }else + ); + } else return (
- {"Player"} - ID} - label={playerId} - clickable - color="primary" - /> + {'Player'} + ID} label={playerId} clickable color="primary" />
- ) + ); } - computeSingleLineHand(cards, fadeClassName="", cardSelectable = false) { - if(cards === "pass"){ - return
PASS
- }else{ + computeSingleLineHand(cards, fadeClassName = '', cardSelectable = false) { + if (cards === 'pass') { return ( -
-
    - {cards.map(card=>{ +
    + PASS +
    + ); + } else { + return ( +
    +
      + {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 (
    • -
    - ) + ); } } 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 {
      - {upCards.map(card => { + {upCards.map((card) => { const [rankClass, suitClass, rankText, suitText] = translateCardData(card); return (
    • @@ -108,7 +103,7 @@ class DoudizhuGameBoard extends React.Component {
        - {downCards.map(card => { + {downCards.map((card) => { const [rankClass, suitClass, rankText, suitText] = translateCardData(card); return (
      • @@ -123,41 +118,69 @@ class DoudizhuGameBoard extends React.Component {
    - ) + ); } - 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 ( -
    -
    -
    {millisecond2Second(this.props.considerationTime)}
    +
    +
    +
    {millisecond2Second(this.props.considerationTime)}
    - - - + + +
    - ) + ); } else { return ( -
    +
    {millisecond2Second(this.props.considerationTime)}
    - ) + ); } - }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 (
    -
    +
    -
    - {this.computePlayerPortrait(leftId, leftIdx)} -
    - {leftIdx >= 0 ? this.computeSideHand(this.props.hands[leftIdx]) :
    Waiting...
    } -
    -
    - {leftIdx >= 0 ? this.playerDecisionArea(leftIdx) : ""} +
    {this.computePlayerPortrait(leftId, leftIdx)}
    + {leftIdx >= 0 ? ( + this.computeSideHand(this.props.hands[leftIdx]) + ) : ( +
    + Waiting... +
    + )}
    +
    {leftIdx >= 0 ? this.playerDecisionArea(leftIdx) : ''}
    -
    +
    -
    - {this.computePlayerPortrait(rightId, rightIdx)} -
    - {rightIdx >= 0 ? this.computeSideHand(this.props.hands[rightIdx]) :
    Waiting...
    } -
    -
    - {rightIdx >= 0 ? this.playerDecisionArea(rightIdx) : ""} +
    {this.computePlayerPortrait(rightId, rightIdx)}
    + {rightIdx >= 0 ? ( + this.computeSideHand(this.props.hands[rightIdx]) + ) : ( +
    + Waiting... +
    + )}
    +
    {rightIdx >= 0 ? this.playerDecisionArea(rightIdx) : ''}
    -
    -
    - {bottomIdx >= 0 ? this.playerDecisionArea(bottomIdx) : ""} -
    +
    +
    {bottomIdx >= 0 ? this.playerDecisionArea(bottomIdx) : ''}
    -
    - {this.computePlayerPortrait(bottomId, bottomIdx)} -
    - {bottomIdx >= 0 ?
    {this.computeSingleLineHand(this.props.hands[bottomIdx], '', true)}
    :
    Waiting...
    } +
    {this.computePlayerPortrait(bottomId, bottomIdx)}
    + {bottomIdx >= 0 ? ( +
    + {this.computeSingleLineHand(this.props.hands[bottomIdx], '', true)} +
    + ) : ( +
    + Waiting... +
    + )}
    diff --git a/src/view/PvEView/PvEDoudizhuDemoView.js b/src/view/PvEView/PvEDoudizhuDemoView.js index 597aa18..98cefbe 100644 --- a/src/view/PvEView/PvEDoudizhuDemoView.js +++ b/src/view/PvEView/PvEDoudizhuDemoView.js @@ -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() {
    Date: Thu, 22 Apr 2021 21:55:49 -0700 Subject: [PATCH 13/14] generate random initial hands & three landlord cards --- src/utils/index.js | 11 +++++++++ src/view/PvEView/PvEDoudizhuDemoView.js | 33 +++++++++++++++++-------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/utils/index.js b/src/utils/index.js index 90ecb6b..8494cc4 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -239,3 +239,14 @@ export function isDoudizhuBomb(cards) { 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; +} diff --git a/src/view/PvEView/PvEDoudizhuDemoView.js b/src/view/PvEView/PvEDoudizhuDemoView.js index 98cefbe..bbc91b3 100644 --- a/src/view/PvEView/PvEDoudizhuDemoView.js +++ b/src/view/PvEView/PvEDoudizhuDemoView.js @@ -4,14 +4,19 @@ 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, isDoudizhuBomb, sortDoudizhuCards } from '../../utils'; +import { + card2SuiteAndRank, + deepCopy, + fullDoudizhuDeck, + isDoudizhuBomb, + shuffleArray, + sortDoudizhuCards, +} from '../../utils'; import { douzeroDemoUrl } from '../../utils/config'; -const initHands = [ - 'S2 H2 HK DK HQ CQ DQ CJ S9 H9 D9 C7 S6 H6 C4 D4 S3', - 'C2 HA CA DA SQ ST HT D8 S7 H7 C6 D6 S5 H5 C5 S4 H4', - 'RJ BJ D2 SA SK CK SJ HJ DJ CT DT C9 S8 H8 C8 D7 D5 H3 S3 D3', -]; +const shuffledDoudizhuDeck = shuffleArray(fullDoudizhuDeck.slice()); + +const threeLandlordCards = shuffleArray(sortDoudizhuCards(shuffledDoudizhuDeck.slice(0, 3))); const initConsiderationTime = 30000; const considerationTimeDeduction = 1000; @@ -37,7 +42,15 @@ const playerInfo = [ douzeroPlayerPosition: 0, }, ]; -const threeLandlordCards = ['RJ', 'BJ', 'D2']; + +let initHands = [ + shuffledDoudizhuDeck.slice(3, 20), + shuffledDoudizhuDeck.slice(20, 37), + shuffledDoudizhuDeck.slice(37, 54), +]; +console.log(initHands); +const landlordIdx = playerInfo.find((player) => player.role === 'landlord').index; +initHands[landlordIdx] = initHands[landlordIdx].concat(threeLandlordCards.slice()); let gameStateTimeout = null; @@ -154,7 +167,7 @@ function PvEDoudizhuDemoView() { } // update value records for douzero - const newHistoryRecord = newLatestAction === 'pass' ? [] : newLatestAction; + const newHistoryRecord = sortDoudizhuCards(newLatestAction === 'pass' ? [] : newLatestAction, true); switch (playerInfo[gameState.currentPlayer].douzeroPlayerPosition) { case 0: lastMoveLandlord = newHistoryRecord; @@ -253,7 +266,7 @@ function PvEDoudizhuDemoView() { rival_move = cardArr2DouzeroFormat( sortDoudizhuCards(gameHistory[gameHistory.length - 1], true), ); - } else if (gameHistory.length > 2 && gameHistory[gameHistory.length - 2].length > 0) { + } else if (gameHistory.length >= 2 && gameHistory[gameHistory.length - 2].length > 0) { rival_move = cardArr2DouzeroFormat( sortDoudizhuCards(gameHistory[gameHistory.length - 2], true), ); @@ -340,7 +353,7 @@ function PvEDoudizhuDemoView() { 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(cardStr2Arr(element))); + newGameState.hands = initHands.map((element) => sortDoudizhuCards(element)); setGameState(newGameState); gameStateTimer(); }, []); From 2b0a2957b084419f9f74801e8c80c20faef18bfd Mon Sep 17 00:00:00 2001 From: Songyi Huang Date: Thu, 22 Apr 2021 21:59:00 -0700 Subject: [PATCH 14/14] add init state log for later debug --- src/view/PvEView/PvEDoudizhuDemoView.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/view/PvEView/PvEDoudizhuDemoView.js b/src/view/PvEView/PvEDoudizhuDemoView.js index bbc91b3..f5d7b14 100644 --- a/src/view/PvEView/PvEDoudizhuDemoView.js +++ b/src/view/PvEView/PvEDoudizhuDemoView.js @@ -48,7 +48,9 @@ let initHands = [ shuffledDoudizhuDeck.slice(20, 37), shuffledDoudizhuDeck.slice(37, 54), ]; -console.log(initHands); +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()); @@ -113,7 +115,6 @@ function PvEDoudizhuDemoView() { 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, @@ -252,7 +253,6 @@ function PvEDoudizhuDemoView() { try { const apiRes = await axios.post(`${douzeroDemoUrl}/predict`, qs.stringify(requestBody)); - console.log(apiRes.data); const data = apiRes.data; if (data.status !== 0) { @@ -276,7 +276,6 @@ function PvEDoudizhuDemoView() { 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({ @@ -286,9 +285,7 @@ function PvEDoudizhuDemoView() { }); } } - console.log(data.status, data.message); } else { - console.log('api res', data, gameStateTimeout); let bestAction = ''; if (data.result && Object.keys(data.result).length > 0) { if (Object.keys(data.result).length === 1) bestAction = Object.keys(data.result)[0];