├── .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 | 
78 | 
79 | 
80 | 
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 |

24 |
{chipTitle} : undefined}
27 | label={chipLabel}
28 | color="primary"
29 | />
30 |
31 | ) : (
32 |
33 |

34 |
{chipTitle} : undefined}
37 | label={chipLabel}
38 | color="primary"
39 | />
40 |
41 | );
42 | } else
43 | return (
44 |
45 |

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 |

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 |
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 |
417 |
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 |

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 |
--------------------------------------------------------------------------------