├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── docs ├── README.md ├── guide.md ├── imgs │ ├── doudizhu-replay.png │ ├── leaderboards.png │ ├── leduc-replay.png │ └── upload.png └── leaderboard_api.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── pve_server ├── .gitignore ├── deep.py ├── models.py ├── pretrained │ └── put_pretrained_models_here ├── run_dmc.py ├── run_douzero.py └── utils │ ├── move_detector.py │ ├── move_generator.py │ ├── move_selector.py │ └── utils.py ├── requirements.txt ├── server ├── manage.py ├── media │ └── example_agents │ │ └── leduc_holdem_dqn.zip ├── server │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── tournament │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── rlcard_wrap │ ├── __init__.py │ ├── doudizhu_random_model.py │ └── leduc_holdem_random_model.py │ ├── tests.py │ ├── tournament.py │ ├── urls.py │ └── views.py ├── src ├── App.js ├── assets │ ├── cards.css │ ├── doudizhu.scss │ ├── faces │ │ ├── JC.gif │ │ ├── JD.gif │ │ ├── JH.gif │ │ ├── JS.gif │ │ ├── KC.gif │ │ ├── KD.gif │ │ ├── KH.gif │ │ ├── KS.gif │ │ ├── QC.gif │ │ ├── QD.gif │ │ ├── QH.gif │ │ ├── QS.gif │ │ ├── README │ │ ├── custom │ │ │ ├── Poker_C10.png │ │ │ ├── Poker_C2.png │ │ │ ├── Poker_C3.png │ │ │ ├── Poker_C4.png │ │ │ ├── Poker_C5.png │ │ │ ├── Poker_C6.png │ │ │ ├── Poker_C7.png │ │ │ ├── Poker_C8.png │ │ │ ├── Poker_C9.png │ │ │ ├── Poker_CA.png │ │ │ ├── Poker_CJ.png │ │ │ ├── Poker_CK.png │ │ │ ├── Poker_CQ.png │ │ │ ├── Poker_D10.png │ │ │ ├── Poker_D2.png │ │ │ ├── Poker_D3.png │ │ │ ├── Poker_D4.png │ │ │ ├── Poker_D5.png │ │ │ ├── Poker_D6.png │ │ │ ├── Poker_D7.png │ │ │ ├── Poker_D8.png │ │ │ ├── Poker_D9.png │ │ │ ├── Poker_DA.png │ │ │ ├── Poker_DJ.png │ │ │ ├── Poker_DK.png │ │ │ ├── Poker_DQ.png │ │ │ ├── Poker_H10.png │ │ │ ├── Poker_H2.png │ │ │ ├── Poker_H3.png │ │ │ ├── Poker_H4.png │ │ │ ├── Poker_H5.png │ │ │ ├── Poker_H6.png │ │ │ ├── Poker_H7.png │ │ │ ├── Poker_H8.png │ │ │ ├── Poker_H9.png │ │ │ ├── Poker_HA.png │ │ │ ├── Poker_HJ.png │ │ │ ├── Poker_HK.png │ │ │ ├── Poker_HQ.png │ │ │ ├── Poker_Joker_B.png │ │ │ ├── Poker_Joker_R.png │ │ │ ├── Poker_S10.png │ │ │ ├── Poker_S2.png │ │ │ ├── Poker_S3.png │ │ │ ├── Poker_S4.png │ │ │ ├── Poker_S5.png │ │ │ ├── Poker_S6.png │ │ │ ├── Poker_S7.png │ │ │ ├── Poker_S8.png │ │ │ ├── Poker_S9.png │ │ │ ├── Poker_SA.png │ │ │ ├── Poker_SJ.png │ │ │ ├── Poker_SK.png │ │ │ └── Poker_SQ.png │ │ ├── joker.gif │ │ └── pokerback.png │ ├── gameview.scss │ ├── images │ │ ├── Actions │ │ │ ├── call.png │ │ │ ├── call_u.png │ │ │ ├── check.png │ │ │ ├── check_u.png │ │ │ ├── fold.png │ │ │ ├── fold_u.png │ │ │ ├── raise.png │ │ │ └── raise_u.png │ │ ├── Portrait │ │ │ ├── Landlord.png │ │ │ ├── Landlord_wName.png │ │ │ ├── Peasant_wName.png │ │ │ ├── Player.png │ │ │ ├── Player1.png │ │ │ ├── Player2.png │ │ │ ├── Player3.png │ │ │ └── Pleasant.png │ │ ├── Tokens │ │ │ ├── Token_1.png │ │ │ ├── Token_10.png │ │ │ ├── Token_100.png │ │ │ ├── Token_25.png │ │ │ ├── Token_5.png │ │ │ ├── Token_Black_100.png │ │ │ ├── Token_Blue_10.png │ │ │ ├── Token_Green_25.png │ │ │ ├── Token_Red_5.png │ │ │ └── Token_White_1.png │ │ ├── gameboard.png │ │ ├── logo_white.png │ │ ├── table.png │ │ └── timer.png │ ├── index.scss │ └── leducholdem.scss ├── components │ ├── GameBoard │ │ ├── DoudizhuGameBoard.js │ │ ├── LeducHoldemGameBoard.js │ │ └── index.js │ ├── MenuBar.js │ └── Navbar.js ├── i18n.js ├── index.js ├── locales │ ├── en │ │ └── translation.json │ └── zh │ │ └── translation.json ├── utils │ ├── config.js │ └── index.js └── view │ ├── LeaderBoard.js │ ├── PvEView │ ├── PvEDoudizhuDemoView.js │ └── index.js │ └── ReplayView │ ├── DoudizhuReplayView.js │ ├── LeducHoldemReplayView.js │ └── index.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended", 5 | "plugin:react-hooks/recommended", 6 | "prettier", 7 | "prettier/react" 8 | ], 9 | "rules": { 10 | "no-redeclare": "off", 11 | "react/display-name": "off", 12 | "react/prop-types": "off", 13 | "react/react-in-jsx-scope": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | db.sqlite3 26 | __pycache__ 27 | *.swp 28 | uploaded_agents 29 | 30 | # idea 31 | /.idea 32 | 33 | package-lock.json 34 | 35 | douzero_pretrained 36 | dmc_pretrained 37 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | dist/ 3 | node_modules/ 4 | src/public/lib/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 120, 5 | "tabWidth": 4 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RLCard Showdown 2 | This is the GUI support for the [RLCard](https://github.com/datamllab/rlcard) project and [DouZero](https://github.com/kwai/DouZero) project. RLCard-Showdown provides evaluation and visualization tools to help understand the performance of the agents. It includes a replay module, where you can analyze the replays, and a PvE module, where you can play with the AI interactively. Currently, we only support Leduc Hold'em and Dou Dizhu. The frontend is developed with [React](https://reactjs.org/). The backend is based on [Django](https://www.djangoproject.com/) and [Flask](https://flask.palletsprojects.com/). Have fun! 3 | 4 | * Official Website: [http://www.rlcard.org](http://www.rlcard.org) 5 | * Tutorial in Jupyter Notebook: [https://github.com/datamllab/rlcard-tutorial](https://github.com/datamllab/rlcard-tutorial) 6 | * Paper: [https://www.ijcai.org/Proceedings/2020/764](https://www.ijcai.org/Proceedings/2020/764) 7 | * Document: [Click Here](docs/README.md) 8 | * Online Demo with DouZero: [https://www.douzero.org/](https://www.douzero.org/) 9 | * Miscellaneous Resources: Have you heard of data-centric AI? Please check out our [data-centric AI survey](https://arxiv.org/abs/2303.10158) and [awesome data-centric AI resources](https://github.com/daochenzha/data-centric-AI)! 10 | 11 | ## Cite this work 12 | Zha, Daochen, et al. "RLCard: A Platform for Reinforcement Learning in Card Games." IJCAI. 2020. 13 | ```bibtex 14 | @inproceedings{zha2020rlcard, 15 | title={RLCard: A Platform for Reinforcement Learning in Card Games}, 16 | author={Zha, Daochen and Lai, Kwei-Herng and Huang, Songyi and Cao, Yuanpu and Reddy, Keerthana and Vargas, Juan and Nguyen, Alex and Wei, Ruzhe and Guo, Junyu and Hu, Xia}, 17 | booktitle={IJCAI}, 18 | year={2020} 19 | } 20 | ``` 21 | 22 | ## Installation 23 | RLCard-Showdown has separated frontend and backend. The frontend is built with React and the backend is based on Django and Flask. 24 | 25 | ### Prerequisite 26 | To set up the frontend, you should make sure you have [Node.js](https://nodejs.org/) and NPM installed. Normally you just need to manually install Node.js, and the NPM package would be automatically installed together with Node.js for you. Please refer to its official website for installation of Node.js. 27 | 28 | You can run the following commands to verify the installation 29 | ``` 30 | node -v 31 | npm -v 32 | ``` 33 | For backend, make sure that you have **Python 3.6+** and **pip** installed. 34 | 35 | ### Install Frontend and Backend 36 | The frontend can be installed with the help of NPM: 37 | ``` 38 | git clone -b master --single-branch --depth=1 https://github.com/datamllab/rlcard-showdown.git 39 | cd rlcard-showdown 40 | npm install 41 | ``` 42 | The backend of leaderboard can be installed with 43 | ``` 44 | pip3 install -r requirements.txt 45 | cd server 46 | python3 manage.py migrate 47 | cd .. 48 | ``` 49 | 50 | ### Run RLCard-Showdown 51 | 1. Launch the backend of leaderboard with 52 | ``` 53 | cd server 54 | python3 manage.py runserver 55 | ``` 56 | 2. Download the pre-trained models in [Google Drive](https://drive.google.com/file/d/1zx-20xNBDbCFd8GWhZFUkl07lofbNHpy/view?usp=sharing) or [百度网盘](https://pan.baidu.com/s/12MgxVBBz4mgitT74quSWfw) 提取码: qh6s. Extract it in `pve_server/pretrained`. 57 | 58 | In a new terminal, start the PvE server (i.e., human vs AI) of DouZero with 59 | ``` 60 | cd pve_server 61 | python3 run_douzero.py 62 | ``` 63 | Alternatively, you can start the PvE server interfaced with RLCard: 64 | ``` 65 | cd pve_server 66 | python3 run_dmc.py 67 | ``` 68 | They are conceptually the same with minor differences in state representation and training time of the pre-trained models (DouZero is fully trained with more than a month, while DMC in RLCard is only trained for hours). 69 | 70 | 3. Run the following command in another new terminal under the project folder to start frontend: 71 | ``` 72 | npm start 73 | ``` 74 | You can view leaderboard at [http://127.0.0.1:3000/](http://127.0.0.1:3000/) and PvE demo of Dou Dizhu at [http://127.0.0.1:3000/pve/doudizhu-demo](http://127.0.0.1:3000/pve/doudizhu-demo). The backend of leaderboard will run in [http://127.0.0.1:8000/](http://127.0.0.1:8000/). The PvE backend will run in [http://127.0.0.1:5000/](http://127.0.0.1:5000/). 75 | 76 | ## Demos 77 | ![leaderboards](https://github.com/datamllab/rlcard-showdown/blob/master/docs/imgs/leaderboards.png?raw=true) 78 | ![upload](https://github.com/datamllab/rlcard-showdown/blob/master/docs/imgs/upload.png?raw=true) 79 | ![doudizhu-replay](https://github.com/datamllab/rlcard-showdown/blob/master/docs/imgs/doudizhu-replay.png?raw=true) 80 | ![leduc-replay](https://github.com/datamllab/rlcard-showdown/blob/master/docs/imgs/leduc-replay.png?raw=true) 81 | 82 | ## Contact Us 83 | If you have any questions or feedback, feel free to drop an email to [Songyi Huang](https://github.com/hsywhu) for the frontend or [Daochen Zha](https://github.com/daochenzha) for backend. 84 | 85 | ## Acknowledgements 86 | We would like to thank JJ World Network Technology Co., LTD for the generous support, [Chieh-An Tsai](https://anntsai.myportfolio.com/) for user interface design, and [Lei Pan](https://github.com/lpan18) for the help in visualizations. 87 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Document of RLCard-Showdown 2 | 3 | * [User Guide](guide.md) 4 | * [API Document of Leaderboard](leaderboard_api.md) 5 | -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | # User Guide 2 | ## Launching Tournaments 3 | 1. Select the game in the left menu. 4 | 2. Click "LAUNCH TOURNAMENT" in the top-right corner to let the agents play against each other 5 | 6 | ## Watch the Game Replay 7 | 1. Select the agent in the left menu. All the games for this agent will be shown on the page. 8 | 2. Click the replay button to watch the replay. You can adjust the replay speed in the control panel. 9 | 10 | ## Upload Your Agent 11 | 1. Click "UPLOAD MODEL" in the top-left corner. 12 | 2. Upload your model following the provided template 13 | 3. Click "LAUNCH TOURNAMENT" to evaluate your agent against existing baseline models. 14 | -------------------------------------------------------------------------------- /docs/imgs/doudizhu-replay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/docs/imgs/doudizhu-replay.png -------------------------------------------------------------------------------- /docs/imgs/leaderboards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/docs/imgs/leaderboards.png -------------------------------------------------------------------------------- /docs/imgs/leduc-replay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/docs/imgs/leduc-replay.png -------------------------------------------------------------------------------- /docs/imgs/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/docs/imgs/upload.png -------------------------------------------------------------------------------- /docs/leaderboard_api.md: -------------------------------------------------------------------------------- 1 | # API Documentation 2 | 3 | ## Changing the Configuration of Frontend 4 | 5 | The default configuration for backend server URL is [http://127.0.0.1:8000/](http://127.0.0.1:8000/), you can also change it in `/src/utils/config.js`. 6 | 7 | You will see blank tables on the frontend if you haven't started the backend Django server, or the backend server URL is configured incorrectly. 8 | 9 | ## REST API of Backend 10 | The definitions of the fields are as follows: 11 | * `eval_num`: Integer. The number of evaluation times. 12 | * `name`: String. The name of the environment. 13 | * `agent0`: String. Model name. 14 | * `agent1`: String. Model name. 15 | * `win`: Boolean. True if model in the first seat wins. 16 | * `payoff`: Float. The payoff of the agent in the first seat. 17 | * `index`: Integer. The index of the game of the same environent and same agent. It is in the range \[0, eval_num-1\] 18 | 19 | | type | Resource | Parameters | Description | 20 | |------|-------------------------------------|-------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| 21 | | GET | tournament/launch | `num_eval_games`, `name` | Launch tournment on the game. Each pair of models will play `num_eval_games` times. Results will be saved in database. | 22 | | GET | tournament/query\_game | `name`, `index`, `agent0`, `agent1`, `win`, `payoff`, `elements_every_page`, `page_index` | Query the games with the given parameters | 23 | | GET | tournament/query\_payoff | `name`, `agent0`, `agent1`, `payoff` | Query the payoffs with the given parameters | 24 | | GET | tournament/query\_agent\_payoff | `name`, `elements_every_page`, `page_index`, | Query the payoffs of all the agents | 25 | | GET | tournament/replay | `name`, `agent0`, `agent1`, `index` | Return the replay data | 26 | | POST | tournament/upload\_agent | `model`(Python file), `name`, `game`, | Upload a model file. `name` is model ID | 27 | | GET | tournament/delete\_agent | `name` | Delete the agent of the given name | 28 | | GET | tournament/list\_uploaded\_agents | `game` | list all the uploaded agents | 29 | | GET | tournament/list\_baseline\_agents | `game` | list all the baseline agents | 30 | | GET | download\_examples | `name` | download the example agents | 31 | 32 | ### Example API 33 | | API | Description | 34 | |------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------| 35 | | http://127.0.0.1:8000/tournament/launch?num_eval_games=200&name=leduc-holdem | Evaluate on Leduc Holdem with 200 games for each pair of models | 36 | | http://127.0.0.1:8000/tournament/replay?name=leduc-holdem&agent0=leduc-holdem-rule-v1&agent1=leduc-holdem-cfr&index=3 | Obtain the replay data between rule model and CFR model. Obtain the data of the 3rd game | 37 | | http://127.0.0.1:8000/tournament/query_game&elements_every_page=10&page_index=0 | Get all the game data | 38 | | http://127.0.0.1:8000/tournament/query_game?name=leduc-holdem&elements_every_page=10&page_index=0 | Get all the game data of Leduc Holdem | 39 | | http://127.0.0.1:8000/tournament/query_payoff | Get all the payoffs | 40 | | http://127.0.0.1:8000/tournament/query_payoff?agent0=leduc-holdem-cfr&agent1=leduc-holdem-rule-v1 | Get all the payoffs between rule and CFR models | 41 | | http://127.0.0.1:8000/tournament/query_agent_payoff?name=leduc-holdem&elements_every_page=1&page_index=1 | Get the payoffs of all the agents of leduc-holdem | 42 | | http://127.0.0.1:8000/tournament/list_uploaded_agents?game=leduc-holdem | List the uploaded agents of leduc-holdem | 43 | | http://127.0.0.1:8000/tournament/list_baseline_agents?game=leduc-holdem | List the baseline agents of leduc-holdem | 44 | | http://127.0.0.1:8000/tournament/download_examples?name=example_luduc_nfsp_model | Download the NFSP example model for Leduc Hold'em | 45 | 46 | ## Registered Models 47 | Some models have been pre-registered as baselines 48 | | Model | Game | Description | 49 | |----------------------|--------------|---------------------------------------| 50 | | leduc-holdem-random | leduc-holdem | A random model | 51 | | leduc-holdem-cfr | leduc-holdem | Pre-trained CFR model on Leduc Holdem | 52 | | leduc-holdem-rule-v1 | leduc-holdem | A rule model that plays greedily | 53 | | doudizhu-random | doudizhu | A random model | 54 | | doudizhu-rule-v1 | doudizhu | Dou Dizhu rule model | 55 | 56 | ### Example of uploading a new model 57 | A example model file is prepared: 58 | ``` 59 | cd server/media/example_agents 60 | ``` 61 | Upload the model with `curl`: 62 | ``` 63 | curl -F 'model=@example_luduc_nfsp_model.zip' -F "name=leduc-nfsp" -F "game=leduc-holdem" http://127.0.0.1:8000/tournament/upload_agent 64 | ``` 65 | Launch the tounament with: 66 | ``` 67 | curl 'http://127.0.0.1:8000/tournament/launch?num_eval_games=200&name=leduc-holdem' 68 | ``` 69 | We list the uploaded agent with 70 | ``` 71 | curl http://127.0.0.1:8000/tournament/list_uploaded_agents 72 | ``` 73 | We can delete the agent with 74 | ``` 75 | curl 'http://127.0.0.1:8000/tournament/delete_agent?name=leduc-new' 76 | ``` 77 | 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rlcard-showdown", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.9.0", 7 | "@material-ui/icons": "^4.9.1", 8 | "element-react": "^1.4.34", 9 | "element-theme-default": "^1.4.13", 10 | "i18next": "^20.3.0", 11 | "node-sass": "^4.13.0", 12 | "query-string": "^6.13.1", 13 | "react": "^16.12.0", 14 | "react-addons-css-transition-group": "^15.6.2", 15 | "react-dom": "^16.12.0", 16 | "react-hot-loader": "^4.12.19", 17 | "react-i18next": "^11.9.0", 18 | "react-router-dom": "^5.1.2", 19 | "react-scripts": "3.4.0", 20 | "socket.io-client": "^2.3.0" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": "react-app" 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "axios": "^0.21.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | RLcard Showdown 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /pve_server/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.so 3 | __pycache__ 4 | .DS_Store 5 | *.egg-info 6 | *.pyc 7 | *.onnx 8 | -------------------------------------------------------------------------------- /pve_server/deep.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import torch 4 | import numpy as np 5 | from collections import Counter 6 | 7 | Card2Column = {3: 0, 4: 1, 5: 2, 6: 3, 7: 4, 8: 5, 9: 6, 10: 7, 8 | 11: 8, 12: 9, 13: 10, 14: 11, 17: 12, 20: 13, 30: 14} 9 | 10 | NumOnes2Array = {0: np.array([0, 0, 0, 0]), 11 | 1: np.array([1, 0, 0, 0]), 12 | 2: np.array([1, 1, 0, 0]), 13 | 3: np.array([1, 1, 1, 0]), 14 | 4: np.array([1, 1, 1, 1])} 15 | 16 | def _get_one_hot_bomb(bomb_num): 17 | one_hot = np.zeros(15, dtype=np.float32) 18 | one_hot[bomb_num] = 1 19 | return one_hot 20 | 21 | def _load_model(position, model_dir, use_onnx): 22 | if not use_onnx or not os.path.isfile(os.path.join(model_dir, position+'.onnx')) : 23 | from models import model_dict 24 | model = model_dict[position]() 25 | model_state_dict = model.state_dict() 26 | model_path = os.path.join(model_dir, position+'.ckpt') 27 | if torch.cuda.is_available(): 28 | pretrained = torch.load(model_path, map_location='cuda:0') 29 | else: 30 | pretrained = torch.load(model_path, map_location='cpu') 31 | pretrained = {k: v for k, v in pretrained.items() if k in model_state_dict} 32 | model_state_dict.update(pretrained) 33 | model.load_state_dict(model_state_dict) 34 | if torch.cuda.is_available(): 35 | model.cuda() 36 | model.eval() 37 | 38 | if use_onnx: 39 | z = torch.randn(1, 5, 162, requires_grad=True) 40 | if position == 'landlord': 41 | x = torch.randn(1, 373, requires_grad=True) 42 | else: 43 | x = torch.randn(1, 484, requires_grad=True) 44 | torch.onnx.export(model, 45 | (z,x), 46 | os.path.join(model_dir, position+'.onnx'), 47 | export_params=True, 48 | opset_version=10, 49 | do_constant_folding=True, 50 | input_names = ['z', 'x'], 51 | output_names = ['y'], 52 | dynamic_axes={'z' : {0 : 'batch_size'}, 53 | 'x' : {0 : 'batch_size'}, 54 | 'y' : {0 : 'batch_size'}}) 55 | 56 | if use_onnx: 57 | import onnxruntime 58 | model = onnxruntime.InferenceSession(os.path.join(model_dir, position+'.onnx')) 59 | return model 60 | 61 | def _process_action_seq(sequence, length=15): 62 | sequence = sequence[-length:].copy() 63 | if len(sequence) < length: 64 | empty_sequence = [[] for _ in range(length - len(sequence))] 65 | empty_sequence.extend(sequence) 66 | sequence = empty_sequence 67 | return sequence 68 | 69 | class DeepAgent: 70 | 71 | def __init__(self, position, model_dir, use_onnx=False): 72 | self.model = _load_model(position, model_dir, use_onnx) 73 | self.use_onnx = use_onnx 74 | 75 | def cards2array(self, list_cards): 76 | if len(list_cards) == 0: 77 | return np.zeros(54, dtype=np.float32) 78 | 79 | matrix = np.zeros([4, 13], dtype=np.float32) 80 | jokers = np.zeros(2, dtype=np.float32) 81 | counter = Counter(list_cards) 82 | for card, num_times in counter.items(): 83 | if card < 20: 84 | matrix[:, Card2Column[card]] = NumOnes2Array[num_times] 85 | elif card == 20: 86 | jokers[0] = 1 87 | elif card == 30: 88 | jokers[1] = 1 89 | return np.concatenate((matrix.flatten('F'), jokers)) 90 | 91 | def get_one_hot_array(self, num_left_cards, max_num_cards): 92 | one_hot = np.zeros(max_num_cards, dtype=np.float32) 93 | one_hot[num_left_cards - 1] = 1 94 | 95 | return one_hot 96 | 97 | def action_seq_list2array(self, action_seq_list): 98 | action_seq_array = np.zeros((len(action_seq_list), 54), dtype=np.float32) 99 | for row, list_cards in enumerate(action_seq_list): 100 | action_seq_array[row, :] = self.cards2array(list_cards) 101 | action_seq_array = action_seq_array.reshape(5, 162) 102 | return action_seq_array 103 | 104 | def act(self, infoset): 105 | player_position = infoset.player_position 106 | num_legal_actions = len(infoset.legal_actions) 107 | my_handcards = self.cards2array(infoset.player_hand_cards) 108 | my_handcards_batch = np.repeat(my_handcards[np.newaxis, :], 109 | num_legal_actions, axis=0) 110 | 111 | other_handcards = self.cards2array(infoset.other_hand_cards) 112 | other_handcards_batch = np.repeat(other_handcards[np.newaxis, :], 113 | num_legal_actions, axis=0) 114 | 115 | my_action_batch = np.zeros(my_handcards_batch.shape, dtype=np.float32) 116 | for j, action in enumerate(infoset.legal_actions): 117 | my_action_batch[j, :] = self.cards2array(action) 118 | 119 | last_action = self.cards2array(infoset.rival_move) 120 | last_action_batch = np.repeat(last_action[np.newaxis, :], 121 | num_legal_actions, axis=0) 122 | 123 | if player_position == 0: 124 | landlord_up_num_cards_left = self.get_one_hot_array( 125 | infoset.num_cards_left[2], 17) 126 | landlord_up_num_cards_left_batch = np.repeat( 127 | landlord_up_num_cards_left[np.newaxis, :], 128 | num_legal_actions, axis=0) 129 | 130 | landlord_down_num_cards_left = self.get_one_hot_array( 131 | infoset.num_cards_left[1], 17) 132 | landlord_down_num_cards_left_batch = np.repeat( 133 | landlord_down_num_cards_left[np.newaxis, :], 134 | num_legal_actions, axis=0) 135 | 136 | landlord_up_played_cards = self.cards2array( 137 | infoset.played_cards[2]) 138 | landlord_up_played_cards_batch = np.repeat( 139 | landlord_up_played_cards[np.newaxis, :], 140 | num_legal_actions, axis=0) 141 | 142 | landlord_down_played_cards = self.cards2array( 143 | infoset.played_cards[1]) 144 | landlord_down_played_cards_batch = np.repeat( 145 | landlord_down_played_cards[np.newaxis, :], 146 | num_legal_actions, axis=0) 147 | 148 | bomb_num = _get_one_hot_bomb( 149 | infoset.bomb_num) 150 | bomb_num_batch = np.repeat( 151 | bomb_num[np.newaxis, :], 152 | num_legal_actions, axis=0) 153 | 154 | x_batch = np.hstack((my_handcards_batch, 155 | other_handcards_batch, 156 | last_action_batch, 157 | landlord_up_played_cards_batch, 158 | landlord_down_played_cards_batch, 159 | landlord_up_num_cards_left_batch, 160 | landlord_down_num_cards_left_batch, 161 | bomb_num_batch, 162 | my_action_batch)) 163 | z = self.action_seq_list2array(_process_action_seq( 164 | infoset.card_play_action_seq)) 165 | z_batch = np.repeat( 166 | z[np.newaxis, :, :], 167 | num_legal_actions, axis=0) 168 | if self.use_onnx: 169 | ort_inputs = {'z': z_batch, 'x': x_batch} 170 | y_pred = self.model.run(None, ort_inputs)[0] 171 | elif torch.cuda.is_available(): 172 | y_pred = self.model.forward(torch.from_numpy(z_batch).float().cuda(), 173 | torch.from_numpy(x_batch).float().cuda()) 174 | y_pred = y_pred.cpu().detach().numpy() 175 | else: 176 | y_pred = self.model.forward(torch.from_numpy(z_batch).float(), 177 | torch.from_numpy(x_batch).float()) 178 | y_pred = y_pred.detach().numpy() 179 | else: 180 | last_landlord_action = self.cards2array( 181 | infoset.last_moves[0]) 182 | last_landlord_action_batch = np.repeat( 183 | last_landlord_action[np.newaxis, :], 184 | num_legal_actions, axis=0) 185 | landlord_num_cards_left = self.get_one_hot_array( 186 | infoset.num_cards_left[0], 20) 187 | landlord_num_cards_left_batch = np.repeat( 188 | landlord_num_cards_left[np.newaxis, :], 189 | num_legal_actions, axis=0) 190 | 191 | landlord_played_cards = self.cards2array( 192 | infoset.played_cards[0]) 193 | landlord_played_cards_batch = np.repeat( 194 | landlord_played_cards[np.newaxis, :], 195 | num_legal_actions, axis=0) 196 | 197 | if player_position == 2: 198 | last_teammate_action = self.cards2array( 199 | infoset.last_moves[1]) 200 | last_teammate_action_batch = np.repeat( 201 | last_teammate_action[np.newaxis, :], 202 | num_legal_actions, axis=0) 203 | teammate_num_cards_left = self.get_one_hot_array( 204 | infoset.num_cards_left[1], 17) 205 | teammate_num_cards_left_batch = np.repeat( 206 | teammate_num_cards_left[np.newaxis, :], 207 | num_legal_actions, axis=0) 208 | 209 | teammate_played_cards = self.cards2array( 210 | infoset.played_cards[1]) 211 | teammate_played_cards_batch = np.repeat( 212 | teammate_played_cards[np.newaxis, :], 213 | num_legal_actions, axis=0) 214 | 215 | else: 216 | last_teammate_action = self.cards2array( 217 | infoset.last_moves[2]) 218 | last_teammate_action_batch = np.repeat( 219 | last_teammate_action[np.newaxis, :], 220 | num_legal_actions, axis=0) 221 | teammate_num_cards_left = self.get_one_hot_array( 222 | infoset.num_cards_left[2], 17) 223 | teammate_num_cards_left_batch = np.repeat( 224 | teammate_num_cards_left[np.newaxis, :], 225 | num_legal_actions, axis=0) 226 | 227 | teammate_played_cards = self.cards2array( 228 | infoset.played_cards[2]) 229 | teammate_played_cards_batch = np.repeat( 230 | teammate_played_cards[np.newaxis, :], 231 | num_legal_actions, axis=0) 232 | 233 | bomb_num = _get_one_hot_bomb( 234 | infoset.bomb_num) 235 | bomb_num_batch = np.repeat( 236 | bomb_num[np.newaxis, :], 237 | num_legal_actions, axis=0) 238 | x_batch = np.hstack((my_handcards_batch, 239 | other_handcards_batch, 240 | landlord_played_cards_batch, 241 | teammate_played_cards_batch, 242 | last_action_batch, 243 | last_landlord_action_batch, 244 | last_teammate_action_batch, 245 | landlord_num_cards_left_batch, 246 | teammate_num_cards_left_batch, 247 | bomb_num_batch, 248 | my_action_batch)) 249 | z = self.action_seq_list2array(_process_action_seq(infoset.card_play_action_seq)) 250 | z_batch = np.repeat( 251 | z[np.newaxis, :, :], 252 | num_legal_actions, axis=0) 253 | if self.use_onnx: 254 | ort_inputs = {'z': z_batch, 'x': x_batch} 255 | y_pred = self.model.run(None, ort_inputs)[0] 256 | elif torch.cuda.is_available(): 257 | y_pred = self.model.forward(torch.from_numpy(z_batch).float().cuda(), 258 | torch.from_numpy(x_batch).float().cuda()) 259 | y_pred = y_pred.cpu().detach().numpy() 260 | else: 261 | y_pred = self.model.forward(torch.from_numpy(z_batch).float(), 262 | torch.from_numpy(x_batch).float()) 263 | y_pred = y_pred.detach().numpy() 264 | 265 | y_pred = y_pred.flatten() 266 | 267 | #best_action_index = np.argmax(y_pred, axis=0)[0] 268 | size = min(3, len(y_pred)) 269 | best_action_index = np.argpartition(y_pred, -size)[-size:] 270 | best_action_confidence = y_pred[best_action_index] 271 | best_action = [infoset.legal_actions[index] for index in best_action_index] 272 | 273 | return best_action, best_action_confidence 274 | -------------------------------------------------------------------------------- /pve_server/models.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch import nn 3 | 4 | class LandlordLstmModel(nn.Module): 5 | def __init__(self): 6 | super().__init__() 7 | self.lstm = nn.LSTM(162, 128, batch_first = True) 8 | self.dense1 = nn.Linear(373 + 128, 512) 9 | self.dense2 = nn.Linear(512, 512) 10 | self.dense3 = nn.Linear(512, 512) 11 | self.dense4 = nn.Linear(512, 512) 12 | self.dense5 = nn.Linear(512, 512) 13 | self.dense5 = nn.Linear(512, 512) 14 | self.dense5 = nn.Linear(512, 512) 15 | self.dense6 = nn.Linear(512, 1) 16 | 17 | def forward(self, z, x): 18 | lstm_out, (h_n, _) = self.lstm(z) 19 | lstm_out = lstm_out[:,-1,:] 20 | x = torch.cat([lstm_out,x], dim=-1) 21 | x = self.dense1(x) 22 | x = torch.relu(x) 23 | x = self.dense2(x) 24 | x = torch.relu(x) 25 | x = self.dense3(x) 26 | x = torch.relu(x) 27 | x = self.dense4(x) 28 | x = torch.relu(x) 29 | x = self.dense5(x) 30 | x = torch.relu(x) 31 | x = self.dense6(x) 32 | return x 33 | 34 | class FarmerLstmModel(nn.Module): 35 | def __init__(self): 36 | super().__init__() 37 | self.lstm = nn.LSTM(162, 128, batch_first = True) 38 | self.dense1 = nn.Linear(484 + 128 , 512) 39 | self.dense2 = nn.Linear(512, 512) 40 | self.dense3 = nn.Linear(512, 512) 41 | self.dense4 = nn.Linear(512, 512) 42 | self.dense4 = nn.Linear(512, 512) 43 | self.dense4 = nn.Linear(512, 512) 44 | self.dense5 = nn.Linear(512, 512) 45 | self.dense6 = nn.Linear(512, 1) 46 | 47 | def forward(self, z, x): 48 | lstm_out, (h_n, _) = self.lstm(z) 49 | lstm_out = lstm_out[:,-1,:] 50 | x = torch.cat([lstm_out,x], dim=-1) 51 | x = self.dense1(x) 52 | x = torch.relu(x) 53 | x = self.dense2(x) 54 | x = torch.relu(x) 55 | x = self.dense3(x) 56 | x = torch.relu(x) 57 | x = self.dense4(x) 58 | x = torch.relu(x) 59 | x = self.dense5(x) 60 | x = torch.relu(x) 61 | x = self.dense6(x) 62 | return x 63 | 64 | model_dict = {} 65 | model_dict['landlord'] = LandlordLstmModel 66 | model_dict['landlord_up'] = FarmerLstmModel 67 | model_dict['landlord_down'] = FarmerLstmModel 68 | -------------------------------------------------------------------------------- /pve_server/pretrained/put_pretrained_models_here: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/pve_server/pretrained/put_pretrained_models_here -------------------------------------------------------------------------------- /pve_server/run_dmc.py: -------------------------------------------------------------------------------- 1 | from utils.move_generator import MovesGener 2 | from utils import move_selector as ms 3 | from utils import move_detector as md 4 | import rlcard 5 | import itertools 6 | import os 7 | from collections import Counter, OrderedDict 8 | from heapq import nlargest 9 | 10 | import numpy as np 11 | import torch 12 | from flask import Flask, jsonify, request 13 | from flask_cors import CORS 14 | 15 | app = Flask(__name__) 16 | CORS(app) 17 | 18 | 19 | env = rlcard.make('doudizhu') 20 | 21 | DouZeroCard2RLCard = {3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 22 | 8: '8', 9: '9', 10: 'T', 11: 'J', 12: 'Q', 23 | 13: 'K', 14: 'A', 17: '2', 20: 'B', 30: 'R'} 24 | 25 | RLCard2DouZeroCard = {'3': 3, '4': 4, '5': 5, '6': 6, '7': 7, 26 | '8': 8, '9': 9, 'T': 10, 'J': 11, 'Q': 12, 27 | 'K': 13, 'A': 14, '2': 17, 'B': 20, 'R': 30} 28 | 29 | EnvCard2RealCard = {'3': '3', '4': '4', '5': '5', '6': '6', '7': '7', 30 | '8': '8', '9': '9', 'T': 'T', 'J': 'J', 'Q': 'Q', 31 | 'K': 'K', 'A': 'A', '2': '2', 'B': 'X', 'R': 'D'} 32 | 33 | RealCard2EnvCard = {'3': '3', '4': '4', '5': '5', '6': '6', '7': '7', 34 | '8': '8', '9': '9', 'T': 'T', 'J': 'J', 'Q': 'Q', 35 | 'K': 'K', 'A': 'A', '2': '2', 'X': 'B', 'D': 'R'} 36 | 37 | pretrained_dir = 'pretrained/dmc_pretrained' 38 | device = torch.device('cpu') 39 | players = [] 40 | for i in range(3): 41 | model_path = os.path.join(pretrained_dir, str(i)+'.pth') 42 | agent = torch.load(model_path, map_location=device) 43 | agent.set_device(device) 44 | players.append(agent) 45 | 46 | 47 | @app.route('/predict', methods=['POST']) 48 | def predict(): 49 | if request.method == 'POST': 50 | try: 51 | # Player postion 52 | player_position = request.form.get('player_position') 53 | if player_position not in ['0', '1', '2']: 54 | return jsonify({'status': 1, 'message': 'player_position must be 0, 1, or 2'}) 55 | player_position = int(player_position) 56 | 57 | # Player hand cards 58 | player_hand_cards = ''.join( 59 | [RealCard2EnvCard[c] for c in request.form.get('player_hand_cards')]) 60 | if player_position == 0: 61 | if len(player_hand_cards) < 1 or len(player_hand_cards) > 20: 62 | return jsonify({'status': 2, 'message': 'the number of hand cards should be 1-20'}) 63 | else: 64 | if len(player_hand_cards) < 1 or len(player_hand_cards) > 17: 65 | return jsonify({'status': 3, 'message': 'the number of hand cards should be 1-17'}) 66 | 67 | # Number cards left 68 | num_cards_left = [int(request.form.get('num_cards_left_landlord')), int(request.form.get( 69 | 'num_cards_left_landlord_down')), int(request.form.get('num_cards_left_landlord_up'))] 70 | if num_cards_left[player_position] != len(player_hand_cards): 71 | return jsonify({'status': 4, 'message': 'the number of cards left do not align with hand cards'}) 72 | if num_cards_left[0] < 0 or num_cards_left[1] < 0 or num_cards_left[2] < 0 or num_cards_left[0] > 20 or num_cards_left[1] > 17 or num_cards_left[2] > 17: 73 | return jsonify({'status': 5, 'message': 'the number of cards left not in range'}) 74 | 75 | # Three landlord cards 76 | three_landlord_cards = ''.join( 77 | [RealCard2EnvCard[c] for c in request.form.get('three_landlord_cards')]) 78 | if len(three_landlord_cards) < 0 or len(three_landlord_cards) > 3: 79 | return jsonify({'status': 6, 'message': 'the number of landlord cards should be 0-3'}) 80 | 81 | # Card play sequence 82 | if request.form.get('card_play_action_seq') == '': 83 | card_play_action_seq = [] 84 | else: 85 | tmp_seq = [''.join([RealCard2EnvCard[c] for c in cards]) 86 | for cards in request.form.get('card_play_action_seq').split(',')] 87 | for i in range(len(tmp_seq)): 88 | if tmp_seq[i] == '': 89 | tmp_seq[i] = 'pass' 90 | card_play_action_seq = [] 91 | for i in range(len(tmp_seq)): 92 | card_play_action_seq.append((i % 3, tmp_seq[i])) 93 | 94 | # Other hand cards 95 | other_hand_cards = ''.join( 96 | [RealCard2EnvCard[c] for c in request.form.get('other_hand_cards')]) 97 | if len(other_hand_cards) != sum(num_cards_left) - num_cards_left[player_position]: 98 | return jsonify({'status': 7, 'message': 'the number of the other hand cards do not align with the number of cards left'}) 99 | 100 | # Played cards 101 | played_cards = [] 102 | for field in ['played_cards_landlord', 'played_cards_landlord_down', 'played_cards_landlord_up']: 103 | played_cards.append( 104 | ''.join([RealCard2EnvCard[c] for c in request.form.get(field)])) 105 | 106 | # RLCard state 107 | state = {} 108 | state['current_hand'] = player_hand_cards 109 | state['landlord'] = 0 110 | state['num_cards_left'] = num_cards_left 111 | state['others_hand'] = other_hand_cards 112 | state['played_cards'] = played_cards 113 | state['seen_cards'] = three_landlord_cards 114 | state['self'] = player_position 115 | state['trace'] = card_play_action_seq 116 | 117 | # Get rival move and legal_actions 118 | rival_move = 'pass' 119 | if len(card_play_action_seq) != 0: 120 | if card_play_action_seq[-1][1] == 'pass': 121 | rival_move = card_play_action_seq[-2][1] 122 | else: 123 | rival_move = card_play_action_seq[-1][1] 124 | if rival_move == 'pass': 125 | rival_move = '' 126 | rival_move = [RLCard2DouZeroCard[c] for c in rival_move] 127 | state['actions'] = _get_legal_card_play_actions( 128 | [RLCard2DouZeroCard[c] for c in player_hand_cards], rival_move) 129 | state['actions'] = [ 130 | ''.join([DouZeroCard2RLCard[c] for c in a]) for a in state['actions']] 131 | for i in range(len(state['actions'])): 132 | if state['actions'][i] == '': 133 | state['actions'][i] = 'pass' 134 | 135 | # Prediction 136 | state = _extract_state(state) 137 | action, info = players[player_position].eval_step(state) 138 | if action == 'pass': 139 | action = '' 140 | for i in info['values']: 141 | if i == 'pass': 142 | info['values'][''] = info['values']['pass'] 143 | del info['values']['pass'] 144 | break 145 | 146 | actions = nlargest(3, info['values'], key=info['values'].get) 147 | actions_confidence = [info['values'].get( 148 | action) for action in actions] 149 | actions = [''.join([EnvCard2RealCard[c] for c in action]) 150 | for action in actions] 151 | result = {} 152 | win_rates = {} 153 | for i in range(len(actions)): 154 | # Here, we calculate the win rate 155 | win_rate = min(actions_confidence[i], 1) 156 | win_rate = max(win_rate, 0) 157 | win_rates[actions[i]] = str(round(win_rate, 4)) 158 | result[actions[i]] = str(round(actions_confidence[i], 6)) 159 | 160 | ############## DEBUG ################ 161 | if app.debug: 162 | print('--------------- DEBUG START --------------') 163 | command = 'curl --data "' 164 | parameters = [] 165 | for key in request.form: 166 | parameters.append(key+'='+request.form.get(key)) 167 | print(key+':', request.form.get(key)) 168 | command += '&'.join(parameters) 169 | command += '" "http://127.0.0.1:5000/predict"' 170 | print('Command:', command) 171 | print('Rival Move:', rival_move) 172 | print('legal_actions:', state['legal_actions']) 173 | print('Result:', result) 174 | print('--------------- DEBUG END --------------') 175 | ############## DEBUG ################ 176 | return jsonify({'status': 0, 'message': 'success', 'result': result, 'win_rates': win_rates}) 177 | except: 178 | import traceback 179 | traceback.print_exc() 180 | return jsonify({'status': -1, 'message': 'unkown error'}) 181 | 182 | 183 | @app.route('/legal', methods=['POST']) 184 | def legal(): 185 | if request.method == 'POST': 186 | try: 187 | player_hand_cards = [RealCard2EnvCard[c] 188 | for c in request.form.get('player_hand_cards')] 189 | rival_move = [RealCard2EnvCard[c] 190 | for c in request.form.get('rival_move')] 191 | if rival_move == '': 192 | rival_move = 'pass' 193 | player_hand_cards = [RLCard2DouZeroCard[c] 194 | for c in player_hand_cards] 195 | rival_move = [RLCard2DouZeroCard[c] for c in rival_move] 196 | legal_actions = _get_legal_card_play_actions( 197 | player_hand_cards, rival_move) 198 | legal_actions = [''.join([DouZeroCard2RLCard[c] 199 | for c in a]) for a in legal_actions] 200 | for i in range(len(legal_actions)): 201 | if legal_actions[i] == 'pass': 202 | legal_actions[i] = '' 203 | legal_actions = ','.join( 204 | [''.join([EnvCard2RealCard[c] for c in action]) for action in legal_actions]) 205 | return jsonify({'status': 0, 'message': 'success', 'legal_action': legal_actions}) 206 | except: 207 | import traceback 208 | traceback.print_exc() 209 | return jsonify({'status': -1, 'message': 'unkown error'}) 210 | 211 | 212 | def _extract_state(state): 213 | current_hand = _cards2array(state['current_hand']) 214 | others_hand = _cards2array(state['others_hand']) 215 | 216 | last_action = '' 217 | if len(state['trace']) != 0: 218 | if state['trace'][-1][1] == 'pass': 219 | last_action = state['trace'][-2][1] 220 | else: 221 | last_action = state['trace'][-1][1] 222 | last_action = _cards2array(last_action) 223 | 224 | last_9_actions = _action_seq2array(_process_action_seq(state['trace'])) 225 | 226 | if state['self'] == 0: # landlord 227 | landlord_up_played_cards = _cards2array(state['played_cards'][2]) 228 | landlord_down_played_cards = _cards2array(state['played_cards'][1]) 229 | landlord_up_num_cards_left = _get_one_hot_array( 230 | state['num_cards_left'][2], 17) 231 | landlord_down_num_cards_left = _get_one_hot_array( 232 | state['num_cards_left'][1], 17) 233 | obs = np.concatenate((current_hand, 234 | others_hand, 235 | last_action, 236 | last_9_actions, 237 | landlord_up_played_cards, 238 | landlord_down_played_cards, 239 | landlord_up_num_cards_left, 240 | landlord_down_num_cards_left)) 241 | else: 242 | landlord_played_cards = _cards2array(state['played_cards'][0]) 243 | for i, action in reversed(state['trace']): 244 | if i == 0: 245 | last_landlord_action = action 246 | last_landlord_action = _cards2array(last_landlord_action) 247 | landlord_num_cards_left = _get_one_hot_array( 248 | state['num_cards_left'][0], 20) 249 | 250 | teammate_id = 3 - state['self'] 251 | teammate_played_cards = _cards2array( 252 | state['played_cards'][teammate_id]) 253 | last_teammate_action = 'pass' 254 | for i, action in reversed(state['trace']): 255 | if i == teammate_id: 256 | last_teammate_action = action 257 | last_teammate_action = _cards2array(last_teammate_action) 258 | teammate_num_cards_left = _get_one_hot_array( 259 | state['num_cards_left'][teammate_id], 17) 260 | obs = np.concatenate((current_hand, 261 | others_hand, 262 | last_action, 263 | last_9_actions, 264 | landlord_played_cards, 265 | teammate_played_cards, 266 | last_landlord_action, 267 | last_teammate_action, 268 | landlord_num_cards_left, 269 | teammate_num_cards_left)) 270 | 271 | legal_actions = {env._ACTION_2_ID[action]: _cards2array( 272 | action) for action in state['actions']} 273 | extracted_state = OrderedDict({'obs': obs, 'legal_actions': legal_actions}) 274 | extracted_state['raw_obs'] = state 275 | extracted_state['raw_legal_actions'] = [a for a in state['actions']] 276 | return extracted_state 277 | 278 | 279 | def _get_legal_card_play_actions(player_hand_cards, rival_move): 280 | mg = MovesGener(player_hand_cards) 281 | 282 | rival_type = md.get_move_type(rival_move) 283 | rival_move_type = rival_type['type'] 284 | rival_move_len = rival_type.get('len', 1) 285 | moves = list() 286 | 287 | if rival_move_type == md.TYPE_0_PASS: 288 | moves = mg.gen_moves() 289 | 290 | elif rival_move_type == md.TYPE_1_SINGLE: 291 | all_moves = mg.gen_type_1_single() 292 | moves = ms.filter_type_1_single(all_moves, rival_move) 293 | 294 | elif rival_move_type == md.TYPE_2_PAIR: 295 | all_moves = mg.gen_type_2_pair() 296 | moves = ms.filter_type_2_pair(all_moves, rival_move) 297 | 298 | elif rival_move_type == md.TYPE_3_TRIPLE: 299 | all_moves = mg.gen_type_3_triple() 300 | moves = ms.filter_type_3_triple(all_moves, rival_move) 301 | 302 | elif rival_move_type == md.TYPE_4_BOMB: 303 | all_moves = mg.gen_type_4_bomb() + mg.gen_type_5_king_bomb() 304 | moves = ms.filter_type_4_bomb(all_moves, rival_move) 305 | 306 | elif rival_move_type == md.TYPE_5_KING_BOMB: 307 | moves = [] 308 | 309 | elif rival_move_type == md.TYPE_6_3_1: 310 | all_moves = mg.gen_type_6_3_1() 311 | moves = ms.filter_type_6_3_1(all_moves, rival_move) 312 | 313 | elif rival_move_type == md.TYPE_7_3_2: 314 | all_moves = mg.gen_type_7_3_2() 315 | moves = ms.filter_type_7_3_2(all_moves, rival_move) 316 | 317 | elif rival_move_type == md.TYPE_8_SERIAL_SINGLE: 318 | all_moves = mg.gen_type_8_serial_single(repeat_num=rival_move_len) 319 | moves = ms.filter_type_8_serial_single(all_moves, rival_move) 320 | 321 | elif rival_move_type == md.TYPE_9_SERIAL_PAIR: 322 | all_moves = mg.gen_type_9_serial_pair(repeat_num=rival_move_len) 323 | moves = ms.filter_type_9_serial_pair(all_moves, rival_move) 324 | 325 | elif rival_move_type == md.TYPE_10_SERIAL_TRIPLE: 326 | all_moves = mg.gen_type_10_serial_triple(repeat_num=rival_move_len) 327 | moves = ms.filter_type_10_serial_triple(all_moves, rival_move) 328 | 329 | elif rival_move_type == md.TYPE_11_SERIAL_3_1: 330 | all_moves = mg.gen_type_11_serial_3_1(repeat_num=rival_move_len) 331 | moves = ms.filter_type_11_serial_3_1(all_moves, rival_move) 332 | 333 | elif rival_move_type == md.TYPE_12_SERIAL_3_2: 334 | all_moves = mg.gen_type_12_serial_3_2(repeat_num=rival_move_len) 335 | moves = ms.filter_type_12_serial_3_2(all_moves, rival_move) 336 | 337 | elif rival_move_type == md.TYPE_13_4_2: 338 | all_moves = mg.gen_type_13_4_2() 339 | moves = ms.filter_type_13_4_2(all_moves, rival_move) 340 | 341 | elif rival_move_type == md.TYPE_14_4_22: 342 | all_moves = mg.gen_type_14_4_22() 343 | moves = ms.filter_type_14_4_22(all_moves, rival_move) 344 | 345 | if rival_move_type not in [md.TYPE_0_PASS, 346 | md.TYPE_4_BOMB, md.TYPE_5_KING_BOMB]: 347 | moves = moves + mg.gen_type_4_bomb() + mg.gen_type_5_king_bomb() 348 | 349 | if len(rival_move) != 0: # rival_move is not 'pass' 350 | moves = moves + [[]] 351 | 352 | for m in moves: 353 | m.sort() 354 | 355 | moves.sort() 356 | moves = list(move for move, _ in itertools.groupby(moves)) 357 | 358 | # Remove Quad with black and red joker 359 | for i in [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17]: 360 | illegal_move = [i]*4 + [20, 30] 361 | if illegal_move in moves: 362 | moves.remove(illegal_move) 363 | 364 | return moves 365 | 366 | 367 | Card2Column = {'3': 0, '4': 1, '5': 2, '6': 3, '7': 4, '8': 5, '9': 6, 'T': 7, 368 | 'J': 8, 'Q': 9, 'K': 10, 'A': 11, '2': 12} 369 | 370 | NumOnes2Array = {0: np.array([0, 0, 0, 0]), 371 | 1: np.array([1, 0, 0, 0]), 372 | 2: np.array([1, 1, 0, 0]), 373 | 3: np.array([1, 1, 1, 0]), 374 | 4: np.array([1, 1, 1, 1])} 375 | 376 | 377 | def _cards2array(cards): 378 | if cards == 'pass': 379 | return np.zeros(54, dtype=np.int8) 380 | 381 | matrix = np.zeros([4, 13], dtype=np.int8) 382 | jokers = np.zeros(2, dtype=np.int8) 383 | counter = Counter(cards) 384 | for card, num_times in counter.items(): 385 | if card == 'B': 386 | jokers[0] = 1 387 | elif card == 'R': 388 | jokers[1] = 1 389 | else: 390 | matrix[:, Card2Column[card]] = NumOnes2Array[num_times] 391 | return np.concatenate((matrix.flatten('F'), jokers)) 392 | 393 | 394 | def _get_one_hot_array(num_left_cards, max_num_cards): 395 | one_hot = np.zeros(max_num_cards, dtype=np.int8) 396 | one_hot[num_left_cards - 1] = 1 397 | 398 | return one_hot 399 | 400 | 401 | def _action_seq2array(action_seq_list): 402 | action_seq_array = np.zeros((len(action_seq_list), 54), np.int8) 403 | for row, cards in enumerate(action_seq_list): 404 | action_seq_array[row, :] = _cards2array(cards) 405 | action_seq_array = action_seq_array.flatten() 406 | return action_seq_array 407 | 408 | 409 | def _process_action_seq(sequence, length=9): 410 | sequence = [action[1] for action in sequence[-length:]] 411 | if len(sequence) < length: 412 | empty_sequence = ['' for _ in range(length - len(sequence))] 413 | empty_sequence.extend(sequence) 414 | sequence = empty_sequence 415 | return sequence 416 | 417 | 418 | if __name__ == '__main__': 419 | import argparse 420 | parser = argparse.ArgumentParser(description='DouZero backend') 421 | parser.add_argument('--debug', action='store_true') 422 | args = parser.parse_args() 423 | app.run(debug=args.debug) 424 | -------------------------------------------------------------------------------- /pve_server/run_douzero.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | from flask import Flask, jsonify, request 4 | from flask_cors import CORS 5 | 6 | app = Flask(__name__) 7 | CORS(app) 8 | 9 | from utils.move_generator import MovesGener 10 | from utils import move_detector as md, move_selector as ms 11 | from deep import DeepAgent 12 | 13 | EnvCard2RealCard = {3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 14 | 8: '8', 9: '9', 10: 'T', 11: 'J', 12: 'Q', 15 | 13: 'K', 14: 'A', 17: '2', 20: 'X', 30: 'D'} 16 | 17 | RealCard2EnvCard = {'3': 3, '4': 4, '5': 5, '6': 6, '7': 7, 18 | '8': 8, '9': 9, 'T': 10, 'J': 11, 'Q': 12, 19 | 'K': 13, 'A': 14, '2': 17, 'X': 20, 'D': 30} 20 | 21 | pretrained_dir = 'pretrained/douzero_pretrained' 22 | players = [] 23 | for position in ['landlord', 'landlord_down', 'landlord_up']: 24 | players.append(DeepAgent(position, pretrained_dir, use_onnx=True)) 25 | 26 | @app.route('/predict', methods=['POST']) 27 | def predict(): 28 | if request.method == 'POST': 29 | try: 30 | # Player postion 31 | player_position = request.form.get('player_position') 32 | if player_position not in ['0', '1', '2']: 33 | return jsonify({'status': 1, 'message': 'player_position must be 0, 1, or 2'}) 34 | player_position = int(player_position) 35 | 36 | # Player hand cards 37 | player_hand_cards = [RealCard2EnvCard[c] for c in request.form.get('player_hand_cards')] 38 | if player_position == 0: 39 | if len(player_hand_cards) < 1 or len(player_hand_cards) > 20: 40 | return jsonify({'status': 2, 'message': 'the number of hand cards should be 1-20'}) 41 | else: 42 | if len(player_hand_cards) < 1 or len(player_hand_cards) > 17: 43 | return jsonify({'status': 3, 'message': 'the number of hand cards should be 1-17'}) 44 | 45 | # Number cards left 46 | num_cards_left = [int(request.form.get('num_cards_left_landlord')), int(request.form.get('num_cards_left_landlord_down')), int(request.form.get('num_cards_left_landlord_up'))] 47 | if num_cards_left[player_position] != len(player_hand_cards): 48 | return jsonify({'status': 4, 'message': 'the number of cards left do not align with hand cards'}) 49 | if num_cards_left[0] < 0 or num_cards_left[1] < 0 or num_cards_left[2] < 0 or num_cards_left[0] > 20 or num_cards_left[1] > 17 or num_cards_left[2] > 17: 50 | return jsonify({'status': 5, 'message': 'the number of cards left not in range'}) 51 | 52 | # Three landlord cards 53 | three_landlord_cards = [RealCard2EnvCard[c] for c in request.form.get('three_landlord_cards')] 54 | if len(three_landlord_cards) < 0 or len(three_landlord_cards) > 3: 55 | return jsonify({'status': 6, 'message': 'the number of landlord cards should be 0-3'}) 56 | 57 | # Card play sequence 58 | if request.form.get('card_play_action_seq') == '': 59 | card_play_action_seq = [] 60 | else: 61 | card_play_action_seq = [[RealCard2EnvCard[c] for c in cards] for cards in request.form.get('card_play_action_seq').split(',')] 62 | 63 | # Other hand cards 64 | other_hand_cards = [RealCard2EnvCard[c] for c in request.form.get('other_hand_cards')] 65 | if len(other_hand_cards) != sum(num_cards_left) - num_cards_left[player_position]: 66 | return jsonify({'status': 7, 'message': 'the number of the other hand cards do not align with the number of cards left'}) 67 | 68 | # Last moves 69 | last_moves = [] 70 | for field in ['last_move_landlord', 'last_move_landlord_down', 'last_move_landlord_up']: 71 | last_moves.append([RealCard2EnvCard[c] for c in request.form.get(field)]) 72 | 73 | # Played cards 74 | played_cards = [] 75 | for field in ['played_cards_landlord', 'played_cards_landlord_down', 'played_cards_landlord_up']: 76 | played_cards.append([RealCard2EnvCard[c] for c in request.form.get(field)]) 77 | 78 | # Bomb Num 79 | bomb_num = int(request.form.get('bomb_num')) 80 | 81 | # InfoSet 82 | info_set = InfoSet() 83 | info_set.player_position = player_position 84 | info_set.player_hand_cards = player_hand_cards 85 | info_set.num_cards_left = num_cards_left 86 | info_set.three_landlord_cards = three_landlord_cards 87 | info_set.card_play_action_seq = card_play_action_seq 88 | info_set.other_hand_cards = other_hand_cards 89 | info_set.last_moves = last_moves 90 | info_set.played_cards = played_cards 91 | info_set.bomb_num = bomb_num 92 | 93 | # Get rival move and legal_actions 94 | rival_move = [] 95 | if len(card_play_action_seq) != 0: 96 | if len(card_play_action_seq[-1]) == 0: 97 | rival_move = card_play_action_seq[-2] 98 | else: 99 | rival_move = card_play_action_seq[-1] 100 | info_set.rival_move = rival_move 101 | info_set.legal_actions = _get_legal_card_play_actions(player_hand_cards, rival_move) 102 | 103 | # Prediction 104 | actions, actions_confidence = players[player_position].act(info_set) 105 | actions = [''.join([EnvCard2RealCard[a] for a in action]) for action in actions] 106 | result = {} 107 | win_rates = {} 108 | for i in range(len(actions)): 109 | # Here, we calculate the win rate 110 | win_rate = max(actions_confidence[i], -1) 111 | win_rate = min(win_rate, 1) 112 | win_rates[actions[i]] = str(round((win_rate + 1) / 2, 4)) 113 | result[actions[i]] = str(round(actions_confidence[i], 6)) 114 | 115 | ############## DEBUG ################ 116 | if app.debug: 117 | print('--------------- DEBUG START --------------') 118 | command = 'curl --data "' 119 | parameters = [] 120 | for key in request.form: 121 | parameters.append(key+'='+request.form.get(key)) 122 | print(key+':', request.form.get(key)) 123 | command += '&'.join(parameters) 124 | command += '" "http://127.0.0.1:5000/predict"' 125 | print('Command:', command) 126 | print('Rival Move:', rival_move) 127 | print('legal_actions:', info_set.legal_actions) 128 | print('Result:', result) 129 | print('--------------- DEBUG END --------------') 130 | ############## DEBUG ################ 131 | return jsonify({'status': 0, 'message': 'success', 'result': result, 'win_rates': win_rates}) 132 | except: 133 | import traceback 134 | traceback.print_exc() 135 | return jsonify({'status': -1, 'message': 'unkown error'}) 136 | 137 | @app.route('/legal', methods=['POST']) 138 | def legal(): 139 | if request.method == 'POST': 140 | try: 141 | player_hand_cards = [RealCard2EnvCard[c] for c in request.form.get('player_hand_cards')] 142 | rival_move = [RealCard2EnvCard[c] for c in request.form.get('rival_move')] 143 | legal_actions = _get_legal_card_play_actions(player_hand_cards, rival_move) 144 | legal_actions = ','.join([''.join([EnvCard2RealCard[a] for a in action]) for action in legal_actions]) 145 | return jsonify({'status': 0, 'message': 'success', 'legal_action': legal_actions}) 146 | except: 147 | import traceback 148 | traceback.print_exc() 149 | return jsonify({'status': -1, 'message': 'unkown error'}) 150 | 151 | class InfoSet(object): 152 | 153 | def __init__(self): 154 | self.player_position = None 155 | self.player_hand_cards = None 156 | self.num_cards_left = None 157 | self.three_landlord_cards = None 158 | self.card_play_action_seq = None 159 | self.other_hand_cards = None 160 | self.legal_actions = None 161 | self.rival_move = None 162 | self.last_moves = None 163 | self.played_cards = None 164 | self.bomb_num = None 165 | 166 | def _get_legal_card_play_actions(player_hand_cards, rival_move): 167 | mg = MovesGener(player_hand_cards) 168 | 169 | rival_type = md.get_move_type(rival_move) 170 | rival_move_type = rival_type['type'] 171 | rival_move_len = rival_type.get('len', 1) 172 | moves = list() 173 | 174 | if rival_move_type == md.TYPE_0_PASS: 175 | moves = mg.gen_moves() 176 | 177 | elif rival_move_type == md.TYPE_1_SINGLE: 178 | all_moves = mg.gen_type_1_single() 179 | moves = ms.filter_type_1_single(all_moves, rival_move) 180 | 181 | elif rival_move_type == md.TYPE_2_PAIR: 182 | all_moves = mg.gen_type_2_pair() 183 | moves = ms.filter_type_2_pair(all_moves, rival_move) 184 | 185 | elif rival_move_type == md.TYPE_3_TRIPLE: 186 | all_moves = mg.gen_type_3_triple() 187 | moves = ms.filter_type_3_triple(all_moves, rival_move) 188 | 189 | elif rival_move_type == md.TYPE_4_BOMB: 190 | all_moves = mg.gen_type_4_bomb() + mg.gen_type_5_king_bomb() 191 | moves = ms.filter_type_4_bomb(all_moves, rival_move) 192 | 193 | elif rival_move_type == md.TYPE_5_KING_BOMB: 194 | moves = [] 195 | 196 | elif rival_move_type == md.TYPE_6_3_1: 197 | all_moves = mg.gen_type_6_3_1() 198 | moves = ms.filter_type_6_3_1(all_moves, rival_move) 199 | 200 | elif rival_move_type == md.TYPE_7_3_2: 201 | all_moves = mg.gen_type_7_3_2() 202 | moves = ms.filter_type_7_3_2(all_moves, rival_move) 203 | 204 | elif rival_move_type == md.TYPE_8_SERIAL_SINGLE: 205 | all_moves = mg.gen_type_8_serial_single(repeat_num=rival_move_len) 206 | moves = ms.filter_type_8_serial_single(all_moves, rival_move) 207 | 208 | elif rival_move_type == md.TYPE_9_SERIAL_PAIR: 209 | all_moves = mg.gen_type_9_serial_pair(repeat_num=rival_move_len) 210 | moves = ms.filter_type_9_serial_pair(all_moves, rival_move) 211 | 212 | elif rival_move_type == md.TYPE_10_SERIAL_TRIPLE: 213 | all_moves = mg.gen_type_10_serial_triple(repeat_num=rival_move_len) 214 | moves = ms.filter_type_10_serial_triple(all_moves, rival_move) 215 | 216 | elif rival_move_type == md.TYPE_11_SERIAL_3_1: 217 | all_moves = mg.gen_type_11_serial_3_1(repeat_num=rival_move_len) 218 | moves = ms.filter_type_11_serial_3_1(all_moves, rival_move) 219 | 220 | elif rival_move_type == md.TYPE_12_SERIAL_3_2: 221 | all_moves = mg.gen_type_12_serial_3_2(repeat_num=rival_move_len) 222 | moves = ms.filter_type_12_serial_3_2(all_moves, rival_move) 223 | 224 | elif rival_move_type == md.TYPE_13_4_2: 225 | all_moves = mg.gen_type_13_4_2() 226 | moves = ms.filter_type_13_4_2(all_moves, rival_move) 227 | 228 | elif rival_move_type == md.TYPE_14_4_22: 229 | all_moves = mg.gen_type_14_4_22() 230 | moves = ms.filter_type_14_4_22(all_moves, rival_move) 231 | 232 | if rival_move_type not in [md.TYPE_0_PASS, 233 | md.TYPE_4_BOMB, md.TYPE_5_KING_BOMB]: 234 | moves = moves + mg.gen_type_4_bomb() + mg.gen_type_5_king_bomb() 235 | 236 | if len(rival_move) != 0: # rival_move is not 'pass' 237 | moves = moves + [[]] 238 | 239 | for m in moves: 240 | m.sort() 241 | 242 | moves.sort() 243 | moves = list(move for move,_ in itertools.groupby(moves)) 244 | 245 | return moves 246 | 247 | if __name__ == '__main__': 248 | import argparse 249 | parser = argparse.ArgumentParser(description='DouZero backend') 250 | parser.add_argument('--debug', action='store_true') 251 | args = parser.parse_args() 252 | app.run(debug=args.debug) 253 | -------------------------------------------------------------------------------- /pve_server/utils/move_detector.py: -------------------------------------------------------------------------------- 1 | from .utils import * 2 | import collections 3 | 4 | # check if move is a continuous sequence 5 | def is_continuous_seq(move): 6 | i = 0 7 | while i < len(move) - 1: 8 | if move[i+1] - move[i] != 1: 9 | return False 10 | i += 1 11 | return True 12 | 13 | # return the type of the move 14 | def get_move_type(move): 15 | move_size = len(move) 16 | move_dict = collections.Counter(move) 17 | 18 | if move_size == 0: 19 | return {'type': TYPE_0_PASS} 20 | 21 | if move_size == 1: 22 | return {'type': TYPE_1_SINGLE, 'rank': move[0]} 23 | 24 | if move_size == 2: 25 | if move[0] == move[1]: 26 | return {'type': TYPE_2_PAIR, 'rank': move[0]} 27 | elif move == [20, 30]: # Kings 28 | return {'type': TYPE_5_KING_BOMB} 29 | else: 30 | return {'type': TYPE_15_WRONG} 31 | 32 | if move_size == 3: 33 | if len(move_dict) == 1: 34 | return {'type': TYPE_3_TRIPLE, 'rank': move[0]} 35 | else: 36 | return {'type': TYPE_15_WRONG} 37 | 38 | if move_size == 4: 39 | if len(move_dict) == 1: 40 | return {'type': TYPE_4_BOMB, 'rank': move[0]} 41 | elif len(move_dict) == 2: 42 | if move[0] == move[1] == move[2] or move[1] == move[2] == move[3]: 43 | return {'type': TYPE_6_3_1, 'rank': move[1]} 44 | else: 45 | return {'type': TYPE_15_WRONG} 46 | else: 47 | return {'type': TYPE_15_WRONG} 48 | 49 | if is_continuous_seq(move): 50 | return {'type': TYPE_8_SERIAL_SINGLE, 'rank': move[0], 'len': len(move)} 51 | 52 | if move_size == 5: 53 | if len(move_dict) == 2: 54 | return {'type': TYPE_7_3_2, 'rank': move[2]} 55 | else: 56 | return {'type': TYPE_15_WRONG} 57 | 58 | count_dict = collections.defaultdict(int) 59 | for c, n in move_dict.items(): 60 | count_dict[n] += 1 61 | 62 | if move_size == 6: 63 | if (len(move_dict) == 2 or len(move_dict) == 3) and count_dict.get(4) == 1 and \ 64 | (count_dict.get(2) == 1 or count_dict.get(1) == 2): 65 | return {'type': TYPE_13_4_2, 'rank': move[2]} 66 | 67 | if move_size == 8 and (((len(move_dict) == 3 or len(move_dict) == 2) and 68 | (count_dict.get(4) == 1 and count_dict.get(2) == 2)) or count_dict.get(4) == 2): 69 | return {'type': TYPE_14_4_22, 'rank': max([c for c, n in move_dict.items() if n == 4])} 70 | 71 | mdkeys = sorted(move_dict.keys()) 72 | if len(move_dict) == count_dict.get(2) and is_continuous_seq(mdkeys): 73 | return {'type': TYPE_9_SERIAL_PAIR, 'rank': mdkeys[0], 'len': len(mdkeys)} 74 | 75 | if len(move_dict) == count_dict.get(3) and is_continuous_seq(mdkeys): 76 | return {'type': TYPE_10_SERIAL_TRIPLE, 'rank': mdkeys[0], 'len': len(mdkeys)} 77 | 78 | # Check Type 11 (serial 3+1) and Type 12 (serial 3+2) 79 | if count_dict.get(3, 0) >= MIN_TRIPLES: 80 | serial_3 = list() 81 | single = list() 82 | pair = list() 83 | 84 | for k, v in move_dict.items(): 85 | if v == 3: 86 | serial_3.append(k) 87 | elif v == 1: 88 | single.append(k) 89 | elif v == 2: 90 | pair.append(k) 91 | else: # no other possibilities 92 | return {'type': TYPE_15_WRONG} 93 | 94 | serial_3.sort() 95 | if is_continuous_seq(serial_3): 96 | if len(serial_3) == len(single)+len(pair)*2: 97 | return {'type': TYPE_11_SERIAL_3_1, 'rank': serial_3[0], 'len': len(serial_3)} 98 | if len(serial_3) == len(pair) and len(move_dict) == len(serial_3) * 2: 99 | return {'type': TYPE_12_SERIAL_3_2, 'rank': serial_3[0], 'len': len(serial_3)} 100 | 101 | if len(serial_3) == 4: 102 | if is_continuous_seq(serial_3[1:]): 103 | return {'type': TYPE_11_SERIAL_3_1, 'rank': serial_3[1], 'len': len(serial_3) - 1} 104 | if is_continuous_seq(serial_3[:-1]): 105 | return {'type': TYPE_11_SERIAL_3_1, 'rank': serial_3[0], 'len': len(serial_3) - 1} 106 | 107 | return {'type': TYPE_15_WRONG} 108 | -------------------------------------------------------------------------------- /pve_server/utils/move_generator.py: -------------------------------------------------------------------------------- 1 | from .utils import MIN_SINGLE_CARDS, MIN_PAIRS, MIN_TRIPLES, select 2 | import collections 3 | import itertools 4 | 5 | class MovesGener(object): 6 | 7 | def __init__(self, cards_list): 8 | self.cards_list = cards_list 9 | self.cards_dict = collections.defaultdict(int) 10 | 11 | for i in self.cards_list: 12 | self.cards_dict[i] += 1 13 | 14 | self.single_card_moves = [] 15 | self.gen_type_1_single() 16 | self.pair_moves = [] 17 | self.gen_type_2_pair() 18 | self.triple_cards_moves = [] 19 | self.gen_type_3_triple() 20 | self.bomb_moves = [] 21 | self.gen_type_4_bomb() 22 | self.final_bomb_moves = [] 23 | self.gen_type_5_king_bomb() 24 | 25 | def _gen_serial_moves(self, cards, min_serial, repeat=1, repeat_num=0): 26 | if repeat_num < min_serial: # at least repeat_num is min_serial 27 | repeat_num = 0 28 | 29 | single_cards = sorted(list(set(cards))) 30 | seq_records = list() 31 | moves = list() 32 | 33 | start = i = 0 34 | longest = 1 35 | while i < len(single_cards): 36 | if i + 1 < len(single_cards) and single_cards[i + 1] - single_cards[i] == 1: 37 | longest += 1 38 | i += 1 39 | else: 40 | seq_records.append((start, longest)) 41 | i += 1 42 | start = i 43 | longest = 1 44 | 45 | for seq in seq_records: 46 | if seq[1] < min_serial: 47 | continue 48 | start, longest = seq[0], seq[1] 49 | longest_list = single_cards[start: start + longest] 50 | 51 | if repeat_num == 0: # No limitation on how many sequences 52 | steps = min_serial 53 | while steps <= longest: 54 | index = 0 55 | while steps + index <= longest: 56 | target_moves = sorted(longest_list[index: index + steps] * repeat) 57 | moves.append(target_moves) 58 | index += 1 59 | steps += 1 60 | 61 | else: # repeat_num > 0 62 | if longest < repeat_num: 63 | continue 64 | index = 0 65 | while index + repeat_num <= longest: 66 | target_moves = sorted(longest_list[index: index + repeat_num] * repeat) 67 | moves.append(target_moves) 68 | index += 1 69 | 70 | return moves 71 | 72 | def gen_type_1_single(self): 73 | self.single_card_moves = [] 74 | for i in set(self.cards_list): 75 | self.single_card_moves.append([i]) 76 | return self.single_card_moves 77 | 78 | def gen_type_2_pair(self): 79 | self.pair_moves = [] 80 | for k, v in self.cards_dict.items(): 81 | if v >= 2: 82 | self.pair_moves.append([k, k]) 83 | return self.pair_moves 84 | 85 | def gen_type_3_triple(self): 86 | self.triple_cards_moves = [] 87 | for k, v in self.cards_dict.items(): 88 | if v >= 3: 89 | self.triple_cards_moves.append([k, k, k]) 90 | return self.triple_cards_moves 91 | 92 | def gen_type_4_bomb(self): 93 | self.bomb_moves = [] 94 | for k, v in self.cards_dict.items(): 95 | if v == 4: 96 | self.bomb_moves.append([k, k, k, k]) 97 | return self.bomb_moves 98 | 99 | def gen_type_5_king_bomb(self): 100 | self.final_bomb_moves = [] 101 | if 20 in self.cards_list and 30 in self.cards_list: 102 | self.final_bomb_moves.append([20, 30]) 103 | return self.final_bomb_moves 104 | 105 | def gen_type_6_3_1(self): 106 | result = [] 107 | for t in self.single_card_moves: 108 | for i in self.triple_cards_moves: 109 | if t[0] != i[0]: 110 | result.append(t+i) 111 | return result 112 | 113 | def gen_type_7_3_2(self): 114 | result = list() 115 | for t in self.pair_moves: 116 | for i in self.triple_cards_moves: 117 | if t[0] != i[0]: 118 | result.append(t+i) 119 | return result 120 | 121 | def gen_type_8_serial_single(self, repeat_num=0): 122 | return self._gen_serial_moves(self.cards_list, MIN_SINGLE_CARDS, repeat=1, repeat_num=repeat_num) 123 | 124 | def gen_type_9_serial_pair(self, repeat_num=0): 125 | single_pairs = list() 126 | for k, v in self.cards_dict.items(): 127 | if v >= 2: 128 | single_pairs.append(k) 129 | 130 | return self._gen_serial_moves(single_pairs, MIN_PAIRS, repeat=2, repeat_num=repeat_num) 131 | 132 | def gen_type_10_serial_triple(self, repeat_num=0): 133 | single_triples = list() 134 | for k, v in self.cards_dict.items(): 135 | if v >= 3: 136 | single_triples.append(k) 137 | 138 | return self._gen_serial_moves(single_triples, MIN_TRIPLES, repeat=3, repeat_num=repeat_num) 139 | 140 | def gen_type_11_serial_3_1(self, repeat_num=0): 141 | serial_3_moves = self.gen_type_10_serial_triple(repeat_num=repeat_num) 142 | serial_3_1_moves = list() 143 | 144 | for s3 in serial_3_moves: # s3 is like [3,3,3,4,4,4] 145 | s3_set = set(s3) 146 | new_cards = [i for i in self.cards_list if i not in s3_set] 147 | 148 | # Get any s3_len items from cards 149 | subcards = select(new_cards, len(s3_set)) 150 | 151 | for i in subcards: 152 | serial_3_1_moves.append(s3 + i) 153 | 154 | return list(k for k, _ in itertools.groupby(serial_3_1_moves)) 155 | 156 | def gen_type_12_serial_3_2(self, repeat_num=0): 157 | serial_3_moves = self.gen_type_10_serial_triple(repeat_num=repeat_num) 158 | serial_3_2_moves = list() 159 | pair_set = sorted([k for k, v in self.cards_dict.items() if v >= 2]) 160 | 161 | for s3 in serial_3_moves: 162 | s3_set = set(s3) 163 | pair_candidates = [i for i in pair_set if i not in s3_set] 164 | 165 | # Get any s3_len items from cards 166 | subcards = select(pair_candidates, len(s3_set)) 167 | for i in subcards: 168 | serial_3_2_moves.append(sorted(s3 + i * 2)) 169 | 170 | return serial_3_2_moves 171 | 172 | def gen_type_13_4_2(self): 173 | four_cards = list() 174 | for k, v in self.cards_dict.items(): 175 | if v == 4: 176 | four_cards.append(k) 177 | 178 | result = list() 179 | for fc in four_cards: 180 | cards_list = [k for k in self.cards_list if k != fc] 181 | subcards = select(cards_list, 2) 182 | for i in subcards: 183 | result.append([fc]*4 + i) 184 | return list(k for k, _ in itertools.groupby(result)) 185 | 186 | def gen_type_14_4_22(self): 187 | four_cards = list() 188 | for k, v in self.cards_dict.items(): 189 | if v == 4: 190 | four_cards.append(k) 191 | 192 | result = list() 193 | for fc in four_cards: 194 | cards_list = [k for k, v in self.cards_dict.items() if k != fc and v>=2] 195 | subcards = select(cards_list, 2) 196 | for i in subcards: 197 | result.append([fc] * 4 + [i[0], i[0], i[1], i[1]]) 198 | return result 199 | 200 | # generate all possible moves from given cards 201 | def gen_moves(self): 202 | moves = [] 203 | moves.extend(self.gen_type_1_single()) 204 | moves.extend(self.gen_type_2_pair()) 205 | moves.extend(self.gen_type_3_triple()) 206 | moves.extend(self.gen_type_4_bomb()) 207 | moves.extend(self.gen_type_5_king_bomb()) 208 | moves.extend(self.gen_type_6_3_1()) 209 | moves.extend(self.gen_type_7_3_2()) 210 | moves.extend(self.gen_type_8_serial_single()) 211 | moves.extend(self.gen_type_9_serial_pair()) 212 | moves.extend(self.gen_type_10_serial_triple()) 213 | moves.extend(self.gen_type_11_serial_3_1()) 214 | moves.extend(self.gen_type_12_serial_3_2()) 215 | moves.extend(self.gen_type_13_4_2()) 216 | moves.extend(self.gen_type_14_4_22()) 217 | return moves 218 | -------------------------------------------------------------------------------- /pve_server/utils/move_selector.py: -------------------------------------------------------------------------------- 1 | # return all moves that can beat rivals, moves and rival_move should be same type 2 | import collections 3 | 4 | 5 | def common_handle(moves, rival_move): 6 | new_moves = list() 7 | for move in moves: 8 | if move[0] > rival_move[0]: 9 | new_moves.append(move) 10 | return new_moves 11 | 12 | 13 | def filter_type_1_single(moves, rival_move): 14 | return common_handle(moves, rival_move) 15 | 16 | 17 | def filter_type_2_pair(moves, rival_move): 18 | return common_handle(moves, rival_move) 19 | 20 | 21 | def filter_type_3_triple(moves, rival_move): 22 | return common_handle(moves, rival_move) 23 | 24 | 25 | def filter_type_4_bomb(moves, rival_move): 26 | return common_handle(moves, rival_move) 27 | 28 | 29 | # No need to filter for type_5_king_bomb 30 | 31 | def filter_type_6_3_1(moves, rival_move): 32 | rival_move.sort() 33 | rival_rank = rival_move[1] 34 | new_moves = list() 35 | for move in moves: 36 | move.sort() 37 | my_rank = move[1] 38 | if my_rank > rival_rank: 39 | new_moves.append(move) 40 | return new_moves 41 | 42 | 43 | def filter_type_7_3_2(moves, rival_move): 44 | rival_move.sort() 45 | rival_rank = rival_move[2] 46 | new_moves = list() 47 | for move in moves: 48 | move.sort() 49 | my_rank = move[2] 50 | if my_rank > rival_rank: 51 | new_moves.append(move) 52 | return new_moves 53 | 54 | 55 | def filter_type_8_serial_single(moves, rival_move): 56 | return common_handle(moves, rival_move) 57 | 58 | 59 | def filter_type_9_serial_pair(moves, rival_move): 60 | return common_handle(moves, rival_move) 61 | 62 | 63 | def filter_type_10_serial_triple(moves, rival_move): 64 | return common_handle(moves, rival_move) 65 | 66 | 67 | def filter_type_11_serial_3_1(moves, rival_move): 68 | rival = collections.Counter(rival_move) 69 | rival_rank = max([k for k, v in rival.items() if v == 3]) 70 | new_moves = list() 71 | for move in moves: 72 | mymove = collections.Counter(move) 73 | my_rank = max([k for k, v in mymove.items() if v == 3]) 74 | if my_rank > rival_rank: 75 | new_moves.append(move) 76 | return new_moves 77 | 78 | 79 | def filter_type_12_serial_3_2(moves, rival_move): 80 | rival = collections.Counter(rival_move) 81 | rival_rank = max([k for k, v in rival.items() if v == 3]) 82 | new_moves = list() 83 | for move in moves: 84 | mymove = collections.Counter(move) 85 | my_rank = max([k for k, v in mymove.items() if v == 3]) 86 | if my_rank > rival_rank: 87 | new_moves.append(move) 88 | return new_moves 89 | 90 | 91 | def filter_type_13_4_2(moves, rival_move): 92 | rival_move.sort() 93 | rival_rank = rival_move[2] 94 | new_moves = list() 95 | for move in moves: 96 | move.sort() 97 | my_rank = move[2] 98 | if my_rank > rival_rank: 99 | new_moves.append(move) 100 | return new_moves 101 | 102 | 103 | def filter_type_14_4_22(moves, rival_move): 104 | rival = collections.Counter(rival_move) 105 | rival_rank = my_rank = 0 106 | for k, v in rival.items(): 107 | if v == 4: 108 | rival_rank = k 109 | new_moves = list() 110 | for move in moves: 111 | mymove = collections.Counter(move) 112 | for k, v in mymove.items(): 113 | if v == 4: 114 | my_rank = k 115 | if my_rank > rival_rank: 116 | new_moves.append(move) 117 | return new_moves 118 | -------------------------------------------------------------------------------- /pve_server/utils/utils.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | # global parameters 4 | MIN_SINGLE_CARDS = 5 5 | MIN_PAIRS = 3 6 | MIN_TRIPLES = 2 7 | 8 | # action types 9 | TYPE_0_PASS = 0 10 | TYPE_1_SINGLE = 1 11 | TYPE_2_PAIR = 2 12 | TYPE_3_TRIPLE = 3 13 | TYPE_4_BOMB = 4 14 | TYPE_5_KING_BOMB = 5 15 | TYPE_6_3_1 = 6 16 | TYPE_7_3_2 = 7 17 | TYPE_8_SERIAL_SINGLE = 8 18 | TYPE_9_SERIAL_PAIR = 9 19 | TYPE_10_SERIAL_TRIPLE = 10 20 | TYPE_11_SERIAL_3_1 = 11 21 | TYPE_12_SERIAL_3_2 = 12 22 | TYPE_13_4_2 = 13 23 | TYPE_14_4_22 = 14 24 | TYPE_15_WRONG = 15 25 | 26 | # betting round action 27 | PASS = 0 28 | CALL = 1 29 | RAISE = 2 30 | 31 | # return all possible results of selecting num cards from cards list 32 | def select(cards, num): 33 | return [list(i) for i in itertools.combinations(cards, num)] 34 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | rlcard[torch] 2 | Django 3 | tqdm 4 | django-cors-headers 5 | flask==1.1 6 | flask-cors 7 | onnx 8 | onnxruntime 9 | 10 | -------------------------------------------------------------------------------- /server/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /server/media/example_agents/leduc_holdem_dqn.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/server/media/example_agents/leduc_holdem_dqn.zip -------------------------------------------------------------------------------- /server/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/server/server/__init__.py -------------------------------------------------------------------------------- /server/server/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for server project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.12. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '-t@mf4fi)gfxzv5lm8qkg)*5u^brj--y*ul2&ryqdem(xin8(!' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | # Cors config 31 | CORS_ORIGIN_ALLOW_ALL=True 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | 'django.contrib.admin', 37 | 'django.contrib.auth', 38 | 'django.contrib.contenttypes', 39 | 'django.contrib.sessions', 40 | 'django.contrib.messages', 41 | 'django.contrib.staticfiles', 42 | 'corsheaders', 43 | 'tournament', 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | 'corsheaders.middleware.CorsMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'server.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'server.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | 109 | # Internationalization 110 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 111 | 112 | LANGUAGE_CODE = 'en-us' 113 | 114 | TIME_ZONE = 'UTC' 115 | 116 | USE_I18N = True 117 | 118 | USE_L10N = True 119 | 120 | USE_TZ = True 121 | 122 | 123 | # Static files (CSS, JavaScript, Images) 124 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 125 | 126 | STATIC_URL = '/static/' 127 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 128 | 129 | -------------------------------------------------------------------------------- /server/server/urls.py: -------------------------------------------------------------------------------- 1 | """server URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import include, path 18 | 19 | urlpatterns = [ 20 | path('tournament/', include('tournament.urls')), 21 | path('admin/', admin.site.urls), 22 | ] 23 | -------------------------------------------------------------------------------- /server/server/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for server project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /server/tournament/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/server/tournament/__init__.py -------------------------------------------------------------------------------- /server/tournament/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /server/tournament/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TournamentConfig(AppConfig): 5 | name = 'tournament' 6 | -------------------------------------------------------------------------------- /server/tournament/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.4 on 2020-12-11 17:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Game', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=100)), 19 | ('index', models.CharField(max_length=100)), 20 | ('agent0', models.CharField(max_length=100)), 21 | ('agent1', models.CharField(max_length=100)), 22 | ('win', models.BooleanField()), 23 | ('payoff', models.FloatField()), 24 | ('replay', models.TextField(blank=True)), 25 | ], 26 | ), 27 | migrations.CreateModel( 28 | name='Payoff', 29 | fields=[ 30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('name', models.CharField(max_length=100)), 32 | ('agent0', models.CharField(max_length=100)), 33 | ('agent1', models.CharField(max_length=100)), 34 | ('payoff', models.FloatField()), 35 | ], 36 | ), 37 | migrations.CreateModel( 38 | name='UploadedAgent', 39 | fields=[ 40 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 41 | ('name', models.CharField(max_length=100)), 42 | ('game', models.CharField(max_length=100)), 43 | ('f', models.FileField(upload_to='uploaded_agents')), 44 | ], 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /server/tournament/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/server/tournament/migrations/__init__.py -------------------------------------------------------------------------------- /server/tournament/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | 4 | class Game(models.Model): 5 | # The name of the game 6 | name = models.CharField(max_length=100) 7 | 8 | # The ID of repeated games 9 | index = models.CharField(max_length=100) 10 | 11 | # The first agent 12 | agent0 = models.CharField(max_length=100) 13 | 14 | # The second agent 15 | agent1 = models.CharField(max_length=100) 16 | 17 | # Whether the first agent wins 18 | win = models.BooleanField() 19 | 20 | # The payoff of the first agent 21 | payoff = models.FloatField() 22 | 23 | # The JSON file 24 | replay = models.TextField(blank=True) 25 | 26 | class Payoff(models.Model): 27 | # The name of the game 28 | name = models.CharField(max_length=100) 29 | 30 | # The first agent 31 | agent0 = models.CharField(max_length=100) 32 | 33 | # The second agent 34 | agent1 = models.CharField(max_length=100) 35 | 36 | # The average payoff of the first agent 37 | payoff = models.FloatField() 38 | 39 | class UploadedAgent(models.Model): 40 | # The name of the agent 41 | name = models.CharField(max_length=100) 42 | 43 | # The game of the agent 44 | game = models.CharField(max_length=100) 45 | 46 | # File 47 | f = models.FileField(upload_to='uploaded_agents') 48 | 49 | -------------------------------------------------------------------------------- /server/tournament/rlcard_wrap/__init__.py: -------------------------------------------------------------------------------- 1 | import rlcard 2 | from .leduc_holdem_random_model import LeducHoldemRandomModelSpec 3 | from .doudizhu_random_model import DoudizhuRandomModelSpec 4 | 5 | 6 | # Register Leduc Holdem Random Model 7 | rlcard.models.registration.model_registry.model_specs['leduc-holdem-random'] = LeducHoldemRandomModelSpec() 8 | 9 | # Register Doudizhu Random Model 10 | rlcard.models.registration.model_registry.model_specs['doudizhu-random'] = DoudizhuRandomModelSpec() 11 | 12 | # The models we are concerned 13 | MODEL_IDS = {} 14 | MODEL_IDS['leduc-holdem'] = [ 15 | 'leduc-holdem-random', 16 | 'leduc-holdem-cfr', 17 | 'leduc-holdem-rule-v1', 18 | ] 19 | 20 | MODEL_IDS['doudizhu'] = [ 21 | 'doudizhu-random', 22 | 'doudizhu-rule-v1', 23 | ] 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /server/tournament/rlcard_wrap/doudizhu_random_model.py: -------------------------------------------------------------------------------- 1 | # A wrap for rlcard 2 | # Here, we include a random model as the default baseline 3 | import rlcard 4 | from rlcard.agents import RandomAgent 5 | from rlcard.models.model import Model 6 | 7 | class DoudizhuRandomModelSpec(object): 8 | def __init__(self): 9 | self.model_id = 'doudizhu-random' 10 | self._entry_point = DoudizhuRandomModel 11 | 12 | def load(self): 13 | model = self._entry_point() 14 | return model 15 | 16 | class DoudizhuRandomModel(Model): 17 | ''' A random model 18 | ''' 19 | 20 | def __init__(self): 21 | ''' Load random model 22 | ''' 23 | env = rlcard.make('doudizhu') 24 | self.agent = RandomAgent(num_actions=env.num_actions) 25 | self.num_players = env.num_players 26 | 27 | @property 28 | def agents(self): 29 | ''' Get a list of agents for each position in a the game 30 | 31 | Returns: 32 | agents (list): A list of agents 33 | 34 | Note: Each agent should be just like RL agent with step and eval_step 35 | functioning well. 36 | ''' 37 | return [self.agent for _ in range(self.num_players)] 38 | 39 | @property 40 | def use_raw(self): 41 | ''' Indicate whether use raw state and action 42 | 43 | Returns: 44 | use_raw (boolean): True if using raw state and action 45 | ''' 46 | return False 47 | -------------------------------------------------------------------------------- /server/tournament/rlcard_wrap/leduc_holdem_random_model.py: -------------------------------------------------------------------------------- 1 | # A wrap for rlcard 2 | # Here, we include a random model as the default baseline 3 | import rlcard 4 | from rlcard.agents import RandomAgent 5 | from rlcard.models.model import Model 6 | 7 | class LeducHoldemRandomModelSpec(object): 8 | def __init__(self): 9 | self.model_id = 'leduc-holdem-random' 10 | self._entry_point = LeducHoldemRandomModel 11 | 12 | def load(self): 13 | model = self._entry_point() 14 | return model 15 | 16 | class LeducHoldemRandomModel(Model): 17 | ''' A random model 18 | ''' 19 | 20 | def __init__(self): 21 | ''' Load random model 22 | ''' 23 | env = rlcard.make('leduc-holdem') 24 | self.agent = RandomAgent(num_actions=env.num_actions) 25 | self.num_players = env.num_players 26 | 27 | @property 28 | def agents(self): 29 | ''' Get a list of agents for each position in a the game 30 | 31 | Returns: 32 | agents (list): A list of agents 33 | 34 | Note: Each agent should be just like RL agent with step and eval_step 35 | functioning well. 36 | ''' 37 | return [self.agent for _ in range(self.num_players)] 38 | 39 | @property 40 | def use_raw(self): 41 | ''' Indicate whether use raw state and action 42 | 43 | Returns: 44 | use_raw (boolean): True if using raw state and action 45 | ''' 46 | return False 47 | -------------------------------------------------------------------------------- /server/tournament/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /server/tournament/tournament.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from tqdm import tqdm 4 | import numpy as np 5 | 6 | from .rlcard_wrap import rlcard 7 | 8 | class Tournament(object): 9 | 10 | def __init__(self, game, model_ids, num_eval_games=100): 11 | """ Default for two player games 12 | For Dou Dizhu, the two peasants use the same model 13 | """ 14 | self.game = game 15 | self.model_ids = model_ids 16 | self.num_eval_games = num_eval_games 17 | # Load the models 18 | self.models = [rlcard.models.load(model_id) for model_id in model_ids] 19 | 20 | def launch(self): 21 | """ Currently for two-player game only 22 | """ 23 | num_models = len(self.model_ids) 24 | games_data = [] 25 | payoffs_data = [] 26 | for i in range(num_models): 27 | for j in range(num_models): 28 | if j == i: 29 | continue 30 | print(self.game, '-', self.model_ids[i], 'VS', self.model_ids[j]) 31 | if self.game == 'doudizhu': 32 | agents = [self.models[i].agents[0], self.models[j].agents[1], self.models[j].agents[2]] 33 | names = [self.model_ids[i], self.model_ids[j], self.model_ids[j]] 34 | data, payoffs, wins = doudizhu_tournament(self.game, agents, names, self.num_eval_games) 35 | elif self.game == 'leduc-holdem': 36 | agents = [self.models[i].agents[0], self.models[j].agents[1]] 37 | names = [self.model_ids[i], self.model_ids[j]] 38 | data, payoffs, wins = leduc_holdem_tournament(self.game, agents, names, self.num_eval_games) 39 | mean_payoff = np.mean(payoffs) 40 | print('Average payoff:', mean_payoff) 41 | print() 42 | 43 | for k in range(len(data)): 44 | game_data = {} 45 | game_data['name'] = self.game 46 | game_data['index'] = k 47 | game_data['agent0'] = self.model_ids[i] 48 | game_data['agent1'] = self.model_ids[j] 49 | game_data['win'] = wins[k] 50 | game_data['replay'] = data[k] 51 | game_data['payoff'] = payoffs[k] 52 | 53 | games_data.append(game_data) 54 | 55 | payoff_data = {} 56 | payoff_data['name'] = self.game 57 | payoff_data['agent0'] = self.model_ids[i] 58 | payoff_data['agent1'] = self.model_ids[j] 59 | payoff_data['payoff'] = mean_payoff 60 | payoffs_data.append(payoff_data) 61 | return games_data, payoffs_data 62 | 63 | def doudizhu_tournament(game, agents, names, num_eval_games): 64 | env = rlcard.make(game, config={'allow_raw_data': True}) 65 | env.set_agents(agents) 66 | payoffs = [] 67 | json_data = [] 68 | wins = [] 69 | for _ in tqdm(range(num_eval_games)): 70 | data = {} 71 | roles = ['landlord', 'peasant', 'peasant'] 72 | data['playerInfo'] = [{'id': i, 'index': i, 'role': roles[i], 'agentInfo': {'name': names[i]}} for i in range(env.num_players)] 73 | state, player_id = env.reset() 74 | perfect = env.get_perfect_information() 75 | data['initHands'] = perfect['hand_cards_with_suit'] 76 | current_hand_cards = perfect['hand_cards_with_suit'].copy() 77 | for i in range(len(current_hand_cards)): 78 | current_hand_cards[i] = current_hand_cards[i].split() 79 | data['moveHistory'] = [] 80 | while not env.is_over(): 81 | action, info = env.agents[player_id].eval_step(state) 82 | history = {} 83 | history['playerIdx'] = player_id 84 | if env.agents[player_id].use_raw: 85 | _action = action 86 | else: 87 | _action = env._decode_action(action) 88 | history['move'] = _calculate_doudizhu_move(_action, player_id, current_hand_cards) 89 | history['info'] = info 90 | 91 | data['moveHistory'].append(history) 92 | state, player_id = env.step(action, env.agents[player_id].use_raw) 93 | data = json.dumps(str(data)) 94 | #data = json.dumps(data, indent=2, sort_keys=True) 95 | json_data.append(data) 96 | if env.get_payoffs()[0] > 0: 97 | wins.append(True) 98 | else: 99 | wins.append(False) 100 | payoffs.append(env.get_payoffs()[0]) 101 | return json_data, payoffs, wins 102 | 103 | def _calculate_doudizhu_move(action, player_id, current_hand_cards): 104 | if action == 'pass': 105 | return action 106 | trans = {'B': 'BJ', 'R': 'RJ'} 107 | cards_with_suit = [] 108 | for card in action: 109 | if card in trans: 110 | cards_with_suit.append(trans[card]) 111 | current_hand_cards[player_id].remove(trans[card]) 112 | else: 113 | for hand_card in current_hand_cards[player_id]: 114 | if hand_card[1] == card: 115 | cards_with_suit.append(hand_card) 116 | current_hand_cards[player_id].remove(hand_card) 117 | break 118 | return ' '.join(cards_with_suit) 119 | 120 | def leduc_holdem_tournament(game, agents, names, num_eval_games): 121 | env = rlcard.make(game, config={'allow_raw_data': True}) 122 | env.set_agents(agents) 123 | payoffs = [] 124 | json_data = [] 125 | wins = [] 126 | for _ in tqdm(range(num_eval_games)): 127 | data = {} 128 | data['playerInfo'] = [{'id': i, 'index': i, 'agentInfo': {'name': names[i]}} for i in range(env.num_players)] 129 | state, player_id = env.reset() 130 | perfect = env.get_perfect_information() 131 | data['initHands'] = perfect['hand_cards'] 132 | data['moveHistory'] = [] 133 | round_history = [] 134 | round_id = 0 135 | while not env.is_over(): 136 | action, info = env.agents[player_id].eval_step(state) 137 | history = {} 138 | history['playerIdx'] = player_id 139 | if env.agents[player_id].use_raw: 140 | history['move'] = action 141 | else: 142 | history['move'] = env._decode_action(action) 143 | 144 | history['info'] = info 145 | round_history.append(history) 146 | state, player_id = env.step(action, env.agents[player_id].use_raw) 147 | perfect = env.get_perfect_information() 148 | if round_id < perfect['current_round'] or env.is_over(): 149 | round_id = perfect['current_round'] 150 | data['moveHistory'].append(round_history) 151 | round_history = [] 152 | perfect = env.get_perfect_information() 153 | data['publicCard'] = perfect['public_card'] 154 | data = json.dumps(str(data)) 155 | #data = json.dumps(data, indent=2, sort_keys=True) 156 | json_data.append(data) 157 | if env.get_payoffs()[0] > 0: 158 | wins.append(True) 159 | else: 160 | wins.append(False) 161 | payoffs.append(env.get_payoffs()[0]) 162 | return json_data, payoffs, wins 163 | 164 | 165 | if __name__=='__main__': 166 | game = 'leduc-holdem' 167 | model_ids = ['leduc-holdem-random', 'leduc-holdem-rule-v1', 'leduc-holdem-cfr'] 168 | t = Tournament(game, model_ids) 169 | games_data = t.launch() 170 | print(len(games_data)) 171 | print(games_data[0]) 172 | #root_path = './models' 173 | #agent1 = LeducHoldemDQNModel1(root_path) 174 | #agent2 = LeducHoldemRandomModel(root_path) 175 | #agent3 = LeducHoldemRuleModel() 176 | #agent4 = LeducHoldemCFRModel(root_path) 177 | #agent5 = LeducHoldemDQNModel2(root_path) 178 | #t = Tournament(agent1, agent2, agent3, agent4, agent5, 'leduc-holdem') 179 | ##t.competition() 180 | #t.evaluate() 181 | -------------------------------------------------------------------------------- /server/tournament/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('replay', views.replay, name='replay'), 7 | path('launch', views.launch, name='launch'), 8 | path('query_agent_payoff', views.query_agent_payoff, name='query_agent_payoff'), 9 | path('query_payoff', views.query_payoff, name='query_payoff'), 10 | path('query_game', views.query_game, name='query_game'), 11 | path('upload_agent', views.upload_agent, name='upload_agent'), 12 | path('delete_agent', views.delete_agent, name='delete_agent'), 13 | path('list_uploaded_agents', views.list_uploaded_agents, name='list_uploaded_agents'), 14 | path('list_baseline_agents', views.list_baseline_agents, name='list_baseline_agents'), 15 | path('download_examples', views.download_examples, name='download_examples'), 16 | ] 17 | -------------------------------------------------------------------------------- /server/tournament/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import importlib.util 4 | import math 5 | import copy 6 | import zipfile 7 | import shutil 8 | 9 | from django.shortcuts import render 10 | from django.http import HttpResponse, Http404 11 | from django.db import transaction 12 | from django.core import serializers 13 | from django.views.decorators.csrf import csrf_exempt 14 | from django.dispatch import receiver 15 | from django.db import models 16 | from django.conf import settings 17 | from django.db.models import Avg 18 | 19 | from .rlcard_wrap import rlcard, MODEL_IDS 20 | from .models import Game, Payoff, UploadedAgent 21 | 22 | from .tournament import Tournament 23 | from .rlcard_wrap import rlcard, MODEL_IDS 24 | 25 | def _get_model_ids_all(): 26 | MODEL_IDS_ALL = copy.deepcopy(MODEL_IDS) 27 | agents = UploadedAgent.objects.all() 28 | for agent in agents: 29 | path = os.path.join(settings.MEDIA_ROOT, agent.f.name) 30 | name = agent.name 31 | game = agent.game 32 | target_path = os.path.join(os.path.abspath(os.path.join(path, os.pardir)), name) 33 | module_name = 'model' 34 | entry = 'Model' 35 | spec = importlib.util.spec_from_file_location(module_name, os.path.join(target_path, module_name+'.py')) 36 | module = importlib.util.module_from_spec(spec) 37 | spec.loader.exec_module(module) 38 | M = getattr(module, entry) 39 | 40 | class ModelSpec(object): 41 | def __init__(self): 42 | self.model_id = name 43 | self._entry_point = M 44 | self.target_path = target_path 45 | 46 | def load(self): 47 | model = self._entry_point(self.target_path) 48 | return model 49 | rlcard.models.registration.model_registry.model_specs[name] = ModelSpec() 50 | MODEL_IDS_ALL[game].append(name) 51 | return MODEL_IDS_ALL 52 | 53 | PAGE_FIELDS = ['elements_every_page', 'page_index'] 54 | 55 | def _get_page(result, elements_every_page, page_index): 56 | elements_every_page = int(elements_every_page) 57 | page_index = int(page_index) 58 | total_row = len(result) 59 | total_page = math.ceil(len(result) / float(elements_every_page)) 60 | begin = page_index * elements_every_page 61 | end = min((page_index+1) * elements_every_page, len(result)) 62 | result = result[begin:end] 63 | return result, total_page, total_row 64 | 65 | def replay(request): 66 | if request.method == 'GET': 67 | name = request.GET['name'] 68 | agent0 = request.GET['agent0'] 69 | agent1 = request.GET['agent1'] 70 | index = request.GET['index'] 71 | g = Game.objects.get(name=name, agent0=agent0, agent1=agent1, index=index) 72 | json_data = g.replay 73 | return HttpResponse(json.dumps(json.loads(json_data))) 74 | 75 | def query_game(request): 76 | if request.method == 'GET': 77 | if not PAGE_FIELDS[0] in request.GET or not PAGE_FIELDS[1] in request.GET: 78 | return HttpResponse(json.dumps({'value': -1, 'info': 'elements_every_page and page_index should be given'})) 79 | filter_dict = {key: request.GET.get(key) for key in dict(request.GET).keys() if key not in PAGE_FIELDS} 80 | result = Game.objects.filter(**filter_dict).order_by('index') 81 | result, total_page, total_row = _get_page(result, request.GET['elements_every_page'], request.GET['page_index']) 82 | result = serializers.serialize('json', result, fields=('name', 'index', 'agent0', 'agent1', 'win', 'payoff')) 83 | return HttpResponse(json.dumps({'value': 0, 'data': json.loads(result), 'total_page': total_page, 'total_row': total_row})) 84 | 85 | def query_payoff(request): 86 | if request.method == 'GET': 87 | filter_dict = {key: request.GET.get(key) for key in dict(request.GET).keys()} 88 | result = Payoff.objects.filter(**filter_dict) 89 | result = serializers.serialize('json', result) 90 | return HttpResponse(result) 91 | 92 | def query_agent_payoff(request): 93 | if request.method == 'GET': 94 | if not PAGE_FIELDS[0] in request.GET or not PAGE_FIELDS[1] in request.GET: 95 | return HttpResponse(json.dumps({'value': -1, 'info': 'elements_every_page and page_index should be given'})) 96 | if not 'name' in request.GET: 97 | return HttpResponse(json.dumps({'value': -2, 'info': 'name should be given'})) 98 | result = list(Payoff.objects.filter(name=request.GET['name']).values('agent0').annotate(payoff = Avg('payoff')).order_by('-payoff')) 99 | result, total_page, total_row = _get_page(result, request.GET['elements_every_page'], request.GET['page_index']) 100 | return HttpResponse(json.dumps({'value': 0, 'data': result, 'total_page': total_page, 'total_row': total_row})) 101 | 102 | @transaction.atomic 103 | def launch(request): 104 | if request.method == 'GET': 105 | try: 106 | num_eval_games = int(request.GET['num_eval_games']) 107 | game = request.GET['name'] 108 | except: 109 | return HttpResponse(json.dumps({'value': -1, 'info': 'parameters error'})) 110 | 111 | MODEL_IDS_ALL = _get_model_ids_all() 112 | games_data, payoffs_data = Tournament(game, MODEL_IDS_ALL[game], num_eval_games).launch() 113 | Game.objects.filter(name=game).delete() 114 | Payoff.objects.filter(name=game).delete() 115 | for game_data in games_data: 116 | g = Game(name=game_data['name'], 117 | index=game_data['index'], 118 | agent0=game_data['agent0'], 119 | agent1=game_data['agent1'], 120 | win=game_data['win'], 121 | payoff=game_data['payoff'], 122 | replay=game_data['replay']) 123 | g.save() 124 | for payoff_data in payoffs_data: 125 | p = Payoff(name=payoff_data['name'], 126 | agent0=payoff_data['agent0'], 127 | agent1=payoff_data['agent1'], 128 | payoff=payoff_data['payoff']) 129 | p.save() 130 | return HttpResponse(json.dumps({'value': 0, 'info': 'success'})) 131 | 132 | @csrf_exempt 133 | def upload_agent(request): 134 | if request.method == 'POST': 135 | f = request.FILES['model'] 136 | name = request.POST['name'] 137 | game = request.POST['game'] 138 | if name == '': 139 | return HttpResponse(json.dumps({'value': -1, 'info': 'name can not be empty'})) 140 | if game not in ['leduc-holdem', 'doudizhu']: 141 | return HttpResponse(json.dumps({'value': -2, 'info': 'game can only be leduc-holdem or doudizhu'})) 142 | if UploadedAgent.objects.filter(name=name).exists(): 143 | return HttpResponse(json.dumps({'value': -3, 'info': 'name exists'})) 144 | 145 | a = UploadedAgent(name=name, game=game, f=f) 146 | a.save() 147 | path = os.path.join(settings.MEDIA_ROOT, a.f.name) 148 | target_path = os.path.join(os.path.abspath(os.path.join(path, os.pardir)), name) 149 | with zipfile.ZipFile(path, 'r') as zip_ref: 150 | zip_ref.extractall(target_path) 151 | return HttpResponse(json.dumps({'value': 0, 'info': 'success'})) 152 | 153 | def delete_agent(request): 154 | if request.method == 'GET': 155 | name = request.GET['name'] 156 | if not UploadedAgent.objects.filter(name=name).exists(): 157 | return HttpResponse(json.dumps({'value': -1, 'info': 'name not exists'})) 158 | 159 | agents = UploadedAgent.objects.filter(name=name) 160 | path = os.path.join(settings.MEDIA_ROOT, agents[0].f.name) 161 | target_path = os.path.join(os.path.abspath(os.path.join(path, os.pardir)), name) 162 | shutil.rmtree(target_path) 163 | 164 | agents.delete() 165 | Game.objects.filter(agent0=name).delete() 166 | Game.objects.filter(agent1=name).delete() 167 | Payoff.objects.filter(agent0=name).delete() 168 | Payoff.objects.filter(agent1=name).delete() 169 | return HttpResponse(json.dumps({'value': 0, 'info': 'success'})) 170 | 171 | def list_uploaded_agents(request): 172 | if request.method == 'GET': 173 | filter_dict = {key: request.GET.get(key) for key in dict(request.GET).keys()} 174 | result = UploadedAgent.objects.filter(**filter_dict) 175 | result = serializers.serialize('json', result) 176 | return HttpResponse(result) 177 | 178 | def list_baseline_agents(request): 179 | if request.method == 'GET': 180 | if not 'game' in request.GET: 181 | return HttpResponse(json.dumps({'value': -2, 'info': 'game should be given'})) 182 | result = MODEL_IDS[request.GET['game']] 183 | return HttpResponse(json.dumps({'value': 0, 'data': result})) 184 | 185 | @receiver(models.signals.post_delete, sender=UploadedAgent) 186 | def auto_delete_file_on_delete(sender, instance, **kwargs): 187 | if instance.f: 188 | if os.path.isfile(instance.f.path): 189 | os.remove(instance.f.path) 190 | 191 | def download_examples(request): 192 | if request.method == 'GET': 193 | name = request.GET['name'] 194 | file_path = os.path.join(settings.MEDIA_ROOT, 'example_agents', name+'.zip') 195 | if os.path.exists(file_path): 196 | with open(file_path, 'rb') as fh: 197 | response = HttpResponse(fh.read(), content_type="application/vnd.ms-excel") 198 | response['Content-Disposition'] = 'inline; filename=' + os.path.basename(file_path) 199 | return response 200 | raise Http404 201 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Redirect, Route } from 'react-router-dom'; 3 | import Navbar from './components/Navbar'; 4 | import LeaderBoard from './view/LeaderBoard'; 5 | import { PvEDoudizhuDemoView } from './view/PvEView'; 6 | import { DoudizhuReplayView, LeducHoldemReplayView } from './view/ReplayView'; 7 | 8 | const navbarSubtitleMap = { 9 | '/leaderboard': '', 10 | '/replay/doudizhu': 'Doudizhu', 11 | '/replay/leduc-holdem': "Leduc Hold'em", 12 | '/pve/doudizhu-demo': 'Doudizhu PvE Demo', 13 | }; 14 | 15 | function App() { 16 | // todo: add 404 page 17 | return ( 18 | 19 | 20 |
21 | 22 | 23 | {/* */} 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | ); 32 | } 33 | 34 | export default App; 35 | -------------------------------------------------------------------------------- /src/assets/doudizhu.scss: -------------------------------------------------------------------------------- 1 | @import "cards.css"; 2 | 3 | .doudizhu-wrapper { 4 | width: 100%; 5 | height: 100%; 6 | position: relative; 7 | -webkit-user-select: none; 8 | -ms-user-select: none; 9 | user-select: none; 10 | 11 | #gameboard-background { 12 | width: 100%; 13 | height: 100%; 14 | background-image: url("./images/gameboard.png"); 15 | background-repeat: no-repeat; 16 | background-size: 100% 125%; 17 | background-position: bottom; 18 | 19 | &.blur-background { 20 | filter: blur(3px); 21 | pointer-events: none; 22 | } 23 | } 24 | 25 | .played-card-area { 26 | font-size: 12px; 27 | width: 300px; 28 | display: flex; 29 | justify-content: center; 30 | 31 | .timer.fade-in { 32 | visibility: hidden; 33 | opacity: 0; 34 | } 35 | 36 | .timer.fade-out { 37 | visibility: hidden; 38 | transition: visibility 0.1s, opacity 0.05s; 39 | opacity: 0; 40 | } 41 | 42 | .main-player-action-wrapper { 43 | display: flex; 44 | align-items: center; 45 | 46 | button { 47 | font-weight: bolder; 48 | border-radius: 40px; 49 | } 50 | } 51 | 52 | .timer { 53 | visibility: visible; 54 | transition: visibility 0s, opacity 0.2s, transform 0.3s; 55 | opacity: 1; 56 | width: 51px; 57 | height: 60px; 58 | .timer-text { 59 | color: #303133; 60 | transform: translateY(5px); 61 | font-size: 23px; 62 | font-weight: bold; 63 | text-shadow: 0 2px 2px #909399; 64 | line-height: 55px; 65 | } 66 | text-align: center; 67 | background-image: url("./images/timer.png"); 68 | background-repeat: no-repeat; 69 | background-size: 100% 100%; 70 | } 71 | 72 | .non-card { 73 | display: table; 74 | width: 280px; 75 | height: 80px; 76 | 77 | span { 78 | display: table-cell; 79 | vertical-align: middle; 80 | text-align: center; 81 | font-size: 23px; 82 | font-weight: bold; 83 | text-shadow: 0 2px 2px rgba(0, 0, 0, 0.87); 84 | color: #F2F6FC; 85 | } 86 | } 87 | } 88 | 89 | .player-hand-placeholder { 90 | font-size: 16px; 91 | display: table; 92 | 93 | span { 94 | color: whitesmoke; 95 | display: table-cell; 96 | vertical-align: middle; 97 | text-align: center; 98 | } 99 | } 100 | 101 | .player-info { 102 | font-size: 16px; 103 | width: 130px; 104 | height: 130px; 105 | div { 106 | text-align: center; 107 | } 108 | span { 109 | white-space: pre; 110 | text-align: center; 111 | } 112 | } 113 | 114 | .player-main-area { 115 | background-color: initial; 116 | } 117 | 118 | #left-player { 119 | position: absolute; 120 | left: 10px; 121 | top: 10px; 122 | 123 | .player-main-area { 124 | width: 300px; 125 | height: 130px; 126 | font-size: 10px; 127 | 128 | .player-hand-up { 129 | position: absolute; 130 | top: 0; 131 | margin-left: 150px; 132 | width: 150px; 133 | } 134 | 135 | .player-hand-down { 136 | position: absolute; 137 | top: 50px; 138 | margin-left: 150px; 139 | width: 150px; 140 | } 141 | 142 | .player-hand-placeholder { 143 | width: 150px; 144 | height: 130px; 145 | position: absolute; 146 | top: 0; 147 | margin-left: 130px; 148 | } 149 | } 150 | 151 | .played-card-area { 152 | position: relative; 153 | left: 20px; 154 | top: 20px; 155 | } 156 | } 157 | 158 | #right-player { 159 | position: absolute; 160 | right: 10px; 161 | top: 10px; 162 | 163 | .player-main-area { 164 | width: 300px; 165 | height: 130px; 166 | font-size: 10px; 167 | 168 | .player-hand-up { 169 | position: absolute; 170 | top: 0; 171 | margin-right: 150px; 172 | width: 150px; 173 | } 174 | 175 | .player-hand-down { 176 | position: absolute; 177 | top: 50px; 178 | margin-right: 150px; 179 | width: 150px; 180 | } 181 | 182 | .player-hand-placeholder { 183 | width: 150px; 184 | height: 130px; 185 | position: absolute; 186 | top: 0; 187 | margin-right: 130px; 188 | } 189 | 190 | .player-info { 191 | float: right; 192 | } 193 | } 194 | 195 | .played-card-area { 196 | position: relative; 197 | right: 20px; 198 | top: 20px; 199 | } 200 | } 201 | 202 | #bottom-player { 203 | position: absolute; 204 | bottom: 10px; 205 | left: 50%; 206 | margin-left: -290px; 207 | 208 | .player-main-area { 209 | width: 580px; 210 | height: 130px; 211 | position: relative; 212 | 213 | .player-hand { 214 | position: absolute; 215 | top: 0; 216 | padding-left: 20px; 217 | margin-left: 130px; 218 | width: 450px; 219 | font-size: 14px; 220 | } 221 | 222 | .player-hand-placeholder { 223 | width: 150px; 224 | height: 130px; 225 | position: absolute; 226 | top: 0; 227 | padding-left: 20px; 228 | margin-left: 130px; 229 | } 230 | } 231 | 232 | .played-card-area { 233 | width: 100%; 234 | position: relative; 235 | transform: translateY(-10px); 236 | 237 | .playingCards ul.hand { 238 | margin-bottom: 0; 239 | } 240 | 241 | .non-card { 242 | height: 70px; 243 | //transform: translateY(-25px); 244 | } 245 | } 246 | } 247 | 248 | .playingCards { 249 | a.card.selected { 250 | bottom: 1em; 251 | } 252 | } 253 | } 254 | 255 | .doudizhu-statistic-table { 256 | .MuiTableHead-root { 257 | th.MuiTableCell-root.MuiTableCell-head { 258 | font-weight: bolder; 259 | } 260 | } 261 | } -------------------------------------------------------------------------------- /src/assets/faces/JC.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/JC.gif -------------------------------------------------------------------------------- /src/assets/faces/JD.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/JD.gif -------------------------------------------------------------------------------- /src/assets/faces/JH.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/JH.gif -------------------------------------------------------------------------------- /src/assets/faces/JS.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/JS.gif -------------------------------------------------------------------------------- /src/assets/faces/KC.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/KC.gif -------------------------------------------------------------------------------- /src/assets/faces/KD.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/KD.gif -------------------------------------------------------------------------------- /src/assets/faces/KH.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/KH.gif -------------------------------------------------------------------------------- /src/assets/faces/KS.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/KS.gif -------------------------------------------------------------------------------- /src/assets/faces/QC.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/QC.gif -------------------------------------------------------------------------------- /src/assets/faces/QD.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/QD.gif -------------------------------------------------------------------------------- /src/assets/faces/QH.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/QH.gif -------------------------------------------------------------------------------- /src/assets/faces/QS.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/QS.gif -------------------------------------------------------------------------------- /src/assets/faces/README: -------------------------------------------------------------------------------- 1 | These images were adjusted from: 2 | 3 | SVG-cards [http://svg-cards.sourceforge.net/] 4 | by David Bellot 5 | under LGPL [http://www.gnu.org/licenses/lgpl-2.1.html] 6 | -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_C10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_C10.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_C2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_C2.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_C3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_C3.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_C4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_C4.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_C5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_C5.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_C6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_C6.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_C7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_C7.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_C8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_C8.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_C9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_C9.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_CA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_CA.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_CJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_CJ.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_CK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_CK.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_CQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_CQ.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_D10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_D10.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_D2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_D2.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_D3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_D3.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_D4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_D4.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_D5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_D5.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_D6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_D6.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_D7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_D7.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_D8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_D8.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_D9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_D9.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_DA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_DA.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_DJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_DJ.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_DK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_DK.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_DQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_DQ.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_H10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_H10.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_H2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_H2.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_H3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_H3.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_H4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_H4.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_H5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_H5.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_H6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_H6.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_H7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_H7.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_H8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_H8.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_H9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_H9.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_HA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_HA.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_HJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_HJ.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_HK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_HK.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_HQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_HQ.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_Joker_B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_Joker_B.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_Joker_R.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_Joker_R.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_S10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_S10.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_S2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_S2.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_S3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_S3.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_S4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_S4.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_S5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_S5.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_S6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_S6.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_S7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_S7.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_S8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_S8.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_S9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_S9.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_SA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_SA.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_SJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_SJ.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_SK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_SK.png -------------------------------------------------------------------------------- /src/assets/faces/custom/Poker_SQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/custom/Poker_SQ.png -------------------------------------------------------------------------------- /src/assets/faces/joker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/joker.gif -------------------------------------------------------------------------------- /src/assets/faces/pokerback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/faces/pokerback.png -------------------------------------------------------------------------------- /src/assets/gameview.scss: -------------------------------------------------------------------------------- 1 | .progress-bar { 2 | margin: 5px; 3 | } 4 | 5 | .MuiAppBar-colorPrimary.header-bar-wrapper { 6 | background-color: #373538; 7 | } 8 | 9 | .header-bar { 10 | width: calc(100% - 60px); 11 | margin-left: auto; 12 | margin-right: auto; 13 | height: 65px; 14 | padding: 5px; 15 | display: flex; 16 | .stretch { 17 | flex: 1; 18 | } 19 | 20 | .github-info { 21 | flex: 0 100px; 22 | cursor: pointer; 23 | transition: color 0.5s ease; 24 | 25 | &:hover{ 26 | transition: color 0.5s ease; 27 | color: #C0C4CC; 28 | } 29 | 30 | .github-icon { 31 | position: absolute; 32 | top: 50%; 33 | -ms-transform: translateY(-50%); 34 | transform: translateY(-50%); 35 | .MuiSvgIcon-root { 36 | font-size: 30px; 37 | } 38 | } 39 | .github-text { 40 | left: 45px; 41 | line-height: 18px; 42 | position: relative; 43 | top: 50%; 44 | font-size: 18px; 45 | -ms-transform: translateY(-50%); 46 | transform: translateY(-50%); 47 | span { 48 | font-size: 12px; 49 | } 50 | } 51 | } 52 | 53 | .title { 54 | margin-left: 12px; 55 | display: flex; 56 | align-items: center; 57 | justify-content: center; 58 | .title-text{ 59 | line-height: 65px; 60 | font-size: 26px; 61 | font-weight: 600; 62 | letter-spacing: 1px; 63 | transform: scale(1, 1.1) translateY(-1px); 64 | -ms-transform:scale(1, 1.1) translateY(-1px); 65 | -webkit-transform:scale(1, 1.1) translateY(1px); 66 | -moz-transform:scale(1, 1.1) translateY(-1px); 67 | -o-transform:scale(1, 1.1) translateY(-1px); 68 | 69 | .subtitle { 70 | color: #C0C4CC; 71 | margin-left: 11px; 72 | font-size: 16px; 73 | -ms-transform: translateY(3px); 74 | -webkit-transform: translateY(3px); 75 | -moz-transform: translateY(3px); 76 | -o-transform: translateY(3px); 77 | } 78 | } 79 | 80 | } 81 | } 82 | 83 | .game-controller { 84 | -webkit-user-select: none; 85 | -ms-user-select: none; 86 | user-select: none; 87 | .game-controller-paper { 88 | margin: 5px; 89 | padding: 2px 10px; 90 | } 91 | 92 | .status-button { 93 | margin-left: 5px; 94 | margin-right: 5px; 95 | width: 125px; 96 | } 97 | 98 | .form-label-left { 99 | float: left; 100 | text-align: center; 101 | font-size: 14px; 102 | line-height: 1; 103 | padding: 11px 6px 11px 6px; 104 | box-sizing: border-box; 105 | width: 80px; 106 | } 107 | 108 | .MuiFormControlLabel-label { 109 | text-align: center; 110 | height: 51px; 111 | line-height: 51px; 112 | vertical-align: middle; 113 | } 114 | 115 | .switch-control { 116 | .MuiTypography-root.MuiFormControlLabel-label.MuiTypography-body1{ 117 | transform: translateY(2px); 118 | font-family: 'Arvo', 'Rockwell', monospace, PingFangSC-Regular, sans-serif; 119 | } 120 | } 121 | 122 | @media (pointer: coarse) { 123 | .MuiSlider-root{ 124 | padding-top: 13px; 125 | padding-bottom: 13px; 126 | 127 | span.MuiSlider-markLabel { 128 | top: 26px; 129 | } 130 | } 131 | } 132 | } 133 | 134 | .doudizhu-view-container { 135 | width: 1000px; 136 | margin-top: 5px; 137 | margin-left: auto; 138 | margin-right: auto; 139 | -webkit-user-select: none; 140 | -ms-user-select: none; 141 | user-select: none; 142 | 143 | 144 | .doudizhu-gameboard-paper { 145 | height: calc(100% - 7px*2); 146 | margin: 5px; 147 | padding: 2px; 148 | } 149 | 150 | .doudizhu-probability-paper { 151 | height: calc(100% - 5px*2); 152 | margin: 5px; 153 | 154 | .probability-player { 155 | height: calc(72px - 16px*2); 156 | padding: 16px; 157 | 158 | span { 159 | vertical-align: middle; 160 | } 161 | } 162 | 163 | .probability-table { 164 | display: table; 165 | border-collapse: collapse; 166 | 167 | width: 100%; 168 | height: calc(100% - 72px); 169 | 170 | &.with-three-landlord-cards { 171 | height: calc(100% - 200px); 172 | .probability-item { 173 | .playing { 174 | height: 109px; 175 | } 176 | } 177 | } 178 | 179 | .probability-item { 180 | display: table-row; 181 | 182 | &:not(:first-child) { 183 | border-top: 1px solid rgba(0, 0, 0, 0.12); 184 | } 185 | .waiting { 186 | display: table-cell; 187 | vertical-align: middle; 188 | text-align: center; 189 | } 190 | .playing { 191 | -webkit-transition: background-color 0.3s ease; 192 | -ms-transition: background-color 0.3s ease; 193 | transition: background-color 0.3s ease; 194 | display: table-cell; 195 | height: 152px; 196 | vertical-align: middle; 197 | 198 | .playingCards ul.hand { 199 | margin-bottom: 0; 200 | } 201 | 202 | .probability-move { 203 | font-size: 10px; 204 | width: 100%; 205 | display: flex; 206 | justify-content: center; 207 | } 208 | .non-card { 209 | visibility: visible; 210 | transition: visibility 0s, opacity 0.5s; 211 | opacity: 1; 212 | display: table; 213 | width: 100%; 214 | margin-top: 5%; 215 | height: 25px; 216 | 217 | span { 218 | display: table-cell; 219 | vertical-align: middle; 220 | text-align: center; 221 | font-size: 16px; 222 | font-weight: bolder; 223 | text-shadow: -1px -1px 0 #fff, 224 | 1px -1px 0 #fff, 225 | -1px 1px 0 #fff, 226 | 1px 1px 0 #fff; 227 | } 228 | } 229 | .non-card.hide { 230 | visibility: hidden; 231 | transition: visibility 0.2s, opacity 0.15s; 232 | opacity: 0; 233 | pointer-events:none; 234 | } 235 | } 236 | 237 | } 238 | } 239 | } 240 | } 241 | 242 | .leduc-view-container { 243 | width: 1000px; 244 | margin-left: auto; 245 | margin-right: auto; 246 | 247 | 248 | 249 | .leduc-gameboard-paper { 250 | height: calc(100% - 7px*2); 251 | margin: 5px; 252 | padding: 2px; 253 | } 254 | 255 | .leduc-probability-paper { 256 | height: calc(100% - 5px*2); 257 | margin: 5px; 258 | 259 | .probability-player { 260 | height: calc(72px - 16px*2); 261 | padding: 16px; 262 | display:table; 263 | span { 264 | display: table-cell; 265 | vertical-align: middle; 266 | } 267 | } 268 | 269 | .probability-table { 270 | display: table; 271 | border-collapse: collapse; 272 | 273 | width: 100%; 274 | height: calc(100% - 72px); 275 | 276 | .probability-item { 277 | display: table-row; 278 | 279 | &:not(:first-child) { 280 | border-top: 1px solid rgba(0, 0, 0, 0.12); 281 | } 282 | .waiting { 283 | display: table-cell; 284 | vertical-align: middle; 285 | text-align: center; 286 | } 287 | .playing { 288 | -webkit-transition: background-color 0.3s ease; 289 | -ms-transition: background-color 0.3s ease; 290 | transition: background-color 0.3s ease; 291 | display: table-cell; 292 | vertical-align: middle; 293 | 294 | .playingCards ul.hand { 295 | margin-bottom: 0; 296 | } 297 | 298 | .probability-move { 299 | font-size: 20px; 300 | font-weight: bold; 301 | width: 100%; 302 | display: flex; 303 | justify-content: center; 304 | } 305 | .non-card { 306 | visibility: visible; 307 | transition: visibility 0s, opacity 0.5s; 308 | opacity: 1; 309 | display: table; 310 | width: 100%; 311 | margin-top: 5%; 312 | height: 25px; 313 | 314 | span { 315 | display: table-cell; 316 | vertical-align: middle; 317 | text-align: center; 318 | font-size: 16px; 319 | font-weight: bolder; 320 | color: #303133; 321 | text-shadow: -1px -1px 0 #fff, 322 | 1px -1px 0 #fff, 323 | -1px 1px 0 #fff, 324 | 1px 1px 0 #fff; 325 | } 326 | } 327 | .non-card.hide { 328 | visibility: hidden; 329 | transition: visibility 0.2s, opacity 0.15s; 330 | opacity: 0; 331 | pointer-events:none; 332 | } 333 | } 334 | 335 | } 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/assets/images/Actions/call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Actions/call.png -------------------------------------------------------------------------------- /src/assets/images/Actions/call_u.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Actions/call_u.png -------------------------------------------------------------------------------- /src/assets/images/Actions/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Actions/check.png -------------------------------------------------------------------------------- /src/assets/images/Actions/check_u.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Actions/check_u.png -------------------------------------------------------------------------------- /src/assets/images/Actions/fold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Actions/fold.png -------------------------------------------------------------------------------- /src/assets/images/Actions/fold_u.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Actions/fold_u.png -------------------------------------------------------------------------------- /src/assets/images/Actions/raise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Actions/raise.png -------------------------------------------------------------------------------- /src/assets/images/Actions/raise_u.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Actions/raise_u.png -------------------------------------------------------------------------------- /src/assets/images/Portrait/Landlord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Portrait/Landlord.png -------------------------------------------------------------------------------- /src/assets/images/Portrait/Landlord_wName.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Portrait/Landlord_wName.png -------------------------------------------------------------------------------- /src/assets/images/Portrait/Peasant_wName.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Portrait/Peasant_wName.png -------------------------------------------------------------------------------- /src/assets/images/Portrait/Player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Portrait/Player.png -------------------------------------------------------------------------------- /src/assets/images/Portrait/Player1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Portrait/Player1.png -------------------------------------------------------------------------------- /src/assets/images/Portrait/Player2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Portrait/Player2.png -------------------------------------------------------------------------------- /src/assets/images/Portrait/Player3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Portrait/Player3.png -------------------------------------------------------------------------------- /src/assets/images/Portrait/Pleasant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Portrait/Pleasant.png -------------------------------------------------------------------------------- /src/assets/images/Tokens/Token_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Tokens/Token_1.png -------------------------------------------------------------------------------- /src/assets/images/Tokens/Token_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Tokens/Token_10.png -------------------------------------------------------------------------------- /src/assets/images/Tokens/Token_100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Tokens/Token_100.png -------------------------------------------------------------------------------- /src/assets/images/Tokens/Token_25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Tokens/Token_25.png -------------------------------------------------------------------------------- /src/assets/images/Tokens/Token_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Tokens/Token_5.png -------------------------------------------------------------------------------- /src/assets/images/Tokens/Token_Black_100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Tokens/Token_Black_100.png -------------------------------------------------------------------------------- /src/assets/images/Tokens/Token_Blue_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Tokens/Token_Blue_10.png -------------------------------------------------------------------------------- /src/assets/images/Tokens/Token_Green_25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Tokens/Token_Green_25.png -------------------------------------------------------------------------------- /src/assets/images/Tokens/Token_Red_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Tokens/Token_Red_5.png -------------------------------------------------------------------------------- /src/assets/images/Tokens/Token_White_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/Tokens/Token_White_1.png -------------------------------------------------------------------------------- /src/assets/images/gameboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/gameboard.png -------------------------------------------------------------------------------- /src/assets/images/logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/logo_white.png -------------------------------------------------------------------------------- /src/assets/images/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/table.png -------------------------------------------------------------------------------- /src/assets/images/timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datamllab/rlcard-showdown/dd1cfdf522d5ce2fb9ed809f804fdbdbeab020fe/src/assets/images/timer.png -------------------------------------------------------------------------------- /src/assets/index.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Arvo&display=swap'); 2 | 3 | body { 4 | background-color: rgb(241, 241, 241); 5 | margin: 0; 6 | color: #606266; 7 | font-family: 'Arvo', 'Rockwell', monospace, PingFangSC-Regular, sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 14 | monospace; 15 | } 16 | 17 | .unselectable { 18 | pointer-events: none; 19 | -webkit-user-select: none; 20 | -ms-user-select: none; 21 | user-select: none; 22 | } 23 | 24 | .el-message { 25 | z-index: 9999; 26 | } 27 | 28 | .citation { 29 | font-family: 'Rockwell', monospace, PingFangSC-Regular, sans-serif; 30 | margin-top: 15px; 31 | padding: 6px; 32 | 33 | -webkit-user-select: text; 34 | -ms-user-select: text; 35 | user-select: text; 36 | 37 | a { 38 | &:visited { 39 | color: blue; 40 | } 41 | } 42 | 43 | pre { 44 | margin-top: 5px; 45 | padding: 10px; 46 | border-radius: 5px; 47 | color: #64686d; 48 | background-color: rgb(255, 255, 255); 49 | border: solid 1px #e9e9e9; 50 | } 51 | } 52 | 53 | 54 | 55 | #upload-model-note { 56 | a { 57 | color: #3f51b5; 58 | text-decoration: none; 59 | 60 | &:visited { 61 | color: #3f51b5; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/assets/leducholdem.scss: -------------------------------------------------------------------------------- 1 | @import "cards.css"; 2 | .leduc-holdem-wrapper { 3 | width: 100%; 4 | height: 100%; 5 | //background-color: #C3CDFF; 6 | background-image: url("./images/gameboard.png"); 7 | background-repeat: no-repeat; 8 | background-size: 100% 132%; 9 | background-position: bottom; 10 | position: relative; 11 | -webkit-user-select: none; 12 | -ms-user-select: none; 13 | user-select: none; 14 | 15 | .played-card-area { 16 | font-size: 12px; 17 | width: 40%; 18 | margin-left: 65%; 19 | position: relative; 20 | display: flex; 21 | justify-content: center; 22 | 23 | .timer.fade-in { 24 | visibility: hidden; 25 | opacity: 0; 26 | } 27 | 28 | .timer.fade-out { 29 | visibility: hidden; 30 | transition: visibility 0.1s, opacity 0.05s; 31 | opacity: 0; 32 | } 33 | 34 | .timer { 35 | visibility: visible; 36 | transition: visibility 0s, opacity 0.2s, transform 0.3s; 37 | opacity: 1; 38 | width: 51px; 39 | height: 60px; 40 | .timer-text { 41 | color: #303133; 42 | margin-top: 5px; 43 | font-size: 23px; 44 | font-weight: bold; 45 | text-shadow: 0 2px 2px #909399; 46 | line-height: 55px; 47 | } 48 | text-align: center; 49 | background-image: url("./images/timer.png"); 50 | background-repeat: no-repeat; 51 | background-size: 100% 100%; 52 | } 53 | 54 | .non-card { 55 | display: table; 56 | width: 80px; 57 | height: 60px; 58 | .action-text { 59 | text-transform:uppercase; 60 | display: table-cell; 61 | vertical-align: middle; 62 | text-align: center; 63 | font-size: 23px; 64 | font-weight: bold; 65 | text-shadow: 0 2px 2px rgba(0, 0, 0, 0.87); 66 | color: #F2F6FC; 67 | } 68 | } 69 | } 70 | 71 | .player-hand-placeholder { 72 | font-size: 16px; 73 | display: table; 74 | 75 | span { 76 | display: table-cell; 77 | vertical-align: middle; 78 | text-align: center; 79 | } 80 | } 81 | 82 | .player-info { 83 | font-size: 16px; 84 | width: 130px; 85 | height: 130px; 86 | div { 87 | text-align: center; 88 | } 89 | span { 90 | white-space: pre; 91 | text-align: center; 92 | } 93 | .bet-value { 94 | color: whitesmoke; 95 | position: relative; 96 | text-shadow: 0 2px 2px rgba(0, 0, 0, 0.87); 97 | top: -120px; 98 | left: 120px; 99 | font-weight: bold; 100 | } 101 | .token-container{ 102 | position: relative; 103 | top: -115px; 104 | left: 120px; 105 | width: 120px; 106 | height: 80px; 107 | display: flex; 108 | flex-direction: row; 109 | .pile-placeholder{ 110 | width: 24px; 111 | margin-top: 0; 112 | .token-img { 113 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.87); 114 | -moz-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.87); 115 | -webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.87); 116 | -o-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.87); 117 | border-radius: 15px; 118 | } 119 | } 120 | div { 121 | position: relative; 122 | margin-top: -15px; 123 | } 124 | .pile-placeholder > :first-child { margin-top: 0 !important;} 125 | } 126 | } 127 | 128 | .player-main-area { 129 | background-color: initial; 130 | width: 300px; 131 | height: 130px; 132 | position: relative; 133 | 134 | .player-hand { 135 | position: absolute; 136 | top: 15px; 137 | left: 65px; 138 | padding-left: 20px; 139 | margin-left: 130px; 140 | width: 75px; 141 | height: 110px; 142 | } 143 | 144 | .player-hand-placeholder { 145 | width: 150px; 146 | height: 130px; 147 | position: absolute; 148 | top: 0; 149 | padding-left: 20px; 150 | margin-left: 130px; 151 | } 152 | } 153 | 154 | #top-player { 155 | position: absolute; 156 | top: 10px; 157 | left: 40%; 158 | margin-left: -190px; 159 | } 160 | 161 | #bottom-player { 162 | position: absolute; 163 | bottom: 10px; 164 | left: 40%; 165 | margin-left: -190px; 166 | } 167 | 168 | #public-card-area { 169 | position: absolute; 170 | right: 10px; 171 | top: 50%; 172 | margin-top: -75px; 173 | width: 130px; 174 | height: 150px; 175 | display: table; 176 | 177 | .info-area { 178 | display: table-cell; 179 | vertical-align: middle; 180 | text-align: center; 181 | span.round-number { 182 | font-size: 20px; 183 | font-weight: bold; 184 | text-shadow: 0 2px 2px rgba(0, 0, 0, 0.87); 185 | color: #F2F6FC; 186 | } 187 | } 188 | .playingCards { 189 | transform: translate(3px, 10px); 190 | } 191 | } 192 | 193 | .playingCards { 194 | a.card.selected { 195 | bottom: 1em; 196 | } 197 | } 198 | } 199 | 200 | -------------------------------------------------------------------------------- /src/components/GameBoard/LeducHoldemGameBoard.js: -------------------------------------------------------------------------------- 1 | import Avatar from '@material-ui/core/Avatar'; 2 | import Chip from '@material-ui/core/Chip'; 3 | import React from 'react'; 4 | import PlaceHolderPlayer from '../../assets/images/Portrait/Player.png'; 5 | import Player1 from '../../assets/images/Portrait/Player1.png'; 6 | import Player2 from '../../assets/images/Portrait/Player2.png'; 7 | import '../../assets/leducholdem.scss'; 8 | import { millisecond2Second, translateCardData } from '../../utils'; 9 | 10 | class LeducHoldemGameBoard extends React.Component { 11 | computePlayerPortrait(playerId, playerIdx) { 12 | if (this.props.playerInfo.length > 0) { 13 | const chipTitle = 14 | this.props.playerInfo[playerIdx].agentInfo && this.props.playerInfo[playerIdx].agentInfo.name 15 | ? '' 16 | : 'ID'; 17 | const chipLabel = 18 | this.props.playerInfo[playerIdx].agentInfo && this.props.playerInfo[playerIdx].agentInfo.name 19 | ? this.props.playerInfo[playerIdx].agentInfo.name 20 | : playerId; 21 | return this.props.playerInfo[playerIdx].id === 0 ? ( 22 |
23 | {'Player 24 | {chipTitle} : undefined} 27 | label={chipLabel} 28 | color="primary" 29 | /> 30 |
31 | ) : ( 32 |
33 | {'Player 34 | {chipTitle} : undefined} 37 | label={chipLabel} 38 | color="primary" 39 | /> 40 |
41 | ); 42 | } else 43 | return ( 44 |
45 | {'Player'} 46 | ID} label={playerId} clickable color="primary" /> 47 |
48 | ); 49 | } 50 | 51 | computeActionText(action) { 52 | if (action.length > 0) { 53 | return {action}; 54 | } 55 | } 56 | 57 | computeTokenImage(bet) { 58 | const values = [100, 25, 10, 5, 1]; 59 | let tokens = { 60 | 100: 0, 61 | 25: 0, 62 | 10: 0, 63 | 5: 0, 64 | 1: 0, 65 | }; 66 | let i = 0; 67 | while (bet !== 0 && i < values.length) { 68 | if (bet >= values[i]) { 69 | bet -= values[i]; 70 | tokens[values[i]]++; 71 | } else { 72 | i++; 73 | } 74 | } 75 | let child = []; 76 | for (let [key, value] of Object.entries(tokens)) { 77 | if (value !== 0) { 78 | let grandChild = []; 79 | for (let j = 0; j < value; j++) { 80 | grandChild.push( 81 |
82 | {'taken'} 89 |
, 90 | ); 91 | } 92 | let subElement = React.createElement( 93 | 'div', 94 | { className: 'pile-placeholder', key: key + '_' + value }, 95 | grandChild, 96 | ); 97 | child.push(subElement); 98 | } 99 | } 100 | return React.createElement('div', { className: 'token-container' }, child); 101 | } 102 | 103 | computeHand(card) { 104 | const [rankClass, suitClass, rankText, suitText] = translateCardData(card); 105 | return ( 106 |
107 |
108 | {rankText} 109 | {suitText} 110 |
111 |
112 | ); 113 | } 114 | 115 | playerDecisionArea(playerIdx) { 116 | if (this.props.currentPlayer === playerIdx) { 117 | return ( 118 |
119 |
{millisecond2Second(this.props.considerationTime)}
120 |
121 | ); 122 | } else { 123 | return
{this.computeActionText(this.props.latestAction[playerIdx])}
; 124 | } 125 | } 126 | 127 | displayPublicCard() { 128 | if (this.props.round === 0) { 129 | return ( 130 |
131 |
*
132 |
133 | ); 134 | } else { 135 | const [rankClass, suitClass, rankText, suitText] = translateCardData(this.props.publicCard); 136 | return ( 137 |
138 |
139 | {rankText} 140 | {suitText} 141 |
142 |
143 | ); 144 | } 145 | } 146 | 147 | render() { 148 | // compute the id as well as index in list for every player 149 | const bottomId = this.props.mainPlayerId; 150 | let found = this.props.playerInfo.find((element) => { 151 | return element.id === bottomId; 152 | }); 153 | const bottomIdx = found ? found.index : -1; 154 | const topIdx = bottomIdx >= 0 ? (bottomIdx + 1) % 2 : -1; 155 | let topId = -1; 156 | if (topIdx > -1) { 157 | found = this.props.playerInfo.find((element) => { 158 | return element.index === topIdx; 159 | }); 160 | if (found) topId = found.id; 161 | } 162 | return ( 163 |
164 |
165 |
{bottomIdx >= 0 ? this.playerDecisionArea(bottomIdx) : ''}
166 |
167 |
168 | {this.computePlayerPortrait(bottomId, bottomIdx)} 169 | {`Bet: ${ 170 | this.props.pot[bottomIdx] ? this.props.pot[bottomIdx] : '...' 171 | }`} 172 | {this.computeTokenImage(this.props.pot[bottomIdx])} 173 |
174 | {bottomIdx >= 0 ? ( 175 |
{this.computeHand(this.props.hands[bottomIdx])}
176 | ) : ( 177 |
178 | Waiting... 179 |
180 | )} 181 |
182 |
183 |
184 |
185 |
186 | {this.computePlayerPortrait(topId, topIdx)} 187 | {`Bet: ${ 188 | this.props.pot[topIdx] ? this.props.pot[topIdx] : '...' 189 | }`} 190 | {this.computeTokenImage(this.props.pot[topIdx])} 191 |
192 | {topIdx >= 0 ? ( 193 |
{this.computeHand(this.props.hands[topIdx])}
194 | ) : ( 195 |
196 | Waiting... 197 |
198 | )} 199 |
200 |
{topIdx >= 0 ? this.playerDecisionArea(topIdx) : ''}
201 |
202 |
203 |
204 | {`Round: ${this.props.round + 1}`} 205 | {this.displayPublicCard()} 206 |
207 |
208 |
209 | ); 210 | } 211 | } 212 | 213 | export default LeducHoldemGameBoard; 214 | -------------------------------------------------------------------------------- /src/components/GameBoard/index.js: -------------------------------------------------------------------------------- 1 | import DoudizhuGameBoard from "./DoudizhuGameBoard"; 2 | import LeducHoldemGameBoard from "./LeducHoldemGameBoard"; 3 | 4 | export {DoudizhuGameBoard, LeducHoldemGameBoard}; 5 | -------------------------------------------------------------------------------- /src/components/MenuBar.js: -------------------------------------------------------------------------------- 1 | import Button from '@material-ui/core/Button'; 2 | import Card from '@material-ui/core/Card'; 3 | import CardContent from '@material-ui/core/CardContent'; 4 | import Collapse from '@material-ui/core/Collapse'; 5 | import Dialog from '@material-ui/core/Dialog'; 6 | import DialogActions from '@material-ui/core/DialogActions'; 7 | import DialogContent from '@material-ui/core/DialogContent'; 8 | import DialogTitle from '@material-ui/core/DialogTitle'; 9 | import Drawer from '@material-ui/core/Drawer'; 10 | import FormControl from '@material-ui/core/FormControl'; 11 | import InputLabel from '@material-ui/core/InputLabel'; 12 | import Link from '@material-ui/core/Link'; 13 | import List from '@material-ui/core/List'; 14 | import ListItem from '@material-ui/core/ListItem'; 15 | import ListItemText from '@material-ui/core/ListItemText'; 16 | import ListSubheader from '@material-ui/core/ListSubheader'; 17 | import MenuItem from '@material-ui/core/MenuItem'; 18 | import Select from '@material-ui/core/Select'; 19 | import { makeStyles } from '@material-ui/core/styles'; 20 | import TextField from '@material-ui/core/TextField'; 21 | import Typography from '@material-ui/core/Typography'; 22 | import ExpandLess from '@material-ui/icons/ExpandLess'; 23 | import ExpandMore from '@material-ui/icons/ExpandMore'; 24 | import HelpIcon from '@material-ui/icons/Help'; 25 | import axios from 'axios'; 26 | import { Loading, Message, Upload } from 'element-react'; 27 | import qs from 'query-string'; 28 | import React from 'react'; 29 | import { useHistory } from 'react-router-dom'; 30 | import { apiUrl } from '../utils/config'; 31 | 32 | const drawerWidth = 250; 33 | 34 | const useStyles = makeStyles((theme) => ({ 35 | uploadNoteRoot: { 36 | maxWidth: '358px', 37 | color: '#E6A23C', 38 | backgroundColor: '#fdf6ec', 39 | }, 40 | title: { 41 | color: '#e6a23c', 42 | lineHeight: '24px', 43 | fontSize: 16, 44 | display: 'flex', 45 | alignItems: 'center', 46 | justifyContent: 'flex-start', 47 | }, 48 | formControl: { 49 | marginTop: theme.spacing(1), 50 | marginBottom: theme.spacing(1), 51 | minWidth: 120, 52 | }, 53 | drawer: { 54 | zIndex: 1001, 55 | width: drawerWidth, 56 | flexShrink: 0, 57 | }, 58 | drawerPaper: { 59 | height: 'calc(100% - 75px)', 60 | zIndex: 1001, 61 | width: drawerWidth, 62 | top: 75, 63 | }, 64 | button: { 65 | width: 200, 66 | position: 'fixed', 67 | left: 25, 68 | bottom: 25, 69 | }, 70 | list: { 71 | height: 'calc(100% - 86px - 16px)', 72 | overflowY: 'auto', 73 | borderBottom: '1px solid #ccc', 74 | paddingTop: '0', 75 | }, 76 | nested: { 77 | paddingLeft: theme.spacing(4), 78 | }, 79 | nestedSubheader: { 80 | paddingLeft: theme.spacing(4), 81 | background: 'white', 82 | }, 83 | menuLayer1: { 84 | '& span': { 85 | fontWeight: 600, 86 | fontSize: 14, 87 | }, 88 | }, 89 | menuLayer2: { 90 | '& span': { 91 | fontWeight: 400, 92 | fontSize: 14, 93 | }, 94 | '&$active': { 95 | '& span': { 96 | color: '#3f51b5', 97 | fontWeight: 600, 98 | }, 99 | }, 100 | }, 101 | active: {}, 102 | })); 103 | 104 | function MenuBar(props) { 105 | const classes = useStyles(); 106 | const [uploadDialogLoading, setUploadDialogLoading] = React.useState(false); 107 | 108 | const [open, setOpen] = React.useState({ game: true, agent: true }); 109 | 110 | const handleClickGame = () => { 111 | setOpen({ game: !open.game, agent: open.agent }); 112 | }; 113 | const handleClickAgent = () => { 114 | setOpen({ game: open.game, agent: !open.agent }); 115 | }; 116 | 117 | const [uploadDialogOpen, setUploadDialogOpen] = React.useState(false); 118 | const [downloadChannelDialogOpen, setDownloadChannelDialogOpen] = React.useState(false); 119 | 120 | const openUploadDialog = () => { 121 | setUploadDialogOpen(true); 122 | }; 123 | 124 | const uploadFormInitValue = { name: '', game: 'leduc-holdem' }; 125 | const [uploadForm, setUploadForm] = React.useState({ ...uploadFormInitValue }); 126 | const handleUploadFormChange = (e, property) => { 127 | let tempUploadForm = { ...uploadForm }; 128 | tempUploadForm[property] = e.target.value; 129 | setUploadForm(tempUploadForm); 130 | }; 131 | 132 | const handleUploadDialogClose = () => { 133 | setUploadForm({ ...uploadFormInitValue }); 134 | setUploadDialogOpen(false); 135 | }; 136 | 137 | const handleDownloadChannelDialogClose = () => { 138 | setDownloadChannelDialogOpen(false); 139 | }; 140 | 141 | let uploadRef = React.createRef(); 142 | const handleSubmitUpload = () => { 143 | // check if data to upload is legal 144 | if (uploadRef.current.state.fileList.length !== 1) { 145 | Message({ 146 | message: 'Please select one zip file to upload', 147 | type: 'warning', 148 | showClose: true, 149 | }); 150 | return; 151 | } 152 | console.log('upload', uploadRef.current); 153 | if ( 154 | !['application/zip', 'application/x-zip-compressed'].includes(uploadRef.current.state.fileList[0].raw.type) 155 | ) { 156 | Message({ 157 | message: 'Only zip file can be uploaded', 158 | type: 'warning', 159 | showClose: true, 160 | }); 161 | return; 162 | } 163 | 164 | if (uploadForm.name === '') { 165 | Message({ 166 | message: 'Model name cannot be blank', 167 | type: 'warning', 168 | showClose: true, 169 | }); 170 | return; 171 | } 172 | 173 | let flatGameList = []; 174 | Object.keys(props.modelList).forEach((game) => { 175 | flatGameList = flatGameList.concat([...props.modelList[game]]); 176 | }); 177 | if (flatGameList.includes(uploadForm.name)) { 178 | Message({ 179 | message: 'Model name exists', 180 | type: 'warning', 181 | showClose: true, 182 | }); 183 | return; 184 | } 185 | 186 | const bodyFormData = new FormData(); 187 | bodyFormData.append('name', uploadForm.name); 188 | bodyFormData.append('game', uploadForm.game); 189 | bodyFormData.append('model', uploadRef.current.state.fileList[0].raw); 190 | setUploadDialogLoading(true); 191 | axios 192 | .post(`${apiUrl}/tournament/upload_agent`, bodyFormData, { 193 | headers: { 'Content-Type': 'multipart/form-data' }, 194 | }) 195 | .then((res) => { 196 | setTimeout(() => { 197 | setUploadDialogLoading(false); 198 | }, 250); 199 | Message({ 200 | message: 'Successfully uploaded model', 201 | type: 'success', 202 | showClose: true, 203 | }); 204 | props.setReloadMenu(props.reloadMenu + 1); 205 | setUploadDialogOpen(false); 206 | setUploadForm({ ...uploadFormInitValue }); 207 | }) 208 | .catch((err) => { 209 | setTimeout(() => { 210 | setUploadDialogLoading(false); 211 | }, 250); 212 | Message({ 213 | message: 'Failed to upload model', 214 | type: 'error', 215 | showClose: true, 216 | }); 217 | console.log(err); 218 | }); 219 | }; 220 | 221 | const history = useHistory(); 222 | const handleGameJump = (gameName) => { 223 | props.resetPagination(); 224 | history.push(`/leaderboard?type=game&name=${gameName}`); 225 | }; 226 | const handleAgentJump = (agentName) => { 227 | props.resetPagination(); 228 | history.push(`/leaderboard?type=agent&name=${agentName}`); 229 | }; 230 | 231 | const { type, name } = qs.parse(window.location.search); 232 | const gameMenu = props.gameList.map((game) => { 233 | return ( 234 | 235 | { 239 | handleGameJump(game.game); 240 | }} 241 | > 242 | 248 | 249 | 250 | ); 251 | }); 252 | 253 | const generateAgentMenu = (modelList) => { 254 | return modelList.map((model) => { 255 | return ( 256 | 257 | { 261 | handleAgentJump(model); 262 | }} 263 | > 264 | 270 | 271 | 272 | ); 273 | }); 274 | }; 275 | 276 | return ( 277 | 284 | 285 | 286 | 287 | {open.agent ? : } 288 | 289 | 290 | {gameMenu} 291 | 292 | 293 | 294 | {open.game ? : } 295 | 296 | 297 | {Object.keys(props.modelList).map((gameName) => { 298 | return ( 299 |
300 | {gameName} 301 | {generateAgentMenu(props.modelList[gameName])} 302 |
303 | ); 304 | })} 305 |
306 |
307 | 310 | 316 | 317 | Upload Model 318 | 319 | 320 | 321 | 322 | 323 | Note 324 | 325 | 326 | Download the example{' '} 327 | 331 | DQN model 332 | {' '} 333 | for Leduc Holdem or{' '} 334 | { 337 | setDownloadChannelDialogOpen(true); 338 | }} 339 | > 340 | DMC model 341 | {' '} 342 | for Doudizhu to test and learn about model upload functionality. 343 | 344 | 345 | 346 | Only zip file can be uploaded} 355 | > 356 | 357 |
358 | Drag the file here, or Click to upload 359 |
360 |
361 | handleUploadFormChange(e, 'name')} 369 | fullWidth 370 | /> 371 | {/* handleUploadFormChange(e, 'entry')}*/} 378 | {/* fullWidth*/} 379 | {/*/>*/} 380 | {/* handleUploadFormChange(e, 'game')}*/} 387 | {/* fullWidth*/} 388 | {/*/>*/} 389 | 390 | Game 391 | 405 | 406 |
407 | 408 | 411 | 414 | 415 |
416 |
417 | 422 | Choose Download Channel 423 | 424 |
425 | 434 | 444 |
(提取码: s54s)
445 |
446 |
447 | 448 | 457 | 458 |
459 |
460 | ); 461 | } 462 | 463 | export default MenuBar; 464 | -------------------------------------------------------------------------------- /src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Link, useLocation } from 'react-router-dom'; 3 | import logo_white from "../assets/images/logo_white.png"; 4 | import GitHubIcon from "@material-ui/icons/GitHub"; 5 | import AppBar from "@material-ui/core/AppBar"; 6 | import axios from 'axios'; 7 | 8 | function Navbar({subtitleMap}) { 9 | const [stars, setStars] = useState('...'); 10 | let location = useLocation(); 11 | console.log(location.pathname, subtitleMap); 12 | const subtitle = subtitleMap[location.pathname]; 13 | 14 | useEffect(() => { 15 | axios.get("https://api.github.com/repos/datamllab/rlcard") 16 | .then(res=>{ 17 | setStars(res.data.stargazers_count); 18 | }); 19 | }, []) 20 | 21 | return ( 22 | 23 |
24 | {"Logo"} 25 |
Showdown{subtitle ? '/ ' + subtitle : ''}
26 |
27 |
{window.location.href = 'https://github.com/datamllab/rlcard'}}> 28 |
29 |
Github
{stars} stars
30 |
31 |
32 | 33 | ) 34 | 35 | } 36 | 37 | export default Navbar; 38 | -------------------------------------------------------------------------------- /src/i18n.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import translationEN from './locales/en/translation.json'; 4 | import translationZH from './locales/zh/translation.json'; 5 | 6 | // the translations 7 | // (tip move them in a JSON file and import them, 8 | // or even better, manage them via a UI: https://react.i18next.com/guides/multiple-translation-files#manage-your-translations-with-a-management-gui) 9 | const resources = { 10 | en: { 11 | translation: translationEN, 12 | }, 13 | zh: { 14 | translation: translationZH, 15 | }, 16 | }; 17 | 18 | i18n.use(initReactI18next) // passes i18n down to react-i18next 19 | .init({ 20 | resources, 21 | lng: localStorage.getItem('LOCALE') || 'en', // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources 22 | // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage 23 | 24 | interpolation: { 25 | escapeValue: false, // react already safes from xss 26 | }, 27 | }); 28 | 29 | export default i18n; 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // import element ui 2 | import 'element-theme-default'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import App from './App'; 6 | import './assets/index.scss'; 7 | import './i18n'; 8 | 9 | ReactDOM.render(, document.getElementById('root')); 10 | -------------------------------------------------------------------------------- /src/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "hidden": "Hidden", 3 | "waiting...": "Waiting...", 4 | "play_again": "Play Again", 5 | "cancel": "Cancel", 6 | "turn": "Turn", 7 | "reset": "Reset", 8 | 9 | "doudizhu": { 10 | "ai_hand_faceup": "AI Hand Face-Up", 11 | "play_as_landlord": "Play As Landlord", 12 | "play_as_peasant": "Play As Peasant", 13 | "landlord_up": "Landlord Up", 14 | "landlord_down": "Landlord Down", 15 | "peasants_win": "Peasants Win!", 16 | "landlord_win": "Landlord Wins!", 17 | "only_choice": "Only Choice", 18 | "role": "Role", 19 | "win": "Win", 20 | "total": "Total", 21 | "win_rate": "Win Rate", 22 | "three_landlord_cards": "Three Landlord Cards", 23 | "ai_thinking_time": "AI Thinking Time", 24 | "landlord": "Landlord", 25 | "expected_win_rate": "Expected Win Rate", 26 | "pass": "Pass", 27 | "deselect": "Deselect", 28 | "hint": "Hint", 29 | "play": "Play" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/locales/zh/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "hidden": "隐藏", 3 | "waiting...": "等待中...", 4 | "play_again": "再来一局", 5 | "cancel": "取消", 6 | "turn": "回合", 7 | "reset": "重置", 8 | 9 | "doudizhu": { 10 | "ai_hand_faceup": "显示AI手牌", 11 | "play_as_landlord": "扮演地主", 12 | "play_as_peasant": "扮演农民", 13 | "landlord_up": "地主上家", 14 | "landlord_down": "地主下家", 15 | "peasants_win": "农民胜利!", 16 | "landlord_win": "地主胜利!", 17 | "only_choice": "唯一选择", 18 | "role": "角色", 19 | "win": "胜场", 20 | "total": "全部", 21 | "win_rate": "胜率", 22 | "three_landlord_cards": "地主牌", 23 | "ai_thinking_time": "AI考虑时间", 24 | "landlord": "地主", 25 | "expected_win_rate": "预计胜率", 26 | "pass": "不出", 27 | "deselect": "取消选择", 28 | "hint": "提示", 29 | "play": "出牌" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | const apiUrl = 'http://127.0.0.1:8000'; 2 | const douzeroDemoUrl = 'http://127.0.0.1:5000'; 3 | 4 | export { apiUrl, douzeroDemoUrl }; 5 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | const suitMap = new Map([ 2 | ['H', 'hearts'], 3 | ['D', 'diams'], 4 | ['S', 'spades'], 5 | ['C', 'clubs'], 6 | ]); 7 | 8 | const suitMapSymbol = new Map([ 9 | ['H', '\u2665'], 10 | ['D', '\u2666'], 11 | ['S', '\u2660'], 12 | ['C', '\u2663'], 13 | ]); 14 | 15 | export function removeCards(cards, hands) { 16 | // remove cards from hands, return the remained hands 17 | let remainedHands = deepCopy(hands); 18 | // if the player's action is pass then return the copy of original hands 19 | if (cards === 'pass') { 20 | return remainedHands; 21 | } 22 | let misMatch = false; 23 | cards.forEach((card) => { 24 | let foundIdx = remainedHands.findIndex((element) => { 25 | return element === card; 26 | }); 27 | if (foundIdx > -1) { 28 | remainedHands.splice(foundIdx, 1); 29 | } else { 30 | misMatch = true; 31 | } 32 | }); 33 | if (misMatch) return false; 34 | else return remainedHands; 35 | } 36 | 37 | export function doubleRaf(callback) { 38 | // secure all the animation got rendered before callback function gets executed 39 | requestAnimationFrame(() => { 40 | requestAnimationFrame(callback); 41 | }); 42 | } 43 | 44 | export function deepCopy(toCopy) { 45 | return JSON.parse(JSON.stringify(toCopy)); 46 | } 47 | 48 | export function translateCardData(card) { 49 | let rankClass; 50 | let suitClass = ''; 51 | let rankText; 52 | let suitText = ''; 53 | // translate rank 54 | if (card === 'RJ') { 55 | rankClass = 'big'; 56 | rankText = '+'; 57 | suitClass = 'joker'; 58 | suitText = 'Joker'; 59 | } else if (card === 'BJ') { 60 | rankClass = 'little'; 61 | rankText = '-'; 62 | suitClass = 'joker'; 63 | suitText = 'Joker'; 64 | } else { 65 | rankClass = card.charAt(1) === 'T' ? `10` : card.charAt(1).toLowerCase(); 66 | rankClass = `rank-${rankClass}`; 67 | rankText = card.charAt(1) === 'T' ? `10` : card.charAt(1); 68 | } 69 | // translate suitClass 70 | if (card !== 'RJ' && card !== 'BJ') { 71 | suitClass = suitMap.get(card.charAt(0)); 72 | suitText = suitMapSymbol.get(card.charAt(0)); 73 | } 74 | 75 | return [rankClass, suitClass, rankText, suitText]; 76 | } 77 | 78 | export function millisecond2Second(t) { 79 | return Math.ceil(t / 1000); 80 | } 81 | 82 | export function debounce(func, wait, immediate) { 83 | let timeout; 84 | return function () { 85 | const context = this, 86 | args = arguments; 87 | const later = function () { 88 | timeout = null; 89 | if (!immediate) func.apply(context, args); 90 | }; 91 | const callNow = immediate && !timeout; 92 | clearTimeout(timeout); 93 | timeout = setTimeout(later, wait); 94 | if (callNow) func.apply(context, args); 95 | }; 96 | } 97 | 98 | export function computeHandCardsWidth(num, emWidth) { 99 | if (num === 0) return 0; 100 | return (num - 1) * 1.1 * emWidth + 4.3 * emWidth * 1.2 + 2; 101 | } 102 | 103 | export function card2SuiteAndRank(card) { 104 | if (card === 'BJ' || card === 'B') { 105 | return { suite: null, rank: 'X' }; 106 | } else if (card === 'RJ' || card === 'R') { 107 | return { suite: null, rank: 'D' }; 108 | } else { 109 | return { suite: card[0], rank: card[1] }; 110 | } 111 | } 112 | 113 | export const fullDoudizhuDeck = [ 114 | 'RJ', 115 | 'BJ', 116 | 'S2', 117 | 'C2', 118 | 'H2', 119 | 'D2', 120 | 'SA', 121 | 'CA', 122 | 'HA', 123 | 'DA', 124 | 'SK', 125 | 'CK', 126 | 'HK', 127 | 'DK', 128 | 'SQ', 129 | 'CQ', 130 | 'HQ', 131 | 'DQ', 132 | 'SJ', 133 | 'CJ', 134 | 'HJ', 135 | 'DJ', 136 | 'ST', 137 | 'CT', 138 | 'HT', 139 | 'DT', 140 | 'S9', 141 | 'C9', 142 | 'H9', 143 | 'D9', 144 | 'S8', 145 | 'C8', 146 | 'H8', 147 | 'D8', 148 | 'S7', 149 | 'C7', 150 | 'H7', 151 | 'D7', 152 | 'S6', 153 | 'C6', 154 | 'H6', 155 | 'D6', 156 | 'S5', 157 | 'C5', 158 | 'H5', 159 | 'D5', 160 | 'S4', 161 | 'C4', 162 | 'H4', 163 | 'D4', 164 | 'S3', 165 | 'C3', 166 | 'H3', 167 | 'D3', 168 | ]; 169 | 170 | export const fullDoudizhuDeckIndex = { 171 | RJ: 54, 172 | BJ: 53, 173 | S2: 52, 174 | C2: 51, 175 | H2: 50, 176 | D2: 49, 177 | SA: 48, 178 | CA: 47, 179 | HA: 46, 180 | DA: 45, 181 | SK: 44, 182 | CK: 43, 183 | HK: 42, 184 | DK: 41, 185 | SQ: 40, 186 | CQ: 39, 187 | HQ: 38, 188 | DQ: 37, 189 | SJ: 36, 190 | CJ: 35, 191 | HJ: 34, 192 | DJ: 33, 193 | ST: 32, 194 | CT: 31, 195 | HT: 30, 196 | DT: 29, 197 | S9: 28, 198 | C9: 27, 199 | H9: 26, 200 | D9: 25, 201 | S8: 24, 202 | C8: 23, 203 | H8: 22, 204 | D8: 21, 205 | S7: 20, 206 | C7: 19, 207 | H7: 18, 208 | D7: 17, 209 | S6: 16, 210 | C6: 15, 211 | H6: 14, 212 | D6: 13, 213 | S5: 12, 214 | C5: 11, 215 | H5: 10, 216 | D5: 9, 217 | S4: 8, 218 | C4: 7, 219 | H4: 6, 220 | D4: 5, 221 | S3: 4, 222 | C3: 3, 223 | H3: 2, 224 | D3: 1, 225 | }; 226 | 227 | export function sortDoudizhuCards(cards, ascending = false) { 228 | const cardsCopy = cards.slice(); 229 | return cardsCopy.sort((a, b) => { 230 | return ascending 231 | ? fullDoudizhuDeckIndex[a] - fullDoudizhuDeckIndex[b] 232 | : fullDoudizhuDeckIndex[b] - fullDoudizhuDeckIndex[a]; 233 | }); 234 | } 235 | 236 | export function isDoudizhuBomb(cards) { 237 | if (cards.length === 2) return (cards[0] === 'RJ' && cards[1] === 'BJ') || (cards[0] === 'BJ' && cards[1] === 'RJ'); 238 | if (cards.length === 4) 239 | return cards[0][1] === cards[1][1] && cards[0][1] === cards[2][1] && cards[0][1] === cards[3][1]; 240 | return false; 241 | } 242 | 243 | export function shuffleArray(inputArray) { 244 | let array = inputArray.slice(); 245 | for (let i = array.length - 1; i > 0; i--) { 246 | const j = Math.floor(Math.random() * (i + 1)); 247 | const temp = array[i]; 248 | array[i] = array[j]; 249 | array[j] = temp; 250 | } 251 | return array; 252 | } 253 | -------------------------------------------------------------------------------- /src/view/PvEView/index.js: -------------------------------------------------------------------------------- 1 | import PvEDoudizhuDemoView from './PvEDoudizhuDemoView'; 2 | 3 | export {PvEDoudizhuDemoView}; -------------------------------------------------------------------------------- /src/view/ReplayView/index.js: -------------------------------------------------------------------------------- 1 | import DoudizhuReplayView from "./DoudizhuReplayView"; 2 | import LeducHoldemReplayView from "./LeducHoldemReplayView"; 3 | 4 | export {DoudizhuReplayView, LeducHoldemReplayView}; 5 | --------------------------------------------------------------------------------