From db813574ae4a07ed7d9d8f29f5df76a0790afa79 Mon Sep 17 00:00:00 2001 From: songyih Date: Thu, 30 Jan 2020 22:14:06 -0800 Subject: [PATCH] implemented basic game state loop for leduc holdem --- package.json | 8 +- public/config.js | 4 + public/index.html | 1 + server/sample_data/sample_leduc_holdem.json | 2 +- .../GameBoard/LeducHoldemGameBoard.js | 22 +-- src/index.js | 8 - src/serviceWorker.js | 137 ----------------- src/setupTests.js | 5 - src/utils/index.js | 7 +- src/view/DoudizhuGameView.js | 21 +-- src/view/LeducHoldemGameView.js | 139 ++++++++++++++++-- 11 files changed, 163 insertions(+), 191 deletions(-) create mode 100644 public/config.js delete mode 100644 src/serviceWorker.js delete mode 100644 src/setupTests.js diff --git a/package.json b/package.json index 51e6dfd..17e7f4d 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,12 @@ "private": true, "dependencies": { "@material-ui/core": "^4.9.0", - "@testing-library/jest-dom": "^4.2.4", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^7.1.2", "element-react": "^1.4.34", "element-theme-default": "^1.4.13", "node-sass": "^4.13.0", "react": "^16.12.0", "react-dom": "^16.12.0", - "react-hot-loader": "^4.12.18", + "react-hot-loader": "^4.12.19", "react-router-dom": "^5.1.2", "react-scripts": "3.3.0", "socket.io-client": "^2.3.0" @@ -37,5 +34,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "axios": "^0.19.2" } } diff --git a/public/config.js b/public/config.js new file mode 100644 index 0000000..19f3b6e --- /dev/null +++ b/public/config.js @@ -0,0 +1,4 @@ +window.g = { + apiUrl: 'http://localhost:10080', // 配置服务器地址 + // WebSocketUrl: '/api/' // 配置WebSocket地址 +}; \ No newline at end of file diff --git a/public/index.html b/public/index.html index d551564..7f8de48 100644 --- a/public/index.html +++ b/public/index.html @@ -24,6 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> + RLcard Showdown diff --git a/server/sample_data/sample_leduc_holdem.json b/server/sample_data/sample_leduc_holdem.json index 515b1b0..490c222 100644 --- a/server/sample_data/sample_leduc_holdem.json +++ b/server/sample_data/sample_leduc_holdem.json @@ -13,6 +13,7 @@ "index": 1 } ], + "publicCard": "SJ", "moveHistory": [ [ { @@ -32,7 +33,6 @@ "move": "Check" } ], - "SJ", [ { "playerIdx": 0, diff --git a/src/components/GameBoard/LeducHoldemGameBoard.js b/src/components/GameBoard/LeducHoldemGameBoard.js index d0db72e..6da64f6 100644 --- a/src/components/GameBoard/LeducHoldemGameBoard.js +++ b/src/components/GameBoard/LeducHoldemGameBoard.js @@ -9,17 +9,17 @@ class LeducHoldemGameBoard extends React.Component { render() { return (
-
-
- played card area -
-
-
- {`Player Id ${bottomId}\n${this.props.playerInfo.length > 0 ? this.props.playerInfo[bottomIdx].role : ""}`} -
- {bottomIdx >= 0 ?
{this.computeSingleLineHand(this.props.hands[bottomIdx])}
:
Waiting...
} -
-
+ {/*
*/} + {/*
*/} + {/* played card area*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/* {`Player Id ${bottomId}\n${this.props.playerInfo.length > 0 ? this.props.playerInfo[bottomIdx].role : ""}`}*/} + {/*
*/} + {/* {bottomIdx >= 0 ?
{this.computeSingleLineHand(this.props.hands[bottomIdx])}
:
Waiting...
}*/} + {/*
*/} + {/*
*/}
); } diff --git a/src/index.js b/src/index.js index db0e237..24ee011 100644 --- a/src/index.js +++ b/src/index.js @@ -4,14 +4,6 @@ import './assets/index.css'; import App from './App'; // import element ui - import 'element-theme-default'; -import * as serviceWorker from './serviceWorker'; - ReactDOM.render(, document.getElementById('root')); - -// If you want your app to work offline and load faster, you can change -// unregister() to register() below. Note this comes with some pitfalls. -// Learn more about service workers: https://bit.ly/CRA-PWA -serviceWorker.unregister(); diff --git a/src/serviceWorker.js b/src/serviceWorker.js deleted file mode 100644 index 8703ddb..0000000 --- a/src/serviceWorker.js +++ /dev/null @@ -1,137 +0,0 @@ -// This optional code is used to register a service worker. -// register() is not called by default. - -// This lets the app load faster on subsequent visits in production, and gives -// it offline capabilities. However, it also means that developers (and users) -// will only see deployed updates on subsequent visits to a page, after all the -// existing tabs open on the page have been closed, since previously cached -// resources are updated in the background. - -// To learn more about the benefits of this model and instructions on how to -// opt-in, read https://bit.ly/CRA-PWA - -const isLocalhost = Boolean( - window.location.hostname === 'localhost' || - // [::1] is the IPv6 localhost address. - window.location.hostname === '[::1]' || - // 127.0.0.0/8 are considered localhost for IPv4. - window.location.hostname.match( - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ - ) -); - -export function register(config) { - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { - // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); - if (publicUrl.origin !== window.location.origin) { - // Our service worker won't work if PUBLIC_URL is on a different origin - // from what our page is served on. This might happen if a CDN is used to - // serve assets; see https://github.com/facebook/create-react-app/issues/2374 - return; - } - - window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; - - if (isLocalhost) { - // This is running on localhost. Let's check if a service worker still exists or not. - checkValidServiceWorker(swUrl, config); - - // Add some additional logging to localhost, pointing developers to the - // service worker/PWA documentation. - navigator.serviceWorker.ready.then(() => { - console.log( - 'This web app is being served cache-first by a service ' + - 'worker. To learn more, visit https://bit.ly/CRA-PWA' - ); - }); - } else { - // Is not localhost. Just register service worker - registerValidSW(swUrl, config); - } - }); - } -} - -function registerValidSW(swUrl, config) { - navigator.serviceWorker - .register(swUrl) - .then(registration => { - registration.onupdatefound = () => { - const installingWorker = registration.installing; - if (installingWorker == null) { - return; - } - installingWorker.onstatechange = () => { - if (installingWorker.state === 'installed') { - if (navigator.serviceWorker.controller) { - // At this point, the updated precached content has been fetched, - // but the previous service worker will still serve the older - // content until all client tabs are closed. - console.log( - 'New content is available and will be used when all ' + - 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' - ); - - // Execute callback - if (config && config.onUpdate) { - config.onUpdate(registration); - } - } else { - // At this point, everything has been precached. - // It's the perfect time to display a - // "Content is cached for offline use." message. - console.log('Content is cached for offline use.'); - - // Execute callback - if (config && config.onSuccess) { - config.onSuccess(registration); - } - } - } - }; - }; - }) - .catch(error => { - console.error('Error during service worker registration:', error); - }); -} - -function checkValidServiceWorker(swUrl, config) { - // Check if the service worker can be found. If it can't reload the page. - fetch(swUrl, { - headers: { 'Service-Worker': 'script' } - }) - .then(response => { - // Ensure service worker exists, and that we really are getting a JS file. - const contentType = response.headers.get('content-type'); - if ( - response.status === 404 || - (contentType != null && contentType.indexOf('javascript') === -1) - ) { - // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then(registration => { - registration.unregister().then(() => { - window.location.reload(); - }); - }); - } else { - // Service worker found. Proceed as normal. - registerValidSW(swUrl, config); - } - }) - .catch(() => { - console.log( - 'No internet connection found. App is running in offline mode.' - ); - }); -} - -export function unregister() { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready.then(registration => { - registration.unregister(); - }); - } -} diff --git a/src/setupTests.js b/src/setupTests.js deleted file mode 100644 index 74b1a27..0000000 --- a/src/setupTests.js +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom/extend-expect'; diff --git a/src/utils/index.js b/src/utils/index.js index c45d893..43cc03a 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,5 +1,5 @@ export function removeCards(cards, hands){ // remove cards from hands, return the remained hands - let remainedHands = JSON.parse(JSON.stringify(hands)); + let remainedHands = deepCopy(hands); // if the player's action is pass then return the copy of original hands if(cards === "P"){ return remainedHands; @@ -19,10 +19,13 @@ export function removeCards(cards, hands){ // remove cards from hands, retur return remainedHands; } -export function doubleRaf (callback) { +export function doubleRaf(callback){ // secure all the animation got rendered before callback function gets executed requestAnimationFrame(() => { requestAnimationFrame(callback) }) } +export function deepCopy(toCopy){ + return JSON.parse(JSON.stringify(toCopy)); +} diff --git a/src/view/DoudizhuGameView.js b/src/view/DoudizhuGameView.js index 6a7146b..4287a73 100644 --- a/src/view/DoudizhuGameView.js +++ b/src/view/DoudizhuGameView.js @@ -2,9 +2,9 @@ import React from 'react'; import '../assets/gameview.scss'; import { DoudizhuGameBoard } from '../components/GameBoard'; import webSocket from "socket.io-client"; -import {removeCards, doubleRaf} from "../utils"; +import { removeCards, doubleRaf, deepCopy } from "../utils"; -import { Button, Layout, Slider as elSlider } from 'element-react'; +import { Button, Layout } from 'element-react'; import Slider from '@material-ui/core/Slider'; class DoudizhuGameView extends React.Component { @@ -30,7 +30,7 @@ class DoudizhuGameView extends React.Component { ws: null, gameInfo: this.initGameState, gameStateLoop: null, - considerationTime: this.initConsiderationTime + considerationTimeSetting: this.initConsiderationTime }; } @@ -39,7 +39,7 @@ class DoudizhuGameView extends React.Component { let currentConsiderationTime = this.state.gameInfo.considerationTime; if(currentConsiderationTime > 0) { currentConsiderationTime -= this.considerationTimeDeduction; - let gameInfo = JSON.parse(JSON.stringify(this.state.gameInfo)); + let gameInfo = deepCopy(this.state.gameInfo); gameInfo.considerationTime = currentConsiderationTime; this.setState({gameInfo: gameInfo}); this.gameStateTimer(); @@ -49,7 +49,7 @@ class DoudizhuGameView extends React.Component { type: 1, message: {turn: turn} }; - let gameInfo = JSON.parse(JSON.stringify(this.state.gameInfo)); + let gameInfo = deepCopy(this.state.gameInfo); this.setState({gameInfo: gameInfo}); this.state.ws.emit("getMessage", gameStateReq); } @@ -80,7 +80,7 @@ class DoudizhuGameView extends React.Component { switch(message.type){ case 0: // init replay info - let gameInfo = JSON.parse(JSON.stringify(this.state.gameInfo)); + let gameInfo = deepCopy(this.state.gameInfo); gameInfo.playerInfo = message.message.playerInfo; gameInfo.hands = message.message.initHands.map(element => { return element.split(" "); @@ -93,7 +93,7 @@ class DoudizhuGameView extends React.Component { // getting player actions let res = message.message; if(res.turn === this.state.gameInfo.turn && res.playerIdx === this.state.gameInfo.currentPlayer){ - let gameInfo = JSON.parse(JSON.stringify(this.state.gameInfo)); + let gameInfo = deepCopy(this.state.gameInfo); gameInfo.latestAction[res.playerIdx] = res.move === "P" ? "P" : res.move.split(" "); gameInfo.turn++; gameInfo.currentPlayer = (gameInfo.currentPlayer+1)%3; @@ -104,7 +104,7 @@ class DoudizhuGameView extends React.Component { }else{ console.log("Cannot find cards in move from player's hand"); } - gameInfo.considerationTime = this.state.considerationTime; + gameInfo.considerationTime = this.state.considerationTimeSetting; this.setState({gameInfo: gameInfo}); }else{ console.log("Mismatched game turn or current player index", message); @@ -140,6 +140,7 @@ class DoudizhuGameView extends React.Component { } render(){ + // todo: reset game state timer when considerationTimeSetting changes return (
@@ -169,8 +170,8 @@ class DoudizhuGameView extends React.Component { {console.log('slider val', newVal);this.setState({considerationTime: newVal})}} + value={this.state.considerationTimeSetting} + onChange={(e, newVal)=>{console.log('slider val', newVal);this.setState({considerationTimeSetting: newVal})}} aria-labelledby="discrete-slider" valueLabelDisplay="auto" step={1000} diff --git a/src/view/LeducHoldemGameView.js b/src/view/LeducHoldemGameView.js index dbcc98b..fd0ed0f 100644 --- a/src/view/LeducHoldemGameView.js +++ b/src/view/LeducHoldemGameView.js @@ -1,7 +1,8 @@ import React from 'react'; +import axios from 'axios'; import '../assets/gameview.scss'; -import {DoudizhuGameBoard, LeducHoldemGameBoard} from '../components/GameBoard'; -import {removeCards, doubleRaf} from "../utils"; +import {LeducHoldemGameBoard} from '../components/GameBoard'; +import {doubleRaf, deepCopy} from "../utils"; import { Button, Layout, Slider as elSlider } from 'element-react'; import Slider from '@material-ui/core/Slider'; @@ -9,21 +10,133 @@ import Slider from '@material-ui/core/Slider'; class LeducHoldemGameView extends React.Component { constructor(props) { super(props); + const mainViewerId = 0; // Id of the player at the bottom of screen + this.initConsiderationTime = 1000; + this.considerationTimeDeduction = 100; + this.gameStateTimeout = null; + this.apiUrl = window.g.apiUrl; + this.moveHistory = []; + this.initGameState = { + playerInfo: [], + hands: [], + latestAction: ["", ""], + mainViewerId: mainViewerId, + round: 0, + turn: 0, + pot: [1, 1], + publicCard: "", + currentPlayer: null, + considerationTime: this.initConsiderationTime + }; + this.state = { + gameInfo: this.initGameState, + gameStateLoop: null, + considerationTimeSetting: this.initConsiderationTime + } + } + + retrieveReplayData(){ + // for test use + const replayId = 0; + + axios.get(`${this.apiUrl}/replay/leduc_holdem/${replayId}`) + .then(res => { + res = res.data; + this.moveHistory = res.moveHistory; + let gameInfo = deepCopy(this.state.gameInfo); + gameInfo.hands = res.initHands; + gameInfo.playerInfo = res.playerInfo; + gameInfo.currentPlayer = res.moveHistory[0][0].playerIdx; + gameInfo.publicCard = res.publicCard; + this.setState({gameInfo: gameInfo}, ()=>{ + this.startReplay(); + }); + }) + .catch(err=>{ + console.log("err", err); + }) + } + + startReplay(){ + if(this.gameStateTimeout){ + window.clearTimeout(this.gameStateTimeout); + this.gameStateTimeout = null; + } + // loop to update game state + this.gameStateTimer(); + } + + gameStateTimer(){ + this.gameStateTimeout = setTimeout(()=>{ + let currentConsiderationTime = this.state.gameInfo.considerationTime; + if(currentConsiderationTime > 0) { + currentConsiderationTime -= this.considerationTimeDeduction; + let gameInfo = deepCopy(this.state.gameInfo); + gameInfo.considerationTime = currentConsiderationTime; + this.setState({gameInfo: gameInfo}); + this.gameStateTimer(); + }else{ + console.log(this.state.gameInfo.latestAction); + const turn = this.state.gameInfo.turn; + if(turn >= this.moveHistory[this.state.gameInfo.round].length){ + if(this.state.gameInfo.round === 0){ + // todo: if it's the first round, then reveal the public card and start the second round + let gameInfo = deepCopy(this.state.gameInfo); + gameInfo.turn = 0; + gameInfo.round = 1; + gameInfo.latestAction = ["", ""]; + gameInfo.currentPlayer = this.moveHistory[1][0].playerIdx; + gameInfo.considerationTime = this.state.considerationTimeSetting; + this.setState({gameInfo: gameInfo}, ()=>{ + this.gameStateTimer(); + }); + }else{ + // todo: if it's the second round, game ends + console.log("game ends"); + } + }else{ + let gameInfo = deepCopy(this.state.gameInfo); + // debugger; + if(gameInfo.currentPlayer === this.moveHistory[gameInfo.round][gameInfo.turn].playerIdx){ + gameInfo.latestAction[gameInfo.currentPlayer] = this.moveHistory[gameInfo.round][gameInfo.turn].move; + // todo: check if the player choose to fold in this turn + + gameInfo.turn++; + gameInfo.currentPlayer = (gameInfo.currentPlayer+1)%2; + gameInfo.considerationTime = this.state.considerationTimeSetting; + this.setState({gameInfo: gameInfo}, ()=>{ + this.gameStateTimer(); + }); + }else{ + console.log("Mismatch in current player & move history"); + } + } + } + }, this.considerationTimeDeduction); } render(){ return ( -
- this.runNewTurn(prevTurn)} - /> +
+
+ {/*this.runNewTurn(prevTurn)}*/} + {/*/>*/} +
+
+ + + + + +
); }