Merge branch 'dmc' of https://github.com/datamllab/rlcard-showdown into dmc
# Conflicts: # README.md # requirements.txt # server/media/example_agents/leduc_holdem_dqn.zip
This commit is contained in:
commit
983a9c8b43
|
@ -31,3 +31,6 @@ uploaded_agents
|
||||||
/.idea
|
/.idea
|
||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
|
douzero_pretrained
|
||||||
|
dmc_pretrained
|
||||||
|
|
61
README.md
61
README.md
|
@ -1,32 +1,25 @@
|
||||||
# RLCard Showdown
|
# RLCard Showdown
|
||||||
This is the GUI support for the [RLCard](https://github.com/datamllab/rlcard) project. The project provides evaluation and visualization tools to help understand the performance of the agents. 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/). Have fun!
|
This is the GUI support for the [RLCard](https://github.com/datamllab/rlcard) project and 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!
|
||||||
|
|
||||||
* Official Website: [http://www.rlcard.org](http://www.rlcard.org)
|
* Official Website: [http://www.rlcard.org](http://www.rlcard.org)
|
||||||
* Tutorial in Jupyter Notebook: [https://github.com/datamllab/rlcard-tutorial](https://github.com/datamllab/rlcard-tutorial)
|
* Tutorial in Jupyter Notebook: [https://github.com/datamllab/rlcard-tutorial](https://github.com/datamllab/rlcard-tutorial)
|
||||||
* Paper: [https://www.ijcai.org/Proceedings/2020/764](https://www.ijcai.org/Proceedings/2020/764)
|
* Paper: [https://www.ijcai.org/Proceedings/2020/764](https://www.ijcai.org/Proceedings/2020/764)
|
||||||
* Document: [click here](docs/README.md)
|
* Document: [Click Here](docs/README.md)
|
||||||
|
|
||||||
## Cite this work
|
## Cite this work
|
||||||
If you find this repo useful, you may cite:
|
Zha, Daochen, Kwei-Herng Lai, Songyi Huang, Yuanpu Cao, Keerthana Reddy, Juan Vargas, Alex Nguyen, Ruzhe Wei, Junyu Guo, and Xia Hu. "RLCard: A Platform for Reinforcement Learning in Card Games." In IJCAI. 2020.
|
||||||
```bibtext
|
|
||||||
@inproceedings{ijcai2020-764,
|
```bibtex
|
||||||
title = {RLCard: A Platform for Reinforcement Learning in Card Games},
|
@inproceedings{zha2020rlcard,
|
||||||
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},
|
title={RLCard: A Platform for Reinforcement Learning in Card Games},
|
||||||
booktitle = {Proceedings of the Twenty-Ninth International Joint Conference on
|
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},
|
||||||
Artificial Intelligence, {IJCAI-20}},
|
booktitle={IJCAI},
|
||||||
publisher = {International Joint Conferences on Artificial Intelligence Organization},
|
year={2020}
|
||||||
editor = {Christian Bessiere},
|
|
||||||
pages = {5264--5266},
|
|
||||||
year = {2020},
|
|
||||||
month = {7},
|
|
||||||
note = {Demos}
|
|
||||||
doi = {10.24963/ijcai.2020/764},
|
|
||||||
url = {https://doi.org/10.24963/ijcai.2020/764},
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
RLCard-Showdown has separated frontend and backend. The frontend is built with React and the backend of leaderboard is based on Django.
|
RLCard-Showdown has separated frontend and backend. The frontend is built with React and the backend is based on Django and Flask.
|
||||||
|
|
||||||
### Prerequisite
|
### Prerequisite
|
||||||
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.
|
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.
|
||||||
|
@ -41,7 +34,7 @@ For backend, make sure that you have **Python 3.6+** and **pip** installed.
|
||||||
### Install Frontend and Backend
|
### Install Frontend and Backend
|
||||||
The frontend can be installed with the help of NPM:
|
The frontend can be installed with the help of NPM:
|
||||||
```
|
```
|
||||||
git clone --depth 1 https://github.com/datamllab/rlcard-showdown.git
|
git clone -b master --single-branch --depth=1 https://github.com/datamllab/rlcard-showdown.git
|
||||||
cd rlcard-showdown
|
cd rlcard-showdown
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
@ -54,27 +47,39 @@ cd ..
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run RLCard-Showdown
|
### Run RLCard-Showdown
|
||||||
Launch the backend of leaderboard with
|
1. Launch the backend of leaderboard with
|
||||||
```
|
```
|
||||||
cd server
|
cd server
|
||||||
python3 manage.py runserver
|
python3 manage.py runserver
|
||||||
```
|
```
|
||||||
Run the following command in a new terminal under the project folder to start frontend in development mode:
|
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`.
|
||||||
|
|
||||||
|
In a new terminal, start the PvE server (i.e., human vs AI) of DouZero with
|
||||||
|
```
|
||||||
|
cd pve_server
|
||||||
|
python3 run_douzero.py
|
||||||
|
```
|
||||||
|
Alternatively, you can start the PvE server interfaced with RLCard:
|
||||||
|
```
|
||||||
|
cd pve_server
|
||||||
|
python3 run_dmc.py
|
||||||
|
```
|
||||||
|
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).
|
||||||
|
|
||||||
|
3. Run the following command in another new terminal under the project folder to start frontend:
|
||||||
```
|
```
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
You can view frontend at [http://127.0.0.1:3000/](http://127.0.0.1:3000/). The backend of leaderboard will run in [http://127.0.0.1:8000/](http://127.0.0.1:8000/).
|
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/).
|
||||||
|
|
||||||
More documentation can be found [here](docs/api.md). User guide is [here](docs/guide.md).
|
## Demos
|
||||||
|
|
||||||
### Demos
|
|
||||||
![leaderboards](https://github.com/datamllab/rlcard-showdown/blob/master/docs/imgs/leaderboards.png?raw=true)
|
![leaderboards](https://github.com/datamllab/rlcard-showdown/blob/master/docs/imgs/leaderboards.png?raw=true)
|
||||||
![upload](https://github.com/datamllab/rlcard-showdown/blob/master/docs/imgs/upload.png?raw=true)
|
![upload](https://github.com/datamllab/rlcard-showdown/blob/master/docs/imgs/upload.png?raw=true)
|
||||||
![doudizhu-replay](https://github.com/datamllab/rlcard-showdown/blob/master/docs/imgs/doudizhu-replay.png?raw=true)
|
![doudizhu-replay](https://github.com/datamllab/rlcard-showdown/blob/master/docs/imgs/doudizhu-replay.png?raw=true)
|
||||||
![leduc-replay](https://github.com/datamllab/rlcard-showdown/blob/master/docs/imgs/leduc-replay.png?raw=true)
|
![leduc-replay](https://github.com/datamllab/rlcard-showdown/blob/master/docs/imgs/leduc-replay.png?raw=true)
|
||||||
|
|
||||||
### Contact Us
|
## Contact Us
|
||||||
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.
|
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.
|
||||||
|
|
||||||
### Acknowledgements
|
## Acknowledgements
|
||||||
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](mailto:lpa25@sfu.ca) for the help in visualizations.
|
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.
|
||||||
|
|
|
@ -38,7 +38,7 @@ The definitions of the fields are as follows:
|
||||||
| 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 |
|
| 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 |
|
||||||
| http://127.0.0.1:8000/tournament/query_payoff | Get all the payoffs |
|
| http://127.0.0.1:8000/tournament/query_payoff | Get all the payoffs |
|
||||||
| 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 |
|
| 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 |
|
||||||
| 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 |
|
| 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 |
|
||||||
| http://127.0.0.1:8000/tournament/list_uploaded_agents?game=leduc-holdem | List the uploaded agents of leduc-holdem |
|
| http://127.0.0.1:8000/tournament/list_uploaded_agents?game=leduc-holdem | List the uploaded agents of leduc-holdem |
|
||||||
| http://127.0.0.1:8000/tournament/list_baseline_agents?game=leduc-holdem | List the baseline agents of leduc-holdem |
|
| http://127.0.0.1:8000/tournament/list_baseline_agents?game=leduc-holdem | List the baseline agents of leduc-holdem |
|
||||||
| http://127.0.0.1:8000/tournament/download_examples?name=example_luduc_nfsp_model | Download the NFSP example model for Leduc Hold'em |
|
| http://127.0.0.1:8000/tournament/download_examples?name=example_luduc_nfsp_model | Download the NFSP example model for Leduc Hold'em |
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
*.swp
|
||||||
|
*.so
|
||||||
|
__pycache__
|
||||||
|
.DS_Store
|
||||||
|
*.egg-info
|
||||||
|
*.pyc
|
||||||
|
*.onnx
|
|
@ -0,0 +1,273 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import numpy as np
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
Card2Column = {3: 0, 4: 1, 5: 2, 6: 3, 7: 4, 8: 5, 9: 6, 10: 7,
|
||||||
|
11: 8, 12: 9, 13: 10, 14: 11, 17: 12, 20: 13, 30: 14}
|
||||||
|
|
||||||
|
NumOnes2Array = {0: np.array([0, 0, 0, 0]),
|
||||||
|
1: np.array([1, 0, 0, 0]),
|
||||||
|
2: np.array([1, 1, 0, 0]),
|
||||||
|
3: np.array([1, 1, 1, 0]),
|
||||||
|
4: np.array([1, 1, 1, 1])}
|
||||||
|
|
||||||
|
def _get_one_hot_bomb(bomb_num):
|
||||||
|
one_hot = np.zeros(15, dtype=np.float32)
|
||||||
|
one_hot[bomb_num] = 1
|
||||||
|
return one_hot
|
||||||
|
|
||||||
|
def _load_model(position, model_dir, use_onnx):
|
||||||
|
if not use_onnx or not os.path.isfile(os.path.join(model_dir, position+'.onnx')) :
|
||||||
|
from models import model_dict
|
||||||
|
model = model_dict[position]()
|
||||||
|
model_state_dict = model.state_dict()
|
||||||
|
model_path = os.path.join(model_dir, position+'.ckpt')
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
pretrained = torch.load(model_path, map_location='cuda:0')
|
||||||
|
else:
|
||||||
|
pretrained = torch.load(model_path, map_location='cpu')
|
||||||
|
pretrained = {k: v for k, v in pretrained.items() if k in model_state_dict}
|
||||||
|
model_state_dict.update(pretrained)
|
||||||
|
model.load_state_dict(model_state_dict)
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
model.cuda()
|
||||||
|
model.eval()
|
||||||
|
|
||||||
|
if use_onnx:
|
||||||
|
z = torch.randn(1, 5, 162, requires_grad=True)
|
||||||
|
if position == 'landlord':
|
||||||
|
x = torch.randn(1, 373, requires_grad=True)
|
||||||
|
else:
|
||||||
|
x = torch.randn(1, 484, requires_grad=True)
|
||||||
|
torch.onnx.export(model,
|
||||||
|
(z,x),
|
||||||
|
os.path.join(model_dir, position+'.onnx'),
|
||||||
|
export_params=True,
|
||||||
|
opset_version=10,
|
||||||
|
do_constant_folding=True,
|
||||||
|
input_names = ['z', 'x'],
|
||||||
|
output_names = ['y'],
|
||||||
|
dynamic_axes={'z' : {0 : 'batch_size'},
|
||||||
|
'x' : {0 : 'batch_size'},
|
||||||
|
'y' : {0 : 'batch_size'}})
|
||||||
|
|
||||||
|
if use_onnx:
|
||||||
|
import onnxruntime
|
||||||
|
model = onnxruntime.InferenceSession(os.path.join(model_dir, position+'.onnx'))
|
||||||
|
return model
|
||||||
|
|
||||||
|
def _process_action_seq(sequence, length=15):
|
||||||
|
sequence = sequence[-length:].copy()
|
||||||
|
if len(sequence) < length:
|
||||||
|
empty_sequence = [[] for _ in range(length - len(sequence))]
|
||||||
|
empty_sequence.extend(sequence)
|
||||||
|
sequence = empty_sequence
|
||||||
|
return sequence
|
||||||
|
|
||||||
|
class DeepAgent:
|
||||||
|
|
||||||
|
def __init__(self, position, model_dir, use_onnx=False):
|
||||||
|
self.model = _load_model(position, model_dir, use_onnx)
|
||||||
|
self.use_onnx = use_onnx
|
||||||
|
|
||||||
|
def cards2array(self, list_cards):
|
||||||
|
if len(list_cards) == 0:
|
||||||
|
return np.zeros(54, dtype=np.float32)
|
||||||
|
|
||||||
|
matrix = np.zeros([4, 13], dtype=np.float32)
|
||||||
|
jokers = np.zeros(2, dtype=np.float32)
|
||||||
|
counter = Counter(list_cards)
|
||||||
|
for card, num_times in counter.items():
|
||||||
|
if card < 20:
|
||||||
|
matrix[:, Card2Column[card]] = NumOnes2Array[num_times]
|
||||||
|
elif card == 20:
|
||||||
|
jokers[0] = 1
|
||||||
|
elif card == 30:
|
||||||
|
jokers[1] = 1
|
||||||
|
return np.concatenate((matrix.flatten('F'), jokers))
|
||||||
|
|
||||||
|
def get_one_hot_array(self, num_left_cards, max_num_cards):
|
||||||
|
one_hot = np.zeros(max_num_cards, dtype=np.float32)
|
||||||
|
one_hot[num_left_cards - 1] = 1
|
||||||
|
|
||||||
|
return one_hot
|
||||||
|
|
||||||
|
def action_seq_list2array(self, action_seq_list):
|
||||||
|
action_seq_array = np.zeros((len(action_seq_list), 54), dtype=np.float32)
|
||||||
|
for row, list_cards in enumerate(action_seq_list):
|
||||||
|
action_seq_array[row, :] = self.cards2array(list_cards)
|
||||||
|
action_seq_array = action_seq_array.reshape(5, 162)
|
||||||
|
return action_seq_array
|
||||||
|
|
||||||
|
def act(self, infoset):
|
||||||
|
player_position = infoset.player_position
|
||||||
|
num_legal_actions = len(infoset.legal_actions)
|
||||||
|
my_handcards = self.cards2array(infoset.player_hand_cards)
|
||||||
|
my_handcards_batch = np.repeat(my_handcards[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
|
||||||
|
other_handcards = self.cards2array(infoset.other_hand_cards)
|
||||||
|
other_handcards_batch = np.repeat(other_handcards[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
|
||||||
|
my_action_batch = np.zeros(my_handcards_batch.shape, dtype=np.float32)
|
||||||
|
for j, action in enumerate(infoset.legal_actions):
|
||||||
|
my_action_batch[j, :] = self.cards2array(action)
|
||||||
|
|
||||||
|
last_action = self.cards2array(infoset.rival_move)
|
||||||
|
last_action_batch = np.repeat(last_action[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
|
||||||
|
if player_position == 0:
|
||||||
|
landlord_up_num_cards_left = self.get_one_hot_array(
|
||||||
|
infoset.num_cards_left[2], 17)
|
||||||
|
landlord_up_num_cards_left_batch = np.repeat(
|
||||||
|
landlord_up_num_cards_left[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
|
||||||
|
landlord_down_num_cards_left = self.get_one_hot_array(
|
||||||
|
infoset.num_cards_left[1], 17)
|
||||||
|
landlord_down_num_cards_left_batch = np.repeat(
|
||||||
|
landlord_down_num_cards_left[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
|
||||||
|
landlord_up_played_cards = self.cards2array(
|
||||||
|
infoset.played_cards[2])
|
||||||
|
landlord_up_played_cards_batch = np.repeat(
|
||||||
|
landlord_up_played_cards[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
|
||||||
|
landlord_down_played_cards = self.cards2array(
|
||||||
|
infoset.played_cards[1])
|
||||||
|
landlord_down_played_cards_batch = np.repeat(
|
||||||
|
landlord_down_played_cards[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
|
||||||
|
bomb_num = _get_one_hot_bomb(
|
||||||
|
infoset.bomb_num)
|
||||||
|
bomb_num_batch = np.repeat(
|
||||||
|
bomb_num[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
|
||||||
|
x_batch = np.hstack((my_handcards_batch,
|
||||||
|
other_handcards_batch,
|
||||||
|
last_action_batch,
|
||||||
|
landlord_up_played_cards_batch,
|
||||||
|
landlord_down_played_cards_batch,
|
||||||
|
landlord_up_num_cards_left_batch,
|
||||||
|
landlord_down_num_cards_left_batch,
|
||||||
|
bomb_num_batch,
|
||||||
|
my_action_batch))
|
||||||
|
z = self.action_seq_list2array(_process_action_seq(
|
||||||
|
infoset.card_play_action_seq))
|
||||||
|
z_batch = np.repeat(
|
||||||
|
z[np.newaxis, :, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
if self.use_onnx:
|
||||||
|
ort_inputs = {'z': z_batch, 'x': x_batch}
|
||||||
|
y_pred = self.model.run(None, ort_inputs)[0]
|
||||||
|
elif torch.cuda.is_available():
|
||||||
|
y_pred = self.model.forward(torch.from_numpy(z_batch).float().cuda(),
|
||||||
|
torch.from_numpy(x_batch).float().cuda())
|
||||||
|
y_pred = y_pred.cpu().detach().numpy()
|
||||||
|
else:
|
||||||
|
y_pred = self.model.forward(torch.from_numpy(z_batch).float(),
|
||||||
|
torch.from_numpy(x_batch).float())
|
||||||
|
y_pred = y_pred.detach().numpy()
|
||||||
|
else:
|
||||||
|
last_landlord_action = self.cards2array(
|
||||||
|
infoset.last_moves[0])
|
||||||
|
last_landlord_action_batch = np.repeat(
|
||||||
|
last_landlord_action[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
landlord_num_cards_left = self.get_one_hot_array(
|
||||||
|
infoset.num_cards_left[0], 20)
|
||||||
|
landlord_num_cards_left_batch = np.repeat(
|
||||||
|
landlord_num_cards_left[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
|
||||||
|
landlord_played_cards = self.cards2array(
|
||||||
|
infoset.played_cards[0])
|
||||||
|
landlord_played_cards_batch = np.repeat(
|
||||||
|
landlord_played_cards[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
|
||||||
|
if player_position == 2:
|
||||||
|
last_teammate_action = self.cards2array(
|
||||||
|
infoset.last_moves[1])
|
||||||
|
last_teammate_action_batch = np.repeat(
|
||||||
|
last_teammate_action[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
teammate_num_cards_left = self.get_one_hot_array(
|
||||||
|
infoset.num_cards_left[1], 17)
|
||||||
|
teammate_num_cards_left_batch = np.repeat(
|
||||||
|
teammate_num_cards_left[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
|
||||||
|
teammate_played_cards = self.cards2array(
|
||||||
|
infoset.played_cards[1])
|
||||||
|
teammate_played_cards_batch = np.repeat(
|
||||||
|
teammate_played_cards[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
|
||||||
|
else:
|
||||||
|
last_teammate_action = self.cards2array(
|
||||||
|
infoset.last_moves[2])
|
||||||
|
last_teammate_action_batch = np.repeat(
|
||||||
|
last_teammate_action[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
teammate_num_cards_left = self.get_one_hot_array(
|
||||||
|
infoset.num_cards_left[2], 17)
|
||||||
|
teammate_num_cards_left_batch = np.repeat(
|
||||||
|
teammate_num_cards_left[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
|
||||||
|
teammate_played_cards = self.cards2array(
|
||||||
|
infoset.played_cards[2])
|
||||||
|
teammate_played_cards_batch = np.repeat(
|
||||||
|
teammate_played_cards[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
|
||||||
|
bomb_num = _get_one_hot_bomb(
|
||||||
|
infoset.bomb_num)
|
||||||
|
bomb_num_batch = np.repeat(
|
||||||
|
bomb_num[np.newaxis, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
x_batch = np.hstack((my_handcards_batch,
|
||||||
|
other_handcards_batch,
|
||||||
|
landlord_played_cards_batch,
|
||||||
|
teammate_played_cards_batch,
|
||||||
|
last_action_batch,
|
||||||
|
last_landlord_action_batch,
|
||||||
|
last_teammate_action_batch,
|
||||||
|
landlord_num_cards_left_batch,
|
||||||
|
teammate_num_cards_left_batch,
|
||||||
|
bomb_num_batch,
|
||||||
|
my_action_batch))
|
||||||
|
z = self.action_seq_list2array(_process_action_seq(infoset.card_play_action_seq))
|
||||||
|
z_batch = np.repeat(
|
||||||
|
z[np.newaxis, :, :],
|
||||||
|
num_legal_actions, axis=0)
|
||||||
|
if self.use_onnx:
|
||||||
|
ort_inputs = {'z': z_batch, 'x': x_batch}
|
||||||
|
y_pred = self.model.run(None, ort_inputs)[0]
|
||||||
|
elif torch.cuda.is_available():
|
||||||
|
y_pred = self.model.forward(torch.from_numpy(z_batch).float().cuda(),
|
||||||
|
torch.from_numpy(x_batch).float().cuda())
|
||||||
|
y_pred = y_pred.cpu().detach().numpy()
|
||||||
|
else:
|
||||||
|
y_pred = self.model.forward(torch.from_numpy(z_batch).float(),
|
||||||
|
torch.from_numpy(x_batch).float())
|
||||||
|
y_pred = y_pred.detach().numpy()
|
||||||
|
|
||||||
|
y_pred = y_pred.flatten()
|
||||||
|
|
||||||
|
#best_action_index = np.argmax(y_pred, axis=0)[0]
|
||||||
|
size = min(3, len(y_pred))
|
||||||
|
best_action_index = np.argpartition(y_pred, -size)[-size:]
|
||||||
|
best_action_confidence = y_pred[best_action_index]
|
||||||
|
best_action = [infoset.legal_actions[index] for index in best_action_index]
|
||||||
|
|
||||||
|
return best_action, best_action_confidence
|
|
@ -0,0 +1,67 @@
|
||||||
|
import torch
|
||||||
|
from torch import nn
|
||||||
|
|
||||||
|
class LandlordLstmModel(nn.Module):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.lstm = nn.LSTM(162, 128, batch_first = True)
|
||||||
|
self.dense1 = nn.Linear(373 + 128, 512)
|
||||||
|
self.dense2 = nn.Linear(512, 512)
|
||||||
|
self.dense3 = nn.Linear(512, 512)
|
||||||
|
self.dense4 = nn.Linear(512, 512)
|
||||||
|
self.dense5 = nn.Linear(512, 512)
|
||||||
|
self.dense5 = nn.Linear(512, 512)
|
||||||
|
self.dense5 = nn.Linear(512, 512)
|
||||||
|
self.dense6 = nn.Linear(512, 1)
|
||||||
|
|
||||||
|
def forward(self, z, x):
|
||||||
|
lstm_out, (h_n, _) = self.lstm(z)
|
||||||
|
lstm_out = lstm_out[:,-1,:]
|
||||||
|
x = torch.cat([lstm_out,x], dim=-1)
|
||||||
|
x = self.dense1(x)
|
||||||
|
x = torch.relu(x)
|
||||||
|
x = self.dense2(x)
|
||||||
|
x = torch.relu(x)
|
||||||
|
x = self.dense3(x)
|
||||||
|
x = torch.relu(x)
|
||||||
|
x = self.dense4(x)
|
||||||
|
x = torch.relu(x)
|
||||||
|
x = self.dense5(x)
|
||||||
|
x = torch.relu(x)
|
||||||
|
x = self.dense6(x)
|
||||||
|
return x
|
||||||
|
|
||||||
|
class FarmerLstmModel(nn.Module):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.lstm = nn.LSTM(162, 128, batch_first = True)
|
||||||
|
self.dense1 = nn.Linear(484 + 128 , 512)
|
||||||
|
self.dense2 = nn.Linear(512, 512)
|
||||||
|
self.dense3 = nn.Linear(512, 512)
|
||||||
|
self.dense4 = nn.Linear(512, 512)
|
||||||
|
self.dense4 = nn.Linear(512, 512)
|
||||||
|
self.dense4 = nn.Linear(512, 512)
|
||||||
|
self.dense5 = nn.Linear(512, 512)
|
||||||
|
self.dense6 = nn.Linear(512, 1)
|
||||||
|
|
||||||
|
def forward(self, z, x):
|
||||||
|
lstm_out, (h_n, _) = self.lstm(z)
|
||||||
|
lstm_out = lstm_out[:,-1,:]
|
||||||
|
x = torch.cat([lstm_out,x], dim=-1)
|
||||||
|
x = self.dense1(x)
|
||||||
|
x = torch.relu(x)
|
||||||
|
x = self.dense2(x)
|
||||||
|
x = torch.relu(x)
|
||||||
|
x = self.dense3(x)
|
||||||
|
x = torch.relu(x)
|
||||||
|
x = self.dense4(x)
|
||||||
|
x = torch.relu(x)
|
||||||
|
x = self.dense5(x)
|
||||||
|
x = torch.relu(x)
|
||||||
|
x = self.dense6(x)
|
||||||
|
return x
|
||||||
|
|
||||||
|
model_dict = {}
|
||||||
|
model_dict['landlord'] = LandlordLstmModel
|
||||||
|
model_dict['landlord_up'] = FarmerLstmModel
|
||||||
|
model_dict['landlord_down'] = FarmerLstmModel
|
|
@ -0,0 +1,384 @@
|
||||||
|
import os
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import numpy as np
|
||||||
|
from heapq import nlargest
|
||||||
|
from collections import Counter, OrderedDict
|
||||||
|
from flask import Flask, jsonify, request
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
from utils.move_generator import MovesGener
|
||||||
|
from utils import move_detector as md, move_selector as ms
|
||||||
|
|
||||||
|
import rlcard
|
||||||
|
env = rlcard.make('doudizhu')
|
||||||
|
|
||||||
|
DouZeroCard2RLCard = {3: '3', 4: '4', 5: '5', 6: '6', 7: '7',
|
||||||
|
8: '8', 9: '9', 10: 'T', 11: 'J', 12: 'Q',
|
||||||
|
13: 'K', 14: 'A', 17: '2', 20: 'B', 30: 'R'}
|
||||||
|
|
||||||
|
RLCard2DouZeroCard = {'3': 3, '4': 4, '5': 5, '6': 6, '7': 7,
|
||||||
|
'8': 8, '9': 9, 'T': 10, 'J': 11, 'Q': 12,
|
||||||
|
'K': 13, 'A': 14, '2': 17, 'B': 20, 'R': 30}
|
||||||
|
|
||||||
|
EnvCard2RealCard = {'3': '3', '4':'4', '5': '5', '6': '6', '7': '7',
|
||||||
|
'8': '8', '9': '9', 'T': 'T', 'J': 'J', 'Q': 'Q',
|
||||||
|
'K': 'K', 'A': 'A', '2': '2', 'B': 'X', 'R': 'D'}
|
||||||
|
|
||||||
|
RealCard2EnvCard = {'3': '3', '4':'4', '5': '5', '6': '6', '7': '7',
|
||||||
|
'8': '8', '9': '9', 'T': 'T', 'J': 'J', 'Q': 'Q',
|
||||||
|
'K': 'K', 'A': 'A', '2': '2', 'X': 'B', 'D': 'R'}
|
||||||
|
|
||||||
|
pretrained_dir = 'pretrained/dmc_pretrained'
|
||||||
|
device = torch.device('cpu')
|
||||||
|
players = []
|
||||||
|
for i in range(3):
|
||||||
|
model_path = os.path.join(pretrained_dir, str(i)+'.pth')
|
||||||
|
agent = torch.load(model_path, map_location=device)
|
||||||
|
agent.set_device(device)
|
||||||
|
players.append(agent)
|
||||||
|
|
||||||
|
@app.route('/predict', methods=['POST'])
|
||||||
|
def predict():
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
# Player postion
|
||||||
|
player_position = request.form.get('player_position')
|
||||||
|
if player_position not in ['0', '1', '2']:
|
||||||
|
return jsonify({'status': 1, 'message': 'player_position must be 0, 1, or 2'})
|
||||||
|
player_position = int(player_position)
|
||||||
|
|
||||||
|
# Player hand cards
|
||||||
|
player_hand_cards = ''.join([RealCard2EnvCard[c] for c in request.form.get('player_hand_cards')])
|
||||||
|
if player_position == 0:
|
||||||
|
if len(player_hand_cards) < 1 or len(player_hand_cards) > 20:
|
||||||
|
return jsonify({'status': 2, 'message': 'the number of hand cards should be 1-20'})
|
||||||
|
else:
|
||||||
|
if len(player_hand_cards) < 1 or len(player_hand_cards) > 17:
|
||||||
|
return jsonify({'status': 3, 'message': 'the number of hand cards should be 1-17'})
|
||||||
|
|
||||||
|
# Number cards left
|
||||||
|
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'))]
|
||||||
|
if num_cards_left[player_position] != len(player_hand_cards):
|
||||||
|
return jsonify({'status': 4, 'message': 'the number of cards left do not align with hand cards'})
|
||||||
|
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:
|
||||||
|
return jsonify({'status': 5, 'message': 'the number of cards left not in range'})
|
||||||
|
|
||||||
|
# Three landlord cards
|
||||||
|
three_landlord_cards = ''.join([RealCard2EnvCard[c] for c in request.form.get('three_landlord_cards')])
|
||||||
|
if len(three_landlord_cards) < 0 or len(three_landlord_cards) > 3:
|
||||||
|
return jsonify({'status': 6, 'message': 'the number of landlord cards should be 0-3'})
|
||||||
|
|
||||||
|
# Card play sequence
|
||||||
|
if request.form.get('card_play_action_seq') == '':
|
||||||
|
card_play_action_seq = []
|
||||||
|
else:
|
||||||
|
tmp_seq = [''.join([RealCard2EnvCard[c] for c in cards]) for cards in request.form.get('card_play_action_seq').split(',')]
|
||||||
|
for i in range(len(tmp_seq)):
|
||||||
|
if tmp_seq[i] == '':
|
||||||
|
tmp_seq[i] = 'pass'
|
||||||
|
card_play_action_seq = []
|
||||||
|
for i in range(len(tmp_seq)):
|
||||||
|
card_play_action_seq.append((i%3, tmp_seq[i]))
|
||||||
|
|
||||||
|
# Other hand cards
|
||||||
|
other_hand_cards = ''.join([RealCard2EnvCard[c] for c in request.form.get('other_hand_cards')])
|
||||||
|
if len(other_hand_cards) != sum(num_cards_left) - num_cards_left[player_position]:
|
||||||
|
return jsonify({'status': 7, 'message': 'the number of the other hand cards do not align with the number of cards left'})
|
||||||
|
|
||||||
|
# Played cards
|
||||||
|
played_cards = []
|
||||||
|
for field in ['played_cards_landlord', 'played_cards_landlord_down', 'played_cards_landlord_up']:
|
||||||
|
played_cards.append(''.join([RealCard2EnvCard[c] for c in request.form.get(field)]))
|
||||||
|
|
||||||
|
# RLCard state
|
||||||
|
state = {}
|
||||||
|
state['current_hand'] = player_hand_cards
|
||||||
|
state['landlord'] = 0
|
||||||
|
state['num_cards_left'] = num_cards_left
|
||||||
|
state['others_hand'] = other_hand_cards
|
||||||
|
state['played_cards'] = played_cards
|
||||||
|
state['seen_cards'] = three_landlord_cards
|
||||||
|
state['self'] = player_position
|
||||||
|
state['trace'] = card_play_action_seq
|
||||||
|
|
||||||
|
# Get rival move and legal_actions
|
||||||
|
rival_move = 'pass'
|
||||||
|
if len(card_play_action_seq) != 0:
|
||||||
|
if card_play_action_seq[-1][1] == 'pass':
|
||||||
|
rival_move = card_play_action_seq[-2][1]
|
||||||
|
else:
|
||||||
|
rival_move = card_play_action_seq[-1][1]
|
||||||
|
if rival_move == 'pass':
|
||||||
|
rival_move = ''
|
||||||
|
rival_move = [RLCard2DouZeroCard[c] for c in rival_move]
|
||||||
|
state['actions'] = _get_legal_card_play_actions([RLCard2DouZeroCard[c] for c in player_hand_cards], rival_move)
|
||||||
|
state['actions'] = [''.join([DouZeroCard2RLCard[c] for c in a]) for a in state['actions']]
|
||||||
|
for i in range(len(state['actions'])):
|
||||||
|
if state['actions'][i] == '':
|
||||||
|
state['actions'][i] = 'pass'
|
||||||
|
|
||||||
|
|
||||||
|
# Prediction
|
||||||
|
state = _extract_state(state)
|
||||||
|
action, info = players[player_position].eval_step(state)
|
||||||
|
if action == 'pass':
|
||||||
|
action = ''
|
||||||
|
for i in info['values']:
|
||||||
|
if i == 'pass':
|
||||||
|
info['values'][''] = info['values']['pass']
|
||||||
|
del info['values']['pass']
|
||||||
|
|
||||||
|
actions = nlargest(3, info['values'], key=info['values'].get)
|
||||||
|
actions_confidence = [info['values'].get(action) for action in actions]
|
||||||
|
actions = [''.join([EnvCard2RealCard[c] for c in action]) for action in actions]
|
||||||
|
result = {}
|
||||||
|
win_rates = {}
|
||||||
|
for i in range(len(actions)):
|
||||||
|
# Here, we calculate the win rate
|
||||||
|
win_rate = min(actions_confidence[i], 1)
|
||||||
|
win_rate = max(win_rate, 0)
|
||||||
|
win_rates[actions[i]] = str(round(win_rate, 4))
|
||||||
|
result[actions[i]] = str(round(actions_confidence[i], 6))
|
||||||
|
|
||||||
|
############## DEBUG ################
|
||||||
|
if app.debug:
|
||||||
|
print('--------------- DEBUG START --------------')
|
||||||
|
command = 'curl --data "'
|
||||||
|
parameters = []
|
||||||
|
for key in request.form:
|
||||||
|
parameters.append(key+'='+request.form.get(key))
|
||||||
|
print(key+':', request.form.get(key))
|
||||||
|
command += '&'.join(parameters)
|
||||||
|
command += '" "http://127.0.0.1:5000/predict"'
|
||||||
|
print('Command:', command)
|
||||||
|
print('Rival Move:', rival_move)
|
||||||
|
print('legal_actions:', state['legal_actions'])
|
||||||
|
print('Result:', result)
|
||||||
|
print('--------------- DEBUG END --------------')
|
||||||
|
############## DEBUG ################
|
||||||
|
return jsonify({'status': 0, 'message': 'success', 'result': result, 'win_rates': win_rates})
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({'status': -1, 'message': 'unkown error'})
|
||||||
|
|
||||||
|
@app.route('/legal', methods=['POST'])
|
||||||
|
def legal():
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
player_hand_cards = [RealCard2EnvCard[c] for c in request.form.get('player_hand_cards')]
|
||||||
|
rival_move = [RealCard2EnvCard[c] for c in request.form.get('rival_move')]
|
||||||
|
if rival_move == '':
|
||||||
|
rival_move = 'pass'
|
||||||
|
player_hand_cards = [RLCard2DouZeroCard[c] for c in player_hand_cards]
|
||||||
|
rival_move = [RLCard2DouZeroCard[c] for c in rival_move]
|
||||||
|
legal_actions = _get_legal_card_play_actions(player_hand_cards, rival_move)
|
||||||
|
legal_actions = [''.join([DouZeroCard2RLCard[c] for c in a]) for a in legal_actions]
|
||||||
|
for i in range(len(legal_actions)):
|
||||||
|
if legal_actions[i] == 'pass':
|
||||||
|
legal_actions[i] = ''
|
||||||
|
legal_actions = ','.join([''.join([EnvCard2RealCard[c] for c in action]) for action in legal_actions])
|
||||||
|
return jsonify({'status': 0, 'message': 'success', 'legal_action': legal_actions})
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({'status': -1, 'message': 'unkown error'})
|
||||||
|
|
||||||
|
def _extract_state(state):
|
||||||
|
current_hand = _cards2array(state['current_hand'])
|
||||||
|
others_hand = _cards2array(state['others_hand'])
|
||||||
|
|
||||||
|
last_action = ''
|
||||||
|
if len(state['trace']) != 0:
|
||||||
|
if state['trace'][-1][1] == 'pass':
|
||||||
|
last_action = state['trace'][-2][1]
|
||||||
|
else:
|
||||||
|
last_action = state['trace'][-1][1]
|
||||||
|
last_action = _cards2array(last_action)
|
||||||
|
|
||||||
|
last_9_actions = _action_seq2array(_process_action_seq(state['trace']))
|
||||||
|
|
||||||
|
if state['self'] == 0: # landlord
|
||||||
|
landlord_up_played_cards = _cards2array(state['played_cards'][2])
|
||||||
|
landlord_down_played_cards = _cards2array(state['played_cards'][1])
|
||||||
|
landlord_up_num_cards_left = _get_one_hot_array(state['num_cards_left'][2], 17)
|
||||||
|
landlord_down_num_cards_left = _get_one_hot_array(state['num_cards_left'][1], 17)
|
||||||
|
obs = np.concatenate((current_hand,
|
||||||
|
others_hand,
|
||||||
|
last_action,
|
||||||
|
last_9_actions,
|
||||||
|
landlord_up_played_cards,
|
||||||
|
landlord_down_played_cards,
|
||||||
|
landlord_up_num_cards_left,
|
||||||
|
landlord_down_num_cards_left))
|
||||||
|
else:
|
||||||
|
landlord_played_cards = _cards2array(state['played_cards'][0])
|
||||||
|
for i, action in reversed(state['trace']):
|
||||||
|
if i == 0:
|
||||||
|
last_landlord_action = action
|
||||||
|
last_landlord_action = _cards2array(last_landlord_action)
|
||||||
|
landlord_num_cards_left = _get_one_hot_array(state['num_cards_left'][0], 20)
|
||||||
|
|
||||||
|
teammate_id = 3 - state['self']
|
||||||
|
teammate_played_cards = _cards2array(state['played_cards'][teammate_id])
|
||||||
|
last_teammate_action = 'pass'
|
||||||
|
for i, action in reversed(state['trace']):
|
||||||
|
if i == teammate_id:
|
||||||
|
last_teammate_action = action
|
||||||
|
last_teammate_action = _cards2array(last_teammate_action)
|
||||||
|
teammate_num_cards_left = _get_one_hot_array(state['num_cards_left'][teammate_id], 17)
|
||||||
|
obs = np.concatenate((current_hand,
|
||||||
|
others_hand,
|
||||||
|
last_action,
|
||||||
|
last_9_actions,
|
||||||
|
landlord_played_cards,
|
||||||
|
teammate_played_cards,
|
||||||
|
last_landlord_action,
|
||||||
|
last_teammate_action,
|
||||||
|
landlord_num_cards_left,
|
||||||
|
teammate_num_cards_left))
|
||||||
|
|
||||||
|
legal_actions = {env._ACTION_2_ID[action]: _cards2array(action) for action in state['actions']}
|
||||||
|
extracted_state = OrderedDict({'obs': obs, 'legal_actions': legal_actions})
|
||||||
|
extracted_state['raw_obs'] = state
|
||||||
|
extracted_state['raw_legal_actions'] = [a for a in state['actions']]
|
||||||
|
return extracted_state
|
||||||
|
|
||||||
|
def _get_legal_card_play_actions(player_hand_cards, rival_move):
|
||||||
|
mg = MovesGener(player_hand_cards)
|
||||||
|
|
||||||
|
rival_type = md.get_move_type(rival_move)
|
||||||
|
rival_move_type = rival_type['type']
|
||||||
|
rival_move_len = rival_type.get('len', 1)
|
||||||
|
moves = list()
|
||||||
|
|
||||||
|
if rival_move_type == md.TYPE_0_PASS:
|
||||||
|
moves = mg.gen_moves()
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_1_SINGLE:
|
||||||
|
all_moves = mg.gen_type_1_single()
|
||||||
|
moves = ms.filter_type_1_single(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_2_PAIR:
|
||||||
|
all_moves = mg.gen_type_2_pair()
|
||||||
|
moves = ms.filter_type_2_pair(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_3_TRIPLE:
|
||||||
|
all_moves = mg.gen_type_3_triple()
|
||||||
|
moves = ms.filter_type_3_triple(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_4_BOMB:
|
||||||
|
all_moves = mg.gen_type_4_bomb() + mg.gen_type_5_king_bomb()
|
||||||
|
moves = ms.filter_type_4_bomb(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_5_KING_BOMB:
|
||||||
|
moves = []
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_6_3_1:
|
||||||
|
all_moves = mg.gen_type_6_3_1()
|
||||||
|
moves = ms.filter_type_6_3_1(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_7_3_2:
|
||||||
|
all_moves = mg.gen_type_7_3_2()
|
||||||
|
moves = ms.filter_type_7_3_2(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_8_SERIAL_SINGLE:
|
||||||
|
all_moves = mg.gen_type_8_serial_single(repeat_num=rival_move_len)
|
||||||
|
moves = ms.filter_type_8_serial_single(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_9_SERIAL_PAIR:
|
||||||
|
all_moves = mg.gen_type_9_serial_pair(repeat_num=rival_move_len)
|
||||||
|
moves = ms.filter_type_9_serial_pair(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_10_SERIAL_TRIPLE:
|
||||||
|
all_moves = mg.gen_type_10_serial_triple(repeat_num=rival_move_len)
|
||||||
|
moves = ms.filter_type_10_serial_triple(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_11_SERIAL_3_1:
|
||||||
|
all_moves = mg.gen_type_11_serial_3_1(repeat_num=rival_move_len)
|
||||||
|
moves = ms.filter_type_11_serial_3_1(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_12_SERIAL_3_2:
|
||||||
|
all_moves = mg.gen_type_12_serial_3_2(repeat_num=rival_move_len)
|
||||||
|
moves = ms.filter_type_12_serial_3_2(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_13_4_2:
|
||||||
|
all_moves = mg.gen_type_13_4_2()
|
||||||
|
moves = ms.filter_type_13_4_2(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_14_4_22:
|
||||||
|
all_moves = mg.gen_type_14_4_22()
|
||||||
|
moves = ms.filter_type_14_4_22(all_moves, rival_move)
|
||||||
|
|
||||||
|
if rival_move_type not in [md.TYPE_0_PASS,
|
||||||
|
md.TYPE_4_BOMB, md.TYPE_5_KING_BOMB]:
|
||||||
|
moves = moves + mg.gen_type_4_bomb() + mg.gen_type_5_king_bomb()
|
||||||
|
|
||||||
|
if len(rival_move) != 0: # rival_move is not 'pass'
|
||||||
|
moves = moves + [[]]
|
||||||
|
|
||||||
|
for m in moves:
|
||||||
|
m.sort()
|
||||||
|
|
||||||
|
moves.sort()
|
||||||
|
moves = list(move for move,_ in itertools.groupby(moves))
|
||||||
|
|
||||||
|
return moves
|
||||||
|
|
||||||
|
Card2Column = {'3': 0, '4': 1, '5': 2, '6': 3, '7': 4, '8': 5, '9': 6, 'T': 7,
|
||||||
|
'J': 8, 'Q': 9, 'K': 10, 'A': 11, '2': 12}
|
||||||
|
|
||||||
|
NumOnes2Array = {0: np.array([0, 0, 0, 0]),
|
||||||
|
1: np.array([1, 0, 0, 0]),
|
||||||
|
2: np.array([1, 1, 0, 0]),
|
||||||
|
3: np.array([1, 1, 1, 0]),
|
||||||
|
4: np.array([1, 1, 1, 1])}
|
||||||
|
|
||||||
|
def _cards2array(cards):
|
||||||
|
if cards == 'pass':
|
||||||
|
return np.zeros(54, dtype=np.int8)
|
||||||
|
|
||||||
|
matrix = np.zeros([4, 13], dtype=np.int8)
|
||||||
|
jokers = np.zeros(2, dtype=np.int8)
|
||||||
|
counter = Counter(cards)
|
||||||
|
for card, num_times in counter.items():
|
||||||
|
if card == 'B':
|
||||||
|
jokers[0] = 1
|
||||||
|
elif card == 'R':
|
||||||
|
jokers[1] = 1
|
||||||
|
else:
|
||||||
|
matrix[:, Card2Column[card]] = NumOnes2Array[num_times]
|
||||||
|
return np.concatenate((matrix.flatten('F'), jokers))
|
||||||
|
|
||||||
|
def _get_one_hot_array(num_left_cards, max_num_cards):
|
||||||
|
one_hot = np.zeros(max_num_cards, dtype=np.int8)
|
||||||
|
one_hot[num_left_cards - 1] = 1
|
||||||
|
|
||||||
|
return one_hot
|
||||||
|
|
||||||
|
def _action_seq2array(action_seq_list):
|
||||||
|
action_seq_array = np.zeros((len(action_seq_list), 54), np.int8)
|
||||||
|
for row, cards in enumerate(action_seq_list):
|
||||||
|
action_seq_array[row, :] = _cards2array(cards)
|
||||||
|
action_seq_array = action_seq_array.flatten()
|
||||||
|
return action_seq_array
|
||||||
|
|
||||||
|
def _process_action_seq(sequence, length=9):
|
||||||
|
sequence = [action[1] for action in sequence[-length:]]
|
||||||
|
if len(sequence) < length:
|
||||||
|
empty_sequence = ['' for _ in range(length - len(sequence))]
|
||||||
|
empty_sequence.extend(sequence)
|
||||||
|
sequence = empty_sequence
|
||||||
|
return sequence
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description='DouZero backend')
|
||||||
|
parser.add_argument('--debug', action='store_true')
|
||||||
|
args = parser.parse_args()
|
||||||
|
app.run(debug=args.debug)
|
|
@ -0,0 +1,252 @@
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from flask import Flask, jsonify, request
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
from utils.move_generator import MovesGener
|
||||||
|
from utils import move_detector as md, move_selector as ms
|
||||||
|
from deep import DeepAgent
|
||||||
|
|
||||||
|
EnvCard2RealCard = {3: '3', 4: '4', 5: '5', 6: '6', 7: '7',
|
||||||
|
8: '8', 9: '9', 10: 'T', 11: 'J', 12: 'Q',
|
||||||
|
13: 'K', 14: 'A', 17: '2', 20: 'X', 30: 'D'}
|
||||||
|
|
||||||
|
RealCard2EnvCard = {'3': 3, '4': 4, '5': 5, '6': 6, '7': 7,
|
||||||
|
'8': 8, '9': 9, 'T': 10, 'J': 11, 'Q': 12,
|
||||||
|
'K': 13, 'A': 14, '2': 17, 'X': 20, 'D': 30}
|
||||||
|
|
||||||
|
pretrained_dir = 'pretrained/douzero_pretrained'
|
||||||
|
players = []
|
||||||
|
for position in ['landlord', 'landlord_down', 'landlord_up']:
|
||||||
|
players.append(DeepAgent(position, pretrained_dir, use_onnx=True))
|
||||||
|
|
||||||
|
@app.route('/predict', methods=['POST'])
|
||||||
|
def predict():
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
# Player postion
|
||||||
|
player_position = request.form.get('player_position')
|
||||||
|
if player_position not in ['0', '1', '2']:
|
||||||
|
return jsonify({'status': 1, 'message': 'player_position must be 0, 1, or 2'})
|
||||||
|
player_position = int(player_position)
|
||||||
|
|
||||||
|
# Player hand cards
|
||||||
|
player_hand_cards = [RealCard2EnvCard[c] for c in request.form.get('player_hand_cards')]
|
||||||
|
if player_position == 0:
|
||||||
|
if len(player_hand_cards) < 1 or len(player_hand_cards) > 20:
|
||||||
|
return jsonify({'status': 2, 'message': 'the number of hand cards should be 1-20'})
|
||||||
|
else:
|
||||||
|
if len(player_hand_cards) < 1 or len(player_hand_cards) > 17:
|
||||||
|
return jsonify({'status': 3, 'message': 'the number of hand cards should be 1-17'})
|
||||||
|
|
||||||
|
# Number cards left
|
||||||
|
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'))]
|
||||||
|
if num_cards_left[player_position] != len(player_hand_cards):
|
||||||
|
return jsonify({'status': 4, 'message': 'the number of cards left do not align with hand cards'})
|
||||||
|
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:
|
||||||
|
return jsonify({'status': 5, 'message': 'the number of cards left not in range'})
|
||||||
|
|
||||||
|
# Three landlord cards
|
||||||
|
three_landlord_cards = [RealCard2EnvCard[c] for c in request.form.get('three_landlord_cards')]
|
||||||
|
if len(three_landlord_cards) < 0 or len(three_landlord_cards) > 3:
|
||||||
|
return jsonify({'status': 6, 'message': 'the number of landlord cards should be 0-3'})
|
||||||
|
|
||||||
|
# Card play sequence
|
||||||
|
if request.form.get('card_play_action_seq') == '':
|
||||||
|
card_play_action_seq = []
|
||||||
|
else:
|
||||||
|
card_play_action_seq = [[RealCard2EnvCard[c] for c in cards] for cards in request.form.get('card_play_action_seq').split(',')]
|
||||||
|
|
||||||
|
# Other hand cards
|
||||||
|
other_hand_cards = [RealCard2EnvCard[c] for c in request.form.get('other_hand_cards')]
|
||||||
|
if len(other_hand_cards) != sum(num_cards_left) - num_cards_left[player_position]:
|
||||||
|
return jsonify({'status': 7, 'message': 'the number of the other hand cards do not align with the number of cards left'})
|
||||||
|
|
||||||
|
# Last moves
|
||||||
|
last_moves = []
|
||||||
|
for field in ['last_move_landlord', 'last_move_landlord_down', 'last_move_landlord_up']:
|
||||||
|
last_moves.append([RealCard2EnvCard[c] for c in request.form.get(field)])
|
||||||
|
|
||||||
|
# Played cards
|
||||||
|
played_cards = []
|
||||||
|
for field in ['played_cards_landlord', 'played_cards_landlord_down', 'played_cards_landlord_up']:
|
||||||
|
played_cards.append([RealCard2EnvCard[c] for c in request.form.get(field)])
|
||||||
|
|
||||||
|
# Bomb Num
|
||||||
|
bomb_num = int(request.form.get('bomb_num'))
|
||||||
|
|
||||||
|
# InfoSet
|
||||||
|
info_set = InfoSet()
|
||||||
|
info_set.player_position = player_position
|
||||||
|
info_set.player_hand_cards = player_hand_cards
|
||||||
|
info_set.num_cards_left = num_cards_left
|
||||||
|
info_set.three_landlord_cards = three_landlord_cards
|
||||||
|
info_set.card_play_action_seq = card_play_action_seq
|
||||||
|
info_set.other_hand_cards = other_hand_cards
|
||||||
|
info_set.last_moves = last_moves
|
||||||
|
info_set.played_cards = played_cards
|
||||||
|
info_set.bomb_num = bomb_num
|
||||||
|
|
||||||
|
# Get rival move and legal_actions
|
||||||
|
rival_move = []
|
||||||
|
if len(card_play_action_seq) != 0:
|
||||||
|
if len(card_play_action_seq[-1]) == 0:
|
||||||
|
rival_move = card_play_action_seq[-2]
|
||||||
|
else:
|
||||||
|
rival_move = card_play_action_seq[-1]
|
||||||
|
info_set.rival_move = rival_move
|
||||||
|
info_set.legal_actions = _get_legal_card_play_actions(player_hand_cards, rival_move)
|
||||||
|
|
||||||
|
# Prediction
|
||||||
|
actions, actions_confidence = players[player_position].act(info_set)
|
||||||
|
actions = [''.join([EnvCard2RealCard[a] for a in action]) for action in actions]
|
||||||
|
result = {}
|
||||||
|
win_rates = {}
|
||||||
|
for i in range(len(actions)):
|
||||||
|
# Here, we calculate the win rate
|
||||||
|
win_rate = max(actions_confidence[i], -1)
|
||||||
|
win_rate = min(win_rate, 1)
|
||||||
|
win_rates[actions[i]] = str(round((win_rate + 1) / 2, 4))
|
||||||
|
result[actions[i]] = str(round(actions_confidence[i], 6))
|
||||||
|
|
||||||
|
############## DEBUG ################
|
||||||
|
if app.debug:
|
||||||
|
print('--------------- DEBUG START --------------')
|
||||||
|
command = 'curl --data "'
|
||||||
|
parameters = []
|
||||||
|
for key in request.form:
|
||||||
|
parameters.append(key+'='+request.form.get(key))
|
||||||
|
print(key+':', request.form.get(key))
|
||||||
|
command += '&'.join(parameters)
|
||||||
|
command += '" "http://127.0.0.1:5000/predict"'
|
||||||
|
print('Command:', command)
|
||||||
|
print('Rival Move:', rival_move)
|
||||||
|
print('legal_actions:', info_set.legal_actions)
|
||||||
|
print('Result:', result)
|
||||||
|
print('--------------- DEBUG END --------------')
|
||||||
|
############## DEBUG ################
|
||||||
|
return jsonify({'status': 0, 'message': 'success', 'result': result, 'win_rates': win_rates})
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({'status': -1, 'message': 'unkown error'})
|
||||||
|
|
||||||
|
@app.route('/legal', methods=['POST'])
|
||||||
|
def legal():
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
player_hand_cards = [RealCard2EnvCard[c] for c in request.form.get('player_hand_cards')]
|
||||||
|
rival_move = [RealCard2EnvCard[c] for c in request.form.get('rival_move')]
|
||||||
|
legal_actions = _get_legal_card_play_actions(player_hand_cards, rival_move)
|
||||||
|
legal_actions = ','.join([''.join([EnvCard2RealCard[a] for a in action]) for action in legal_actions])
|
||||||
|
return jsonify({'status': 0, 'message': 'success', 'legal_action': legal_actions})
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({'status': -1, 'message': 'unkown error'})
|
||||||
|
|
||||||
|
class InfoSet(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.player_position = None
|
||||||
|
self.player_hand_cards = None
|
||||||
|
self.num_cards_left = None
|
||||||
|
self.three_landlord_cards = None
|
||||||
|
self.card_play_action_seq = None
|
||||||
|
self.other_hand_cards = None
|
||||||
|
self.legal_actions = None
|
||||||
|
self.rival_move = None
|
||||||
|
self.last_moves = None
|
||||||
|
self.played_cards = None
|
||||||
|
self.bomb_num = None
|
||||||
|
|
||||||
|
def _get_legal_card_play_actions(player_hand_cards, rival_move):
|
||||||
|
mg = MovesGener(player_hand_cards)
|
||||||
|
|
||||||
|
rival_type = md.get_move_type(rival_move)
|
||||||
|
rival_move_type = rival_type['type']
|
||||||
|
rival_move_len = rival_type.get('len', 1)
|
||||||
|
moves = list()
|
||||||
|
|
||||||
|
if rival_move_type == md.TYPE_0_PASS:
|
||||||
|
moves = mg.gen_moves()
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_1_SINGLE:
|
||||||
|
all_moves = mg.gen_type_1_single()
|
||||||
|
moves = ms.filter_type_1_single(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_2_PAIR:
|
||||||
|
all_moves = mg.gen_type_2_pair()
|
||||||
|
moves = ms.filter_type_2_pair(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_3_TRIPLE:
|
||||||
|
all_moves = mg.gen_type_3_triple()
|
||||||
|
moves = ms.filter_type_3_triple(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_4_BOMB:
|
||||||
|
all_moves = mg.gen_type_4_bomb() + mg.gen_type_5_king_bomb()
|
||||||
|
moves = ms.filter_type_4_bomb(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_5_KING_BOMB:
|
||||||
|
moves = []
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_6_3_1:
|
||||||
|
all_moves = mg.gen_type_6_3_1()
|
||||||
|
moves = ms.filter_type_6_3_1(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_7_3_2:
|
||||||
|
all_moves = mg.gen_type_7_3_2()
|
||||||
|
moves = ms.filter_type_7_3_2(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_8_SERIAL_SINGLE:
|
||||||
|
all_moves = mg.gen_type_8_serial_single(repeat_num=rival_move_len)
|
||||||
|
moves = ms.filter_type_8_serial_single(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_9_SERIAL_PAIR:
|
||||||
|
all_moves = mg.gen_type_9_serial_pair(repeat_num=rival_move_len)
|
||||||
|
moves = ms.filter_type_9_serial_pair(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_10_SERIAL_TRIPLE:
|
||||||
|
all_moves = mg.gen_type_10_serial_triple(repeat_num=rival_move_len)
|
||||||
|
moves = ms.filter_type_10_serial_triple(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_11_SERIAL_3_1:
|
||||||
|
all_moves = mg.gen_type_11_serial_3_1(repeat_num=rival_move_len)
|
||||||
|
moves = ms.filter_type_11_serial_3_1(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_12_SERIAL_3_2:
|
||||||
|
all_moves = mg.gen_type_12_serial_3_2(repeat_num=rival_move_len)
|
||||||
|
moves = ms.filter_type_12_serial_3_2(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_13_4_2:
|
||||||
|
all_moves = mg.gen_type_13_4_2()
|
||||||
|
moves = ms.filter_type_13_4_2(all_moves, rival_move)
|
||||||
|
|
||||||
|
elif rival_move_type == md.TYPE_14_4_22:
|
||||||
|
all_moves = mg.gen_type_14_4_22()
|
||||||
|
moves = ms.filter_type_14_4_22(all_moves, rival_move)
|
||||||
|
|
||||||
|
if rival_move_type not in [md.TYPE_0_PASS,
|
||||||
|
md.TYPE_4_BOMB, md.TYPE_5_KING_BOMB]:
|
||||||
|
moves = moves + mg.gen_type_4_bomb() + mg.gen_type_5_king_bomb()
|
||||||
|
|
||||||
|
if len(rival_move) != 0: # rival_move is not 'pass'
|
||||||
|
moves = moves + [[]]
|
||||||
|
|
||||||
|
for m in moves:
|
||||||
|
m.sort()
|
||||||
|
|
||||||
|
moves.sort()
|
||||||
|
moves = list(move for move,_ in itertools.groupby(moves))
|
||||||
|
|
||||||
|
return moves
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description='DouZero backend')
|
||||||
|
parser.add_argument('--debug', action='store_true')
|
||||||
|
args = parser.parse_args()
|
||||||
|
app.run(debug=args.debug)
|
|
@ -0,0 +1,107 @@
|
||||||
|
from .utils import *
|
||||||
|
import collections
|
||||||
|
|
||||||
|
# check if move is a continuous sequence
|
||||||
|
def is_continuous_seq(move):
|
||||||
|
i = 0
|
||||||
|
while i < len(move) - 1:
|
||||||
|
if move[i+1] - move[i] != 1:
|
||||||
|
return False
|
||||||
|
i += 1
|
||||||
|
return True
|
||||||
|
|
||||||
|
# return the type of the move
|
||||||
|
def get_move_type(move):
|
||||||
|
move_size = len(move)
|
||||||
|
move_dict = collections.Counter(move)
|
||||||
|
|
||||||
|
if move_size == 0:
|
||||||
|
return {'type': TYPE_0_PASS}
|
||||||
|
|
||||||
|
if move_size == 1:
|
||||||
|
return {'type': TYPE_1_SINGLE, 'rank': move[0]}
|
||||||
|
|
||||||
|
if move_size == 2:
|
||||||
|
if move[0] == move[1]:
|
||||||
|
return {'type': TYPE_2_PAIR, 'rank': move[0]}
|
||||||
|
elif move == [20, 30]: # Kings
|
||||||
|
return {'type': TYPE_5_KING_BOMB}
|
||||||
|
else:
|
||||||
|
return {'type': TYPE_15_WRONG}
|
||||||
|
|
||||||
|
if move_size == 3:
|
||||||
|
if len(move_dict) == 1:
|
||||||
|
return {'type': TYPE_3_TRIPLE, 'rank': move[0]}
|
||||||
|
else:
|
||||||
|
return {'type': TYPE_15_WRONG}
|
||||||
|
|
||||||
|
if move_size == 4:
|
||||||
|
if len(move_dict) == 1:
|
||||||
|
return {'type': TYPE_4_BOMB, 'rank': move[0]}
|
||||||
|
elif len(move_dict) == 2:
|
||||||
|
if move[0] == move[1] == move[2] or move[1] == move[2] == move[3]:
|
||||||
|
return {'type': TYPE_6_3_1, 'rank': move[1]}
|
||||||
|
else:
|
||||||
|
return {'type': TYPE_15_WRONG}
|
||||||
|
else:
|
||||||
|
return {'type': TYPE_15_WRONG}
|
||||||
|
|
||||||
|
if is_continuous_seq(move):
|
||||||
|
return {'type': TYPE_8_SERIAL_SINGLE, 'rank': move[0], 'len': len(move)}
|
||||||
|
|
||||||
|
if move_size == 5:
|
||||||
|
if len(move_dict) == 2:
|
||||||
|
return {'type': TYPE_7_3_2, 'rank': move[2]}
|
||||||
|
else:
|
||||||
|
return {'type': TYPE_15_WRONG}
|
||||||
|
|
||||||
|
count_dict = collections.defaultdict(int)
|
||||||
|
for c, n in move_dict.items():
|
||||||
|
count_dict[n] += 1
|
||||||
|
|
||||||
|
if move_size == 6:
|
||||||
|
if (len(move_dict) == 2 or len(move_dict) == 3) and count_dict.get(4) == 1 and \
|
||||||
|
(count_dict.get(2) == 1 or count_dict.get(1) == 2):
|
||||||
|
return {'type': TYPE_13_4_2, 'rank': move[2]}
|
||||||
|
|
||||||
|
if move_size == 8 and (((len(move_dict) == 3 or len(move_dict) == 2) and
|
||||||
|
(count_dict.get(4) == 1 and count_dict.get(2) == 2)) or count_dict.get(4) == 2):
|
||||||
|
return {'type': TYPE_14_4_22, 'rank': max([c for c, n in move_dict.items() if n == 4])}
|
||||||
|
|
||||||
|
mdkeys = sorted(move_dict.keys())
|
||||||
|
if len(move_dict) == count_dict.get(2) and is_continuous_seq(mdkeys):
|
||||||
|
return {'type': TYPE_9_SERIAL_PAIR, 'rank': mdkeys[0], 'len': len(mdkeys)}
|
||||||
|
|
||||||
|
if len(move_dict) == count_dict.get(3) and is_continuous_seq(mdkeys):
|
||||||
|
return {'type': TYPE_10_SERIAL_TRIPLE, 'rank': mdkeys[0], 'len': len(mdkeys)}
|
||||||
|
|
||||||
|
# Check Type 11 (serial 3+1) and Type 12 (serial 3+2)
|
||||||
|
if count_dict.get(3, 0) >= MIN_TRIPLES:
|
||||||
|
serial_3 = list()
|
||||||
|
single = list()
|
||||||
|
pair = list()
|
||||||
|
|
||||||
|
for k, v in move_dict.items():
|
||||||
|
if v == 3:
|
||||||
|
serial_3.append(k)
|
||||||
|
elif v == 1:
|
||||||
|
single.append(k)
|
||||||
|
elif v == 2:
|
||||||
|
pair.append(k)
|
||||||
|
else: # no other possibilities
|
||||||
|
return {'type': TYPE_15_WRONG}
|
||||||
|
|
||||||
|
serial_3.sort()
|
||||||
|
if is_continuous_seq(serial_3):
|
||||||
|
if len(serial_3) == len(single)+len(pair)*2:
|
||||||
|
return {'type': TYPE_11_SERIAL_3_1, 'rank': serial_3[0], 'len': len(serial_3)}
|
||||||
|
if len(serial_3) == len(pair) and len(move_dict) == len(serial_3) * 2:
|
||||||
|
return {'type': TYPE_12_SERIAL_3_2, 'rank': serial_3[0], 'len': len(serial_3)}
|
||||||
|
|
||||||
|
if len(serial_3) == 4:
|
||||||
|
if is_continuous_seq(serial_3[1:]):
|
||||||
|
return {'type': TYPE_11_SERIAL_3_1, 'rank': serial_3[1], 'len': len(serial_3) - 1}
|
||||||
|
if is_continuous_seq(serial_3[:-1]):
|
||||||
|
return {'type': TYPE_11_SERIAL_3_1, 'rank': serial_3[0], 'len': len(serial_3) - 1}
|
||||||
|
|
||||||
|
return {'type': TYPE_15_WRONG}
|
|
@ -0,0 +1,217 @@
|
||||||
|
from .utils import MIN_SINGLE_CARDS, MIN_PAIRS, MIN_TRIPLES, select
|
||||||
|
import collections
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
class MovesGener(object):
|
||||||
|
|
||||||
|
def __init__(self, cards_list):
|
||||||
|
self.cards_list = cards_list
|
||||||
|
self.cards_dict = collections.defaultdict(int)
|
||||||
|
|
||||||
|
for i in self.cards_list:
|
||||||
|
self.cards_dict[i] += 1
|
||||||
|
|
||||||
|
self.single_card_moves = []
|
||||||
|
self.gen_type_1_single()
|
||||||
|
self.pair_moves = []
|
||||||
|
self.gen_type_2_pair()
|
||||||
|
self.triple_cards_moves = []
|
||||||
|
self.gen_type_3_triple()
|
||||||
|
self.bomb_moves = []
|
||||||
|
self.gen_type_4_bomb()
|
||||||
|
self.final_bomb_moves = []
|
||||||
|
self.gen_type_5_king_bomb()
|
||||||
|
|
||||||
|
def _gen_serial_moves(self, cards, min_serial, repeat=1, repeat_num=0):
|
||||||
|
if repeat_num < min_serial: # at least repeat_num is min_serial
|
||||||
|
repeat_num = 0
|
||||||
|
|
||||||
|
single_cards = sorted(list(set(cards)))
|
||||||
|
seq_records = list()
|
||||||
|
moves = list()
|
||||||
|
|
||||||
|
start = i = 0
|
||||||
|
longest = 1
|
||||||
|
while i < len(single_cards):
|
||||||
|
if i + 1 < len(single_cards) and single_cards[i + 1] - single_cards[i] == 1:
|
||||||
|
longest += 1
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
seq_records.append((start, longest))
|
||||||
|
i += 1
|
||||||
|
start = i
|
||||||
|
longest = 1
|
||||||
|
|
||||||
|
for seq in seq_records:
|
||||||
|
if seq[1] < min_serial:
|
||||||
|
continue
|
||||||
|
start, longest = seq[0], seq[1]
|
||||||
|
longest_list = single_cards[start: start + longest]
|
||||||
|
|
||||||
|
if repeat_num == 0: # No limitation on how many sequences
|
||||||
|
steps = min_serial
|
||||||
|
while steps <= longest:
|
||||||
|
index = 0
|
||||||
|
while steps + index <= longest:
|
||||||
|
target_moves = sorted(longest_list[index: index + steps] * repeat)
|
||||||
|
moves.append(target_moves)
|
||||||
|
index += 1
|
||||||
|
steps += 1
|
||||||
|
|
||||||
|
else: # repeat_num > 0
|
||||||
|
if longest < repeat_num:
|
||||||
|
continue
|
||||||
|
index = 0
|
||||||
|
while index + repeat_num <= longest:
|
||||||
|
target_moves = sorted(longest_list[index: index + repeat_num] * repeat)
|
||||||
|
moves.append(target_moves)
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
return moves
|
||||||
|
|
||||||
|
def gen_type_1_single(self):
|
||||||
|
self.single_card_moves = []
|
||||||
|
for i in set(self.cards_list):
|
||||||
|
self.single_card_moves.append([i])
|
||||||
|
return self.single_card_moves
|
||||||
|
|
||||||
|
def gen_type_2_pair(self):
|
||||||
|
self.pair_moves = []
|
||||||
|
for k, v in self.cards_dict.items():
|
||||||
|
if v >= 2:
|
||||||
|
self.pair_moves.append([k, k])
|
||||||
|
return self.pair_moves
|
||||||
|
|
||||||
|
def gen_type_3_triple(self):
|
||||||
|
self.triple_cards_moves = []
|
||||||
|
for k, v in self.cards_dict.items():
|
||||||
|
if v >= 3:
|
||||||
|
self.triple_cards_moves.append([k, k, k])
|
||||||
|
return self.triple_cards_moves
|
||||||
|
|
||||||
|
def gen_type_4_bomb(self):
|
||||||
|
self.bomb_moves = []
|
||||||
|
for k, v in self.cards_dict.items():
|
||||||
|
if v == 4:
|
||||||
|
self.bomb_moves.append([k, k, k, k])
|
||||||
|
return self.bomb_moves
|
||||||
|
|
||||||
|
def gen_type_5_king_bomb(self):
|
||||||
|
self.final_bomb_moves = []
|
||||||
|
if 20 in self.cards_list and 30 in self.cards_list:
|
||||||
|
self.final_bomb_moves.append([20, 30])
|
||||||
|
return self.final_bomb_moves
|
||||||
|
|
||||||
|
def gen_type_6_3_1(self):
|
||||||
|
result = []
|
||||||
|
for t in self.single_card_moves:
|
||||||
|
for i in self.triple_cards_moves:
|
||||||
|
if t[0] != i[0]:
|
||||||
|
result.append(t+i)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def gen_type_7_3_2(self):
|
||||||
|
result = list()
|
||||||
|
for t in self.pair_moves:
|
||||||
|
for i in self.triple_cards_moves:
|
||||||
|
if t[0] != i[0]:
|
||||||
|
result.append(t+i)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def gen_type_8_serial_single(self, repeat_num=0):
|
||||||
|
return self._gen_serial_moves(self.cards_list, MIN_SINGLE_CARDS, repeat=1, repeat_num=repeat_num)
|
||||||
|
|
||||||
|
def gen_type_9_serial_pair(self, repeat_num=0):
|
||||||
|
single_pairs = list()
|
||||||
|
for k, v in self.cards_dict.items():
|
||||||
|
if v >= 2:
|
||||||
|
single_pairs.append(k)
|
||||||
|
|
||||||
|
return self._gen_serial_moves(single_pairs, MIN_PAIRS, repeat=2, repeat_num=repeat_num)
|
||||||
|
|
||||||
|
def gen_type_10_serial_triple(self, repeat_num=0):
|
||||||
|
single_triples = list()
|
||||||
|
for k, v in self.cards_dict.items():
|
||||||
|
if v >= 3:
|
||||||
|
single_triples.append(k)
|
||||||
|
|
||||||
|
return self._gen_serial_moves(single_triples, MIN_TRIPLES, repeat=3, repeat_num=repeat_num)
|
||||||
|
|
||||||
|
def gen_type_11_serial_3_1(self, repeat_num=0):
|
||||||
|
serial_3_moves = self.gen_type_10_serial_triple(repeat_num=repeat_num)
|
||||||
|
serial_3_1_moves = list()
|
||||||
|
|
||||||
|
for s3 in serial_3_moves: # s3 is like [3,3,3,4,4,4]
|
||||||
|
s3_set = set(s3)
|
||||||
|
new_cards = [i for i in self.cards_list if i not in s3_set]
|
||||||
|
|
||||||
|
# Get any s3_len items from cards
|
||||||
|
subcards = select(new_cards, len(s3_set))
|
||||||
|
|
||||||
|
for i in subcards:
|
||||||
|
serial_3_1_moves.append(s3 + i)
|
||||||
|
|
||||||
|
return list(k for k, _ in itertools.groupby(serial_3_1_moves))
|
||||||
|
|
||||||
|
def gen_type_12_serial_3_2(self, repeat_num=0):
|
||||||
|
serial_3_moves = self.gen_type_10_serial_triple(repeat_num=repeat_num)
|
||||||
|
serial_3_2_moves = list()
|
||||||
|
pair_set = sorted([k for k, v in self.cards_dict.items() if v >= 2])
|
||||||
|
|
||||||
|
for s3 in serial_3_moves:
|
||||||
|
s3_set = set(s3)
|
||||||
|
pair_candidates = [i for i in pair_set if i not in s3_set]
|
||||||
|
|
||||||
|
# Get any s3_len items from cards
|
||||||
|
subcards = select(pair_candidates, len(s3_set))
|
||||||
|
for i in subcards:
|
||||||
|
serial_3_2_moves.append(sorted(s3 + i * 2))
|
||||||
|
|
||||||
|
return serial_3_2_moves
|
||||||
|
|
||||||
|
def gen_type_13_4_2(self):
|
||||||
|
four_cards = list()
|
||||||
|
for k, v in self.cards_dict.items():
|
||||||
|
if v == 4:
|
||||||
|
four_cards.append(k)
|
||||||
|
|
||||||
|
result = list()
|
||||||
|
for fc in four_cards:
|
||||||
|
cards_list = [k for k in self.cards_list if k != fc]
|
||||||
|
subcards = select(cards_list, 2)
|
||||||
|
for i in subcards:
|
||||||
|
result.append([fc]*4 + i)
|
||||||
|
return list(k for k, _ in itertools.groupby(result))
|
||||||
|
|
||||||
|
def gen_type_14_4_22(self):
|
||||||
|
four_cards = list()
|
||||||
|
for k, v in self.cards_dict.items():
|
||||||
|
if v == 4:
|
||||||
|
four_cards.append(k)
|
||||||
|
|
||||||
|
result = list()
|
||||||
|
for fc in four_cards:
|
||||||
|
cards_list = [k for k, v in self.cards_dict.items() if k != fc and v>=2]
|
||||||
|
subcards = select(cards_list, 2)
|
||||||
|
for i in subcards:
|
||||||
|
result.append([fc] * 4 + [i[0], i[0], i[1], i[1]])
|
||||||
|
return result
|
||||||
|
|
||||||
|
# generate all possible moves from given cards
|
||||||
|
def gen_moves(self):
|
||||||
|
moves = []
|
||||||
|
moves.extend(self.gen_type_1_single())
|
||||||
|
moves.extend(self.gen_type_2_pair())
|
||||||
|
moves.extend(self.gen_type_3_triple())
|
||||||
|
moves.extend(self.gen_type_4_bomb())
|
||||||
|
moves.extend(self.gen_type_5_king_bomb())
|
||||||
|
moves.extend(self.gen_type_6_3_1())
|
||||||
|
moves.extend(self.gen_type_7_3_2())
|
||||||
|
moves.extend(self.gen_type_8_serial_single())
|
||||||
|
moves.extend(self.gen_type_9_serial_pair())
|
||||||
|
moves.extend(self.gen_type_10_serial_triple())
|
||||||
|
moves.extend(self.gen_type_11_serial_3_1())
|
||||||
|
moves.extend(self.gen_type_12_serial_3_2())
|
||||||
|
moves.extend(self.gen_type_13_4_2())
|
||||||
|
moves.extend(self.gen_type_14_4_22())
|
||||||
|
return moves
|
|
@ -0,0 +1,117 @@
|
||||||
|
# return all moves that can beat rivals, moves and rival_move should be same type
|
||||||
|
import collections
|
||||||
|
|
||||||
|
|
||||||
|
def common_handle(moves, rival_move):
|
||||||
|
new_moves = list()
|
||||||
|
for move in moves:
|
||||||
|
if move[0] > rival_move[0]:
|
||||||
|
new_moves.append(move)
|
||||||
|
return new_moves
|
||||||
|
|
||||||
|
|
||||||
|
def filter_type_1_single(moves, rival_move):
|
||||||
|
return common_handle(moves, rival_move)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_type_2_pair(moves, rival_move):
|
||||||
|
return common_handle(moves, rival_move)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_type_3_triple(moves, rival_move):
|
||||||
|
return common_handle(moves, rival_move)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_type_4_bomb(moves, rival_move):
|
||||||
|
return common_handle(moves, rival_move)
|
||||||
|
|
||||||
|
|
||||||
|
# No need to filter for type_5_king_bomb
|
||||||
|
|
||||||
|
def filter_type_6_3_1(moves, rival_move):
|
||||||
|
rival_move.sort()
|
||||||
|
rival_rank = rival_move[1]
|
||||||
|
new_moves = list()
|
||||||
|
for move in moves:
|
||||||
|
move.sort()
|
||||||
|
my_rank = move[1]
|
||||||
|
if my_rank > rival_rank:
|
||||||
|
new_moves.append(move)
|
||||||
|
return new_moves
|
||||||
|
|
||||||
|
|
||||||
|
def filter_type_7_3_2(moves, rival_move):
|
||||||
|
rival_move.sort()
|
||||||
|
rival_rank = rival_move[2]
|
||||||
|
new_moves = list()
|
||||||
|
for move in moves:
|
||||||
|
move.sort()
|
||||||
|
my_rank = move[2]
|
||||||
|
if my_rank > rival_rank:
|
||||||
|
new_moves.append(move)
|
||||||
|
return new_moves
|
||||||
|
|
||||||
|
|
||||||
|
def filter_type_8_serial_single(moves, rival_move):
|
||||||
|
return common_handle(moves, rival_move)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_type_9_serial_pair(moves, rival_move):
|
||||||
|
return common_handle(moves, rival_move)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_type_10_serial_triple(moves, rival_move):
|
||||||
|
return common_handle(moves, rival_move)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_type_11_serial_3_1(moves, rival_move):
|
||||||
|
rival = collections.Counter(rival_move)
|
||||||
|
rival_rank = max([k for k, v in rival.items() if v == 3])
|
||||||
|
new_moves = list()
|
||||||
|
for move in moves:
|
||||||
|
mymove = collections.Counter(move)
|
||||||
|
my_rank = max([k for k, v in mymove.items() if v == 3])
|
||||||
|
if my_rank > rival_rank:
|
||||||
|
new_moves.append(move)
|
||||||
|
return new_moves
|
||||||
|
|
||||||
|
|
||||||
|
def filter_type_12_serial_3_2(moves, rival_move):
|
||||||
|
rival = collections.Counter(rival_move)
|
||||||
|
rival_rank = max([k for k, v in rival.items() if v == 3])
|
||||||
|
new_moves = list()
|
||||||
|
for move in moves:
|
||||||
|
mymove = collections.Counter(move)
|
||||||
|
my_rank = max([k for k, v in mymove.items() if v == 3])
|
||||||
|
if my_rank > rival_rank:
|
||||||
|
new_moves.append(move)
|
||||||
|
return new_moves
|
||||||
|
|
||||||
|
|
||||||
|
def filter_type_13_4_2(moves, rival_move):
|
||||||
|
rival_move.sort()
|
||||||
|
rival_rank = rival_move[2]
|
||||||
|
new_moves = list()
|
||||||
|
for move in moves:
|
||||||
|
move.sort()
|
||||||
|
my_rank = move[2]
|
||||||
|
if my_rank > rival_rank:
|
||||||
|
new_moves.append(move)
|
||||||
|
return new_moves
|
||||||
|
|
||||||
|
|
||||||
|
def filter_type_14_4_22(moves, rival_move):
|
||||||
|
rival = collections.Counter(rival_move)
|
||||||
|
rival_rank = my_rank = 0
|
||||||
|
for k, v in rival.items():
|
||||||
|
if v == 4:
|
||||||
|
rival_rank = k
|
||||||
|
new_moves = list()
|
||||||
|
for move in moves:
|
||||||
|
mymove = collections.Counter(move)
|
||||||
|
for k, v in mymove.items():
|
||||||
|
if v == 4:
|
||||||
|
my_rank = k
|
||||||
|
if my_rank > rival_rank:
|
||||||
|
new_moves.append(move)
|
||||||
|
return new_moves
|
|
@ -0,0 +1,33 @@
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
# global parameters
|
||||||
|
MIN_SINGLE_CARDS = 5
|
||||||
|
MIN_PAIRS = 3
|
||||||
|
MIN_TRIPLES = 2
|
||||||
|
|
||||||
|
# action types
|
||||||
|
TYPE_0_PASS = 0
|
||||||
|
TYPE_1_SINGLE = 1
|
||||||
|
TYPE_2_PAIR = 2
|
||||||
|
TYPE_3_TRIPLE = 3
|
||||||
|
TYPE_4_BOMB = 4
|
||||||
|
TYPE_5_KING_BOMB = 5
|
||||||
|
TYPE_6_3_1 = 6
|
||||||
|
TYPE_7_3_2 = 7
|
||||||
|
TYPE_8_SERIAL_SINGLE = 8
|
||||||
|
TYPE_9_SERIAL_PAIR = 9
|
||||||
|
TYPE_10_SERIAL_TRIPLE = 10
|
||||||
|
TYPE_11_SERIAL_3_1 = 11
|
||||||
|
TYPE_12_SERIAL_3_2 = 12
|
||||||
|
TYPE_13_4_2 = 13
|
||||||
|
TYPE_14_4_22 = 14
|
||||||
|
TYPE_15_WRONG = 15
|
||||||
|
|
||||||
|
# betting round action
|
||||||
|
PASS = 0
|
||||||
|
CALL = 1
|
||||||
|
RAISE = 2
|
||||||
|
|
||||||
|
# return all possible results of selecting num cards from cards list
|
||||||
|
def select(cards, num):
|
||||||
|
return [list(i) for i in itertools.combinations(cards, num)]
|
|
@ -1,4 +1,4 @@
|
||||||
rlcard[training]
|
rlcard[torch]
|
||||||
Django
|
Django
|
||||||
tqdm
|
tqdm
|
||||||
django-cors-headers
|
django-cors-headers
|
||||||
|
|
|
@ -41,9 +41,10 @@ def _get_model_ids_all():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.model_id = name
|
self.model_id = name
|
||||||
self._entry_point = M
|
self._entry_point = M
|
||||||
|
self.target_path = target_path
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
model = self._entry_point(target_path)
|
model = self._entry_point(self.target_path)
|
||||||
return model
|
return model
|
||||||
rlcard.models.registration.model_registry.model_specs[name] = ModelSpec()
|
rlcard.models.registration.model_registry.model_specs[name] = ModelSpec()
|
||||||
MODEL_IDS_ALL[game].append(name)
|
MODEL_IDS_ALL[game].append(name)
|
||||||
|
|
|
@ -1,50 +1,49 @@
|
||||||
import React from 'react';
|
import Button from '@material-ui/core/Button';
|
||||||
import axios from 'axios';
|
import Card from '@material-ui/core/Card';
|
||||||
import { useHistory } from 'react-router-dom';
|
import CardContent from '@material-ui/core/CardContent';
|
||||||
|
import Collapse from '@material-ui/core/Collapse';
|
||||||
|
import Dialog from '@material-ui/core/Dialog';
|
||||||
|
import DialogActions from '@material-ui/core/DialogActions';
|
||||||
|
import DialogContent from '@material-ui/core/DialogContent';
|
||||||
|
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||||
import Drawer from '@material-ui/core/Drawer';
|
import Drawer from '@material-ui/core/Drawer';
|
||||||
|
import FormControl from '@material-ui/core/FormControl';
|
||||||
|
import InputLabel from '@material-ui/core/InputLabel';
|
||||||
|
import Link from '@material-ui/core/Link';
|
||||||
import List from '@material-ui/core/List';
|
import List from '@material-ui/core/List';
|
||||||
import ListItem from '@material-ui/core/ListItem';
|
import ListItem from '@material-ui/core/ListItem';
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
import ListItemText from '@material-ui/core/ListItemText';
|
||||||
import Collapse from '@material-ui/core/Collapse';
|
import ListSubheader from '@material-ui/core/ListSubheader';
|
||||||
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
import Select from '@material-ui/core/Select';
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import TextField from '@material-ui/core/TextField';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
import ExpandLess from '@material-ui/icons/ExpandLess';
|
import ExpandLess from '@material-ui/icons/ExpandLess';
|
||||||
import ExpandMore from '@material-ui/icons/ExpandMore';
|
import ExpandMore from '@material-ui/icons/ExpandMore';
|
||||||
import {makeStyles} from "@material-ui/core/styles";
|
|
||||||
import qs from 'query-string';
|
|
||||||
import ListSubheader from "@material-ui/core/ListSubheader";
|
|
||||||
import Button from "@material-ui/core/Button";
|
|
||||||
import Dialog from "@material-ui/core/Dialog";
|
|
||||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
|
||||||
import DialogContent from "@material-ui/core/DialogContent";
|
|
||||||
import TextField from "@material-ui/core/TextField";
|
|
||||||
import Select from '@material-ui/core/Select';
|
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
|
||||||
import InputLabel from '@material-ui/core/InputLabel';
|
|
||||||
import FormControl from '@material-ui/core/FormControl';
|
|
||||||
import DialogActions from "@material-ui/core/DialogActions";
|
|
||||||
import Card from '@material-ui/core/Card';
|
|
||||||
import CardContent from '@material-ui/core/CardContent';
|
|
||||||
import Typography from '@material-ui/core/Typography';
|
|
||||||
import HelpIcon from '@material-ui/icons/Help';
|
import HelpIcon from '@material-ui/icons/Help';
|
||||||
import Link from '@material-ui/core/Link';
|
import axios from 'axios';
|
||||||
|
import { Loading, Message, Upload } from 'element-react';
|
||||||
import {Message, Upload, Loading} from 'element-react';
|
import qs from 'query-string';
|
||||||
import {apiUrl} from "../utils/config";
|
import React from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { apiUrl } from '../utils/config';
|
||||||
|
|
||||||
const drawerWidth = 250;
|
const drawerWidth = 250;
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
uploadNoteRoot: {
|
uploadNoteRoot: {
|
||||||
maxWidth: "358px",
|
maxWidth: '358px',
|
||||||
color: "#E6A23C",
|
color: '#E6A23C',
|
||||||
backgroundColor: "#fdf6ec",
|
backgroundColor: '#fdf6ec',
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
color: "#e6a23c",
|
color: '#e6a23c',
|
||||||
lineHeight: "24px",
|
lineHeight: '24px',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "flex-start"
|
justifyContent: 'flex-start',
|
||||||
},
|
},
|
||||||
formControl: {
|
formControl: {
|
||||||
marginTop: theme.spacing(1),
|
marginTop: theme.spacing(1),
|
||||||
|
@ -54,65 +53,65 @@ const useStyles = makeStyles((theme) => ({
|
||||||
drawer: {
|
drawer: {
|
||||||
zIndex: 1001,
|
zIndex: 1001,
|
||||||
width: drawerWidth,
|
width: drawerWidth,
|
||||||
flexShrink: 0
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
drawerPaper: {
|
drawerPaper: {
|
||||||
height: 'calc(100% - 75px)',
|
height: 'calc(100% - 75px)',
|
||||||
zIndex: 1001,
|
zIndex: 1001,
|
||||||
width: drawerWidth,
|
width: drawerWidth,
|
||||||
top: 75
|
top: 75,
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
width: 200,
|
width: 200,
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
left: 25,
|
left: 25,
|
||||||
bottom: 25
|
bottom: 25,
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
height: 'calc(100% - 86px - 16px)',
|
height: 'calc(100% - 86px - 16px)',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
borderBottom: '1px solid #ccc',
|
borderBottom: '1px solid #ccc',
|
||||||
paddingTop: '0'
|
paddingTop: '0',
|
||||||
},
|
},
|
||||||
nested: {
|
nested: {
|
||||||
paddingLeft: theme.spacing(4)
|
paddingLeft: theme.spacing(4),
|
||||||
},
|
},
|
||||||
nestedSubheader: {
|
nestedSubheader: {
|
||||||
paddingLeft: theme.spacing(4),
|
paddingLeft: theme.spacing(4),
|
||||||
background: 'white'
|
background: 'white',
|
||||||
},
|
},
|
||||||
menuLayer1: {
|
menuLayer1: {
|
||||||
'& span': {
|
'& span': {
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: 14
|
fontSize: 14,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
menuLayer2: {
|
menuLayer2: {
|
||||||
'& span': {
|
'& span': {
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
fontSize: 14
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
'&$active': {
|
'&$active': {
|
||||||
'& span': {
|
'& span': {
|
||||||
color: '#3f51b5',
|
color: '#3f51b5',
|
||||||
fontWeight: 600
|
fontWeight: 600,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
active: {}
|
active: {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function MenuBar (props) {
|
function MenuBar(props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [uploadDialogLoading, setUploadDialogLoading] = React.useState(false);
|
const [uploadDialogLoading, setUploadDialogLoading] = React.useState(false);
|
||||||
|
|
||||||
const [open, setOpen] = React.useState({game: true, agent: true});
|
const [open, setOpen] = React.useState({ game: true, agent: true });
|
||||||
|
|
||||||
const handleClickGame = () => {
|
const handleClickGame = () => {
|
||||||
setOpen({game: !open.game, agent: open.agent});
|
setOpen({ game: !open.game, agent: open.agent });
|
||||||
};
|
};
|
||||||
const handleClickAgent = () => {
|
const handleClickAgent = () => {
|
||||||
setOpen({game: open.game, agent: !open.agent});
|
setOpen({ game: open.game, agent: !open.agent });
|
||||||
};
|
};
|
||||||
|
|
||||||
const [uploadDialogOpen, setUploadDialogOpen] = React.useState(false);
|
const [uploadDialogOpen, setUploadDialogOpen] = React.useState(false);
|
||||||
|
@ -121,16 +120,16 @@ function MenuBar (props) {
|
||||||
setUploadDialogOpen(true);
|
setUploadDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadFormInitValue = {name: '', game: 'leduc-holdem'};
|
const uploadFormInitValue = { name: '', game: 'leduc-holdem' };
|
||||||
const [uploadForm, setUploadForm] = React.useState({...uploadFormInitValue});
|
const [uploadForm, setUploadForm] = React.useState({ ...uploadFormInitValue });
|
||||||
const handleUploadFormChange = (e, property) => {
|
const handleUploadFormChange = (e, property) => {
|
||||||
let tempUploadForm = {...uploadForm};
|
let tempUploadForm = { ...uploadForm };
|
||||||
tempUploadForm[property] = e.target.value;
|
tempUploadForm[property] = e.target.value;
|
||||||
setUploadForm(tempUploadForm);
|
setUploadForm(tempUploadForm);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleUploadDialogClose = () => {
|
const handleUploadDialogClose = () => {
|
||||||
setUploadForm({...uploadFormInitValue});
|
setUploadForm({ ...uploadFormInitValue });
|
||||||
setUploadDialogOpen(false);
|
setUploadDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -139,42 +138,44 @@ function MenuBar (props) {
|
||||||
// check if data to upload is legal
|
// check if data to upload is legal
|
||||||
if (uploadRef.current.state.fileList.length !== 1) {
|
if (uploadRef.current.state.fileList.length !== 1) {
|
||||||
Message({
|
Message({
|
||||||
message: "Please select one zip file to upload",
|
message: 'Please select one zip file to upload',
|
||||||
type: "warning",
|
type: 'warning',
|
||||||
showClose: true,
|
showClose: true,
|
||||||
});
|
});
|
||||||
return ;
|
return;
|
||||||
}
|
}
|
||||||
console.log('upload', uploadRef.current);
|
console.log('upload', uploadRef.current);
|
||||||
if (!["application/zip", "application/x-zip-compressed"].includes(uploadRef.current.state.fileList[0].raw.type)) {
|
if (
|
||||||
|
!['application/zip', 'application/x-zip-compressed'].includes(uploadRef.current.state.fileList[0].raw.type)
|
||||||
|
) {
|
||||||
Message({
|
Message({
|
||||||
message: "Only zip file can be uploaded",
|
message: 'Only zip file can be uploaded',
|
||||||
type: "warning",
|
type: 'warning',
|
||||||
showClose: true,
|
showClose: true,
|
||||||
});
|
});
|
||||||
return ;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uploadForm.name === '') {
|
if (uploadForm.name === '') {
|
||||||
Message({
|
Message({
|
||||||
message: "Model name cannot be blank",
|
message: 'Model name cannot be blank',
|
||||||
type: "warning",
|
type: 'warning',
|
||||||
showClose: true,
|
showClose: true,
|
||||||
});
|
});
|
||||||
return ;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let flatGameList = [];
|
let flatGameList = [];
|
||||||
Object.keys(props.modelList).forEach(game => {
|
Object.keys(props.modelList).forEach((game) => {
|
||||||
flatGameList = flatGameList.concat([...props.modelList[game]]);
|
flatGameList = flatGameList.concat([...props.modelList[game]]);
|
||||||
});
|
});
|
||||||
if (flatGameList.includes(uploadForm.name)) {
|
if (flatGameList.includes(uploadForm.name)) {
|
||||||
Message({
|
Message({
|
||||||
message: "Model name exists",
|
message: 'Model name exists',
|
||||||
type: "warning",
|
type: 'warning',
|
||||||
showClose: true,
|
showClose: true,
|
||||||
});
|
});
|
||||||
return ;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bodyFormData = new FormData();
|
const bodyFormData = new FormData();
|
||||||
|
@ -182,56 +183,89 @@ function MenuBar (props) {
|
||||||
bodyFormData.append('game', uploadForm.game);
|
bodyFormData.append('game', uploadForm.game);
|
||||||
bodyFormData.append('model', uploadRef.current.state.fileList[0].raw);
|
bodyFormData.append('model', uploadRef.current.state.fileList[0].raw);
|
||||||
setUploadDialogLoading(true);
|
setUploadDialogLoading(true);
|
||||||
axios.post(`${apiUrl}/tournament/upload_agent`, bodyFormData, {headers: {'Content-Type': 'multipart/form-data'}})
|
axios
|
||||||
.then(res => {
|
.post(`${apiUrl}/tournament/upload_agent`, bodyFormData, {
|
||||||
setTimeout(() => {setUploadDialogLoading(false)}, 250);
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setUploadDialogLoading(false);
|
||||||
|
}, 250);
|
||||||
Message({
|
Message({
|
||||||
message: "Successfully uploaded model",
|
message: 'Successfully uploaded model',
|
||||||
type: "success",
|
type: 'success',
|
||||||
showClose: true,
|
showClose: true,
|
||||||
});
|
});
|
||||||
props.setReloadMenu(props.reloadMenu+1);
|
props.setReloadMenu(props.reloadMenu + 1);
|
||||||
setUploadDialogOpen(false);
|
setUploadDialogOpen(false);
|
||||||
setUploadForm({...uploadFormInitValue});
|
setUploadForm({ ...uploadFormInitValue });
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
setTimeout(() => {setUploadDialogLoading(false)}, 250);
|
setTimeout(() => {
|
||||||
|
setUploadDialogLoading(false);
|
||||||
|
}, 250);
|
||||||
Message({
|
Message({
|
||||||
message: "Failed to upload model",
|
message: 'Failed to upload model',
|
||||||
type: "error",
|
type: 'error',
|
||||||
showClose: true,
|
showClose: true,
|
||||||
});
|
});
|
||||||
console.log(err);
|
console.log(err);
|
||||||
})
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const handleGameJump = (gameName) => {
|
const handleGameJump = (gameName) => {
|
||||||
props.resetPagination();
|
props.resetPagination();
|
||||||
history.push(`/leaderboard?type=game&name=${gameName}`);
|
history.push(`/leaderboard?type=game&name=${gameName}`);
|
||||||
}
|
};
|
||||||
const handleAgentJump = (agentName) => {
|
const handleAgentJump = (agentName) => {
|
||||||
props.resetPagination();
|
props.resetPagination();
|
||||||
history.push(`/leaderboard?type=agent&name=${agentName}`);
|
history.push(`/leaderboard?type=agent&name=${agentName}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
const { type, name } = qs.parse(window.location.search);
|
const { type, name } = qs.parse(window.location.search);
|
||||||
const gameMenu = props.gameList.map(game => {
|
const gameMenu = props.gameList.map((game) => {
|
||||||
return <List component="div" disablePadding key={"game-menu-"+game.game}>
|
return (
|
||||||
<ListItem button className={classes.nested} onClick={() => {handleGameJump(game.game)}}>
|
<List component="div" disablePadding key={'game-menu-' + game.game}>
|
||||||
<ListItemText primary={game.dispName} className={`${classes.menuLayer2} ${(type === 'game' && name === game.game) ? classes.active : classes.inactive}`} />
|
<ListItem
|
||||||
</ListItem>
|
button
|
||||||
</List>
|
className={classes.nested}
|
||||||
|
onClick={() => {
|
||||||
|
handleGameJump(game.game);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={game.dispName}
|
||||||
|
className={`${classes.menuLayer2} ${
|
||||||
|
type === 'game' && name === game.game ? classes.active : classes.inactive
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateAgentMenu = (modelList) => {
|
const generateAgentMenu = (modelList) => {
|
||||||
return modelList.map((model) => {
|
return modelList.map((model) => {
|
||||||
return <List component="div" disablePadding key={"game-menu-"+model}>
|
return (
|
||||||
<ListItem button className={classes.nested} onClick={() => {handleAgentJump(model)}}>
|
<List component="div" disablePadding key={'game-menu-' + model}>
|
||||||
<ListItemText primary={model} className={`${classes.menuLayer2} ${(type === 'agent' && name === model) ? classes.active : classes.inactive}`} />
|
<ListItem
|
||||||
</ListItem>
|
button
|
||||||
</List>
|
className={classes.nested}
|
||||||
})
|
onClick={() => {
|
||||||
|
handleAgentJump(model);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={model}
|
||||||
|
className={`${classes.menuLayer2} ${
|
||||||
|
type === 'agent' && name === model ? classes.active : classes.inactive
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -239,35 +273,30 @@ function MenuBar (props) {
|
||||||
className={classes.drawer}
|
className={classes.drawer}
|
||||||
variant="permanent"
|
variant="permanent"
|
||||||
classes={{
|
classes={{
|
||||||
paper: classes.drawerPaper
|
paper: classes.drawerPaper,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<List
|
<List component="nav" aria-labelledby="nested-list-subheader" className={classes.list}>
|
||||||
component="nav"
|
|
||||||
aria-labelledby="nested-list-subheader"
|
|
||||||
className={classes.list}
|
|
||||||
>
|
|
||||||
<ListItem button onClick={handleClickAgent}>
|
<ListItem button onClick={handleClickAgent}>
|
||||||
<ListItemText primary="Game LeaderBoards" className={classes.menuLayer1}/>
|
<ListItemText primary="Game LeaderBoards" className={classes.menuLayer1} />
|
||||||
{open.agent ? <ExpandLess /> : <ExpandMore />}
|
{open.agent ? <ExpandLess /> : <ExpandMore />}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Collapse in={open.agent} timeout="auto" unmountOnExit>
|
<Collapse in={open.agent} timeout="auto" unmountOnExit>
|
||||||
{gameMenu}
|
{gameMenu}
|
||||||
</Collapse>
|
</Collapse>
|
||||||
<ListItem button onClick={handleClickGame}>
|
<ListItem button onClick={handleClickGame}>
|
||||||
<ListItemText primary="Agents" className={classes.menuLayer1}/>
|
<ListItemText primary="Agents" className={classes.menuLayer1} />
|
||||||
{open.game ? <ExpandLess /> : <ExpandMore />}
|
{open.game ? <ExpandLess /> : <ExpandMore />}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Collapse in={open.game} timeout="auto" unmountOnExit>
|
<Collapse in={open.game} timeout="auto" unmountOnExit>
|
||||||
{Object.keys(props.modelList).map(gameName => {
|
{Object.keys(props.modelList).map((gameName) => {
|
||||||
return (
|
return (
|
||||||
<div key={`agentMenu-sublist-${gameName}`}>
|
<div key={`agentMenu-sublist-${gameName}`}>
|
||||||
<ListSubheader className={classes.nestedSubheader}>{gameName}</ListSubheader>
|
<ListSubheader className={classes.nestedSubheader}>{gameName}</ListSubheader>
|
||||||
{generateAgentMenu(props.modelList[gameName])}
|
{generateAgentMenu(props.modelList[gameName])}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</List>
|
</List>
|
||||||
<Button variant="contained" color="primary" onClick={openUploadDialog} className={classes.button}>
|
<Button variant="contained" color="primary" onClick={openUploadDialog} className={classes.button}>
|
||||||
|
@ -280,85 +309,118 @@ function MenuBar (props) {
|
||||||
disableBackdropClick={true}
|
disableBackdropClick={true}
|
||||||
>
|
>
|
||||||
<Loading loading={uploadDialogLoading}>
|
<Loading loading={uploadDialogLoading}>
|
||||||
<DialogTitle id="form-dialog-title">Upload Model</DialogTitle>
|
<DialogTitle id="form-dialog-title">Upload Model</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Card variant="outlined" className={classes.uploadNoteRoot}>
|
<Card variant="outlined" className={classes.uploadNoteRoot}>
|
||||||
<CardContent style={{paddingBottom: "16px"}}>
|
<CardContent style={{ paddingBottom: '16px' }}>
|
||||||
<Typography className={classes.title} color="textSecondary" gutterBottom>
|
<Typography className={classes.title} color="textSecondary" gutterBottom>
|
||||||
<HelpIcon style={{marginRight: "5px"}} />Note
|
<HelpIcon style={{ marginRight: '5px' }} />
|
||||||
</Typography>
|
Note
|
||||||
<Typography variant="body2" component="p">
|
</Typography>
|
||||||
Download the example <Link href={apiUrl + "/tournament/download_examples?name=example_luduc_nfsp_model"} download>NFSP model</Link> and <Link href={apiUrl + "/tournament/download_examples?name=example_luduc_rule_model"} download>Rule model</Link> of Leduc Hold'em to test and learn about model upload functionality.
|
<Typography variant="body2" component="p">
|
||||||
</Typography>
|
Download the example{' '}
|
||||||
</CardContent>
|
<Link
|
||||||
</Card>
|
href={apiUrl + '/tournament/download_examples?name=leduc_holdem_dqn'}
|
||||||
<Upload
|
download
|
||||||
className={classes.formControl}
|
>
|
||||||
drag
|
DQN model
|
||||||
ref={uploadRef}
|
</Link>{' '}
|
||||||
action="//placeholder/"
|
for Leduc Holdem or{' '}
|
||||||
multiple
|
<Link
|
||||||
limit={1}
|
href={apiUrl + '/tournament/download_examples?name=example_luduc_rule_model'}
|
||||||
autoUpload={false}
|
download
|
||||||
tip={<div className="el-upload__tip">Only zip file can be uploaded</div>}
|
>
|
||||||
>
|
DMC model
|
||||||
<i className="el-icon-upload"/>
|
</Link>{' '}
|
||||||
<div className="el-upload__text">Drag the file here, or <em>Click to upload</em></div>
|
for Doudizhu to test and learn about model upload functionality.
|
||||||
</Upload>
|
</Typography>
|
||||||
<TextField
|
</CardContent>
|
||||||
className={classes.formControl}
|
</Card>
|
||||||
required
|
<Upload
|
||||||
margin="dense"
|
className={classes.formControl}
|
||||||
id="name"
|
drag
|
||||||
label="Model Name"
|
ref={uploadRef}
|
||||||
value={uploadForm.name}
|
action="//placeholder/"
|
||||||
onChange={(e) => handleUploadFormChange(e, 'name')}
|
multiple
|
||||||
fullWidth
|
limit={1}
|
||||||
/>
|
autoUpload={false}
|
||||||
{/*<TextField*/}
|
tip={<div className="el-upload__tip">Only zip file can be uploaded</div>}
|
||||||
{/* required*/}
|
|
||||||
{/* margin="dense"*/}
|
|
||||||
{/* id="entry"*/}
|
|
||||||
{/* label="Entry"*/}
|
|
||||||
{/* value={uploadForm.entry}*/}
|
|
||||||
{/* onChange={(e) => handleUploadFormChange(e, 'entry')}*/}
|
|
||||||
{/* fullWidth*/}
|
|
||||||
{/*/>*/}
|
|
||||||
{/*<TextField*/}
|
|
||||||
{/* required*/}
|
|
||||||
{/* margin="dense"*/}
|
|
||||||
{/* id="game"*/}
|
|
||||||
{/* label="Game"*/}
|
|
||||||
{/* value={uploadForm.game}*/}
|
|
||||||
{/* onChange={(e) => handleUploadFormChange(e, 'game')}*/}
|
|
||||||
{/* fullWidth*/}
|
|
||||||
{/*/>*/}
|
|
||||||
<FormControl required className={classes.formControl} fullWidth>
|
|
||||||
<InputLabel id="upload-game-label">Game</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="upload-game-label"
|
|
||||||
id="upload-game"
|
|
||||||
value={uploadForm.game}
|
|
||||||
onChange={(e) => handleUploadFormChange(e, "game")}
|
|
||||||
>
|
>
|
||||||
{props.gameList.map(game => {
|
<i className="el-icon-upload" />
|
||||||
return <MenuItem key={"upload-game-"+game.game} value={game.game}>{game.dispName}</MenuItem>
|
<div className="el-upload__text">
|
||||||
})}
|
Drag the file here, or <em>Click to upload</em>
|
||||||
</Select>
|
</div>
|
||||||
</FormControl>
|
</Upload>
|
||||||
</DialogContent>
|
<TextField
|
||||||
<DialogActions>
|
className={classes.formControl}
|
||||||
<Button onClick={handleUploadDialogClose} variant="contained" disableElevation>
|
required
|
||||||
Cancel
|
margin="dense"
|
||||||
</Button>
|
id="name"
|
||||||
<Button onClick={handleSubmitUpload} color="primary" variant="contained" disableElevation>
|
label="Model Name"
|
||||||
Upload
|
value={uploadForm.name}
|
||||||
</Button>
|
onChange={(e) => handleUploadFormChange(e, 'name')}
|
||||||
</DialogActions>
|
fullWidth
|
||||||
|
/>
|
||||||
|
{/*<TextField*/}
|
||||||
|
{/* required*/}
|
||||||
|
{/* margin="dense"*/}
|
||||||
|
{/* id="entry"*/}
|
||||||
|
{/* label="Entry"*/}
|
||||||
|
{/* value={uploadForm.entry}*/}
|
||||||
|
{/* onChange={(e) => handleUploadFormChange(e, 'entry')}*/}
|
||||||
|
{/* fullWidth*/}
|
||||||
|
{/*/>*/}
|
||||||
|
{/*<TextField*/}
|
||||||
|
{/* required*/}
|
||||||
|
{/* margin="dense"*/}
|
||||||
|
{/* id="game"*/}
|
||||||
|
{/* label="Game"*/}
|
||||||
|
{/* value={uploadForm.game}*/}
|
||||||
|
{/* onChange={(e) => handleUploadFormChange(e, 'game')}*/}
|
||||||
|
{/* fullWidth*/}
|
||||||
|
{/*/>*/}
|
||||||
|
<FormControl required className={classes.formControl} fullWidth>
|
||||||
|
<InputLabel id="upload-game-label">Game</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="upload-game-label"
|
||||||
|
id="upload-game"
|
||||||
|
value={uploadForm.game}
|
||||||
|
onChange={(e) => handleUploadFormChange(e, 'game')}
|
||||||
|
>
|
||||||
|
{props.gameList.map((game) => {
|
||||||
|
return (
|
||||||
|
<MenuItem key={'upload-game-' + game.game} value={game.game}>
|
||||||
|
{game.dispName}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleUploadDialogClose} variant="contained" disableElevation>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmitUpload} color="primary" variant="contained" disableElevation>
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
</Loading>
|
</Loading>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<Dialog
|
||||||
|
open={uploadDialogOpen}
|
||||||
|
onClose={handleUploadDialogClose}
|
||||||
|
aria-labelledby="form-dialog-title"
|
||||||
|
disableBackdropClick={true}
|
||||||
|
>
|
||||||
|
<DialogTitle id="form-dialog-title">Choose Download Channel</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Button>Google Drive</Button>
|
||||||
|
<Button>百度网盘</Button>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MenuBar;
|
export default MenuBar;
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
import React, {useEffect} from 'react';
|
import Breadcrumbs from "@material-ui/core/Breadcrumbs";
|
||||||
import qs from 'query-string';
|
import Button from "@material-ui/core/Button";
|
||||||
import MenuBar from "../components/MenuBar";
|
import Paper from '@material-ui/core/Paper';
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { lighten, makeStyles } from '@material-ui/core/styles';
|
import { lighten, makeStyles } from '@material-ui/core/styles';
|
||||||
|
import withStyles from "@material-ui/core/styles/withStyles";
|
||||||
import Table from '@material-ui/core/Table';
|
import Table from '@material-ui/core/Table';
|
||||||
import TableBody from '@material-ui/core/TableBody';
|
import TableBody from '@material-ui/core/TableBody';
|
||||||
import TableCell from '@material-ui/core/TableCell';
|
import TableCell from '@material-ui/core/TableCell';
|
||||||
import TableContainer from '@material-ui/core/TableContainer';
|
import TableContainer from '@material-ui/core/TableContainer';
|
||||||
import TableHead from '@material-ui/core/TableHead';
|
import TableHead from '@material-ui/core/TableHead';
|
||||||
|
import TablePagination from "@material-ui/core/TablePagination";
|
||||||
import TableRow from '@material-ui/core/TableRow';
|
import TableRow from '@material-ui/core/TableRow';
|
||||||
import Toolbar from '@material-ui/core/Toolbar';
|
import Toolbar from '@material-ui/core/Toolbar';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
import Paper from '@material-ui/core/Paper';
|
|
||||||
import { apiUrl } from "../utils/config";
|
|
||||||
import axios from 'axios';
|
|
||||||
import TablePagination from "@material-ui/core/TablePagination";
|
|
||||||
import Breadcrumbs from "@material-ui/core/Breadcrumbs";
|
|
||||||
import withStyles from "@material-ui/core/styles/withStyles";
|
|
||||||
import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline';
|
import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline';
|
||||||
import Button from "@material-ui/core/Button";
|
import axios from 'axios';
|
||||||
import {useHistory} from "react-router-dom";
|
import { Loading, Message } from "element-react";
|
||||||
import {Message, Loading} from "element-react";
|
import PropTypes from 'prop-types';
|
||||||
|
import qs from 'query-string';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import MenuBar from "../components/MenuBar";
|
||||||
|
import { apiUrl } from "../utils/config";
|
||||||
|
|
||||||
|
|
||||||
const gameList = [
|
const gameList = [
|
||||||
{game: 'leduc-holdem', dispName: 'Leduc Hold\'em'},
|
{game: 'leduc-holdem', dispName: 'Leduc Hold\'em'},
|
||||||
|
@ -241,7 +241,7 @@ const EnhancedTableToolbar = (props) => {
|
||||||
const handleLaunchTournament = (gameName) => {
|
const handleLaunchTournament = (gameName) => {
|
||||||
// todo: customize eval num
|
// todo: customize eval num
|
||||||
setButtonLoading(true);
|
setButtonLoading(true);
|
||||||
axios.get(`${apiUrl}/tournament/launch?eval_num=200&name=${gameName}`)
|
axios.get(`${apiUrl}/tournament/launch?num_eval_games=200&name=${gameName}`)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
setTimeout(() => {setButtonLoading(false)}, 250);
|
setTimeout(() => {setButtonLoading(false)}, 250);
|
||||||
Message({
|
Message({
|
||||||
|
|
|
@ -1,51 +1,49 @@
|
||||||
import React from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import qs from 'query-string';
|
|
||||||
import '../../assets/gameview.scss';
|
|
||||||
import {LeducHoldemGameBoard} from '../../components/GameBoard';
|
|
||||||
import {deepCopy} from "../../utils";
|
|
||||||
import { apiUrl } from "../../utils/config";
|
|
||||||
|
|
||||||
import { Layout, Loading } from 'element-react';
|
|
||||||
import { Message } from 'element-react';
|
|
||||||
import Slider from '@material-ui/core/Slider';
|
|
||||||
import Button from '@material-ui/core/Button';
|
import Button from '@material-ui/core/Button';
|
||||||
import Paper from '@material-ui/core/Paper';
|
|
||||||
import Divider from '@material-ui/core/Divider';
|
|
||||||
import LinearProgress from '@material-ui/core/LinearProgress';
|
|
||||||
import PlayArrowRoundedIcon from '@material-ui/icons/PlayArrowRounded';
|
|
||||||
import PauseCircleOutlineRoundedIcon from '@material-ui/icons/PauseCircleOutlineRounded';
|
|
||||||
import ReplayRoundedIcon from '@material-ui/icons/ReplayRounded';
|
|
||||||
import NotInterestedIcon from '@material-ui/icons/NotInterested';
|
|
||||||
import SkipNextIcon from '@material-ui/icons/SkipNext';
|
|
||||||
import SkipPreviousIcon from '@material-ui/icons/SkipPrevious';
|
|
||||||
import Dialog from '@material-ui/core/Dialog';
|
import Dialog from '@material-ui/core/Dialog';
|
||||||
import DialogActions from '@material-ui/core/DialogActions';
|
import DialogActions from '@material-ui/core/DialogActions';
|
||||||
import DialogContent from '@material-ui/core/DialogContent';
|
import DialogContent from '@material-ui/core/DialogContent';
|
||||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||||
|
import Divider from '@material-ui/core/Divider';
|
||||||
|
import LinearProgress from '@material-ui/core/LinearProgress';
|
||||||
|
import Paper from '@material-ui/core/Paper';
|
||||||
|
import Slider from '@material-ui/core/Slider';
|
||||||
|
import NotInterestedIcon from '@material-ui/icons/NotInterested';
|
||||||
|
import PauseCircleOutlineRoundedIcon from '@material-ui/icons/PauseCircleOutlineRounded';
|
||||||
|
import PlayArrowRoundedIcon from '@material-ui/icons/PlayArrowRounded';
|
||||||
|
import ReplayRoundedIcon from '@material-ui/icons/ReplayRounded';
|
||||||
|
import SkipNextIcon from '@material-ui/icons/SkipNext';
|
||||||
|
import SkipPreviousIcon from '@material-ui/icons/SkipPrevious';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Layout, Loading, Message } from 'element-react';
|
||||||
|
import qs from 'query-string';
|
||||||
|
import React from 'react';
|
||||||
|
import '../../assets/gameview.scss';
|
||||||
|
import { LeducHoldemGameBoard } from '../../components/GameBoard';
|
||||||
|
import { deepCopy } from '../../utils';
|
||||||
|
import { apiUrl } from '../../utils/config';
|
||||||
|
|
||||||
class LeducHoldemReplayView extends React.Component {
|
class LeducHoldemReplayView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const mainViewerId = 0; // Id of the player at the bottom of screen
|
const mainViewerId = 0; // Id of the player at the bottom of screen
|
||||||
this.initConsiderationTime = 2000;
|
this.initConsiderationTime = 2000;
|
||||||
this.considerationTimeDeduction = 100;
|
this.considerationTimeDeduction = 100;
|
||||||
this.gameStateTimeout = null;
|
this.gameStateTimeout = null;
|
||||||
this.moveHistory = [];
|
this.moveHistory = [];
|
||||||
this.moveHistoryTotalLength = null;
|
this.moveHistoryTotalLength = null;
|
||||||
this.gameStateHistory = [[],[]];
|
this.gameStateHistory = [[], []];
|
||||||
this.initGameState = {
|
this.initGameState = {
|
||||||
gameStatus: "ready", // "ready", "playing", "paused", "over"
|
gameStatus: 'ready', // "ready", "playing", "paused", "over"
|
||||||
playerInfo: [],
|
playerInfo: [],
|
||||||
hands: [],
|
hands: [],
|
||||||
latestAction: ["", ""],
|
latestAction: ['', ''],
|
||||||
mainViewerId: mainViewerId,
|
mainViewerId: mainViewerId,
|
||||||
round: 0,
|
round: 0,
|
||||||
turn: 0,
|
turn: 0,
|
||||||
pot: [1, 1],
|
pot: [1, 1],
|
||||||
publicCard: "",
|
publicCard: '',
|
||||||
currentPlayer: null,
|
currentPlayer: null,
|
||||||
considerationTime: this.initConsiderationTime,
|
considerationTime: this.initConsiderationTime,
|
||||||
completedPercent: 0,
|
completedPercent: 0,
|
||||||
|
@ -55,61 +53,66 @@ class LeducHoldemReplayView extends React.Component {
|
||||||
gameStateLoop: null,
|
gameStateLoop: null,
|
||||||
gameSpeed: 0,
|
gameSpeed: 0,
|
||||||
gameEndDialog: false,
|
gameEndDialog: false,
|
||||||
gameEndDialogText: "",
|
gameEndDialogText: '',
|
||||||
fullScreenLoading: false
|
fullScreenLoading: false,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
generateNewState(){
|
generateNewState() {
|
||||||
let gameInfo = deepCopy(this.state.gameInfo);
|
let gameInfo = deepCopy(this.state.gameInfo);
|
||||||
const turn = this.state.gameInfo.turn;
|
const turn = this.state.gameInfo.turn;
|
||||||
if(turn >= this.moveHistory[this.state.gameInfo.round].length){
|
if (turn >= this.moveHistory[this.state.gameInfo.round].length) {
|
||||||
gameInfo.turn = 0;
|
gameInfo.turn = 0;
|
||||||
gameInfo.round = 1;
|
gameInfo.round = 1;
|
||||||
// check if the game state of next turn is already in game state history
|
// check if the game state of next turn is already in game state history
|
||||||
if(gameInfo.turn < this.gameStateHistory[gameInfo.round].length){
|
if (gameInfo.turn < this.gameStateHistory[gameInfo.round].length) {
|
||||||
gameInfo = deepCopy(this.gameStateHistory[gameInfo.round][gameInfo.turn]);
|
gameInfo = deepCopy(this.gameStateHistory[gameInfo.round][gameInfo.turn]);
|
||||||
return gameInfo;
|
return gameInfo;
|
||||||
}
|
}
|
||||||
gameInfo.latestAction = ["", ""];
|
gameInfo.latestAction = ['', ''];
|
||||||
gameInfo.currentPlayer = this.moveHistory[1][0].playerIdx;
|
gameInfo.currentPlayer = this.moveHistory[1][0].playerIdx;
|
||||||
gameInfo.considerationTime = this.initConsiderationTime;
|
gameInfo.considerationTime = this.initConsiderationTime;
|
||||||
this.setState({ gameInfo: gameInfo });
|
this.setState({ gameInfo: gameInfo });
|
||||||
}else{
|
} else {
|
||||||
// check if the game state of next turn is already in game state history
|
// check if the game state of next turn is already in game state history
|
||||||
if(turn+1 < this.gameStateHistory[gameInfo.round].length){
|
if (turn + 1 < this.gameStateHistory[gameInfo.round].length) {
|
||||||
gameInfo = deepCopy(this.gameStateHistory[gameInfo.round][gameInfo.turn+1]);
|
gameInfo = deepCopy(this.gameStateHistory[gameInfo.round][gameInfo.turn + 1]);
|
||||||
return gameInfo;
|
return gameInfo;
|
||||||
}
|
}
|
||||||
if(gameInfo.currentPlayer === this.moveHistory[gameInfo.round][gameInfo.turn].playerIdx){
|
if (gameInfo.currentPlayer === this.moveHistory[gameInfo.round][gameInfo.turn].playerIdx) {
|
||||||
gameInfo.latestAction[gameInfo.currentPlayer] = this.moveHistory[gameInfo.round][gameInfo.turn].move;
|
gameInfo.latestAction[gameInfo.currentPlayer] = this.moveHistory[gameInfo.round][gameInfo.turn].move;
|
||||||
switch (gameInfo.latestAction[gameInfo.currentPlayer]) {
|
switch (gameInfo.latestAction[gameInfo.currentPlayer]) {
|
||||||
case "check":
|
case 'check':
|
||||||
break;
|
break;
|
||||||
case "raise":
|
case 'raise':
|
||||||
gameInfo.pot[gameInfo.currentPlayer] = (gameInfo.pot[(gameInfo.currentPlayer+2-1)%2] + (gameInfo.round+1) * 2);
|
gameInfo.pot[gameInfo.currentPlayer] =
|
||||||
|
gameInfo.pot[(gameInfo.currentPlayer + 2 - 1) % 2] + (gameInfo.round + 1) * 2;
|
||||||
break;
|
break;
|
||||||
case "call":
|
case 'call':
|
||||||
// the upstream player must have bet more
|
// the upstream player must have bet more
|
||||||
if(gameInfo.pot[(gameInfo.currentPlayer+2-1)%2] > gameInfo.pot[gameInfo.currentPlayer]){
|
if (gameInfo.pot[(gameInfo.currentPlayer + 2 - 1) % 2] > gameInfo.pot[gameInfo.currentPlayer]) {
|
||||||
gameInfo.pot[gameInfo.currentPlayer] = gameInfo.pot[(gameInfo.currentPlayer+2-1)%2];
|
gameInfo.pot[gameInfo.currentPlayer] = gameInfo.pot[(gameInfo.currentPlayer + 2 - 1) % 2];
|
||||||
}else{
|
} else {
|
||||||
Message({
|
Message({
|
||||||
message: "Current player choose call but has bet more or equal to the upstream player",
|
message: 'Current player choose call but has bet more or equal to the upstream player',
|
||||||
type: "error",
|
type: 'error',
|
||||||
showClose: true
|
showClose: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "fold":
|
case 'fold':
|
||||||
// if one player folds, game ends
|
// if one player folds, game ends
|
||||||
const foldedFound = gameInfo.playerInfo.find(element=>{return element.index === gameInfo.currentPlayer});
|
const foldedFound = gameInfo.playerInfo.find((element) => {
|
||||||
|
return element.index === gameInfo.currentPlayer;
|
||||||
|
});
|
||||||
const foldedId = foldedFound ? foldedFound.id : -1;
|
const foldedId = foldedFound ? foldedFound.id : -1;
|
||||||
const winnerFound = gameInfo.playerInfo.find(element=>{return element.index === (gameInfo.currentPlayer+1)%2});
|
const winnerFound = gameInfo.playerInfo.find((element) => {
|
||||||
|
return element.index === (gameInfo.currentPlayer + 1) % 2;
|
||||||
|
});
|
||||||
const winnerId = winnerFound ? winnerFound.id : -1;
|
const winnerId = winnerFound ? winnerFound.id : -1;
|
||||||
gameInfo.gameStatus = "over";
|
gameInfo.gameStatus = 'over';
|
||||||
this.setState({ gameInfo: gameInfo });
|
this.setState({ gameInfo: gameInfo });
|
||||||
setTimeout(()=>{
|
setTimeout(() => {
|
||||||
const mes = `Player ${foldedId} folded, player ${winnerId} wins!`;
|
const mes = `Player ${foldedId} folded, player ${winnerId} wins!`;
|
||||||
this.setState({ gameEndDialog: true, gameEndDialogText: mes });
|
this.setState({ gameEndDialog: true, gameEndDialogText: mes });
|
||||||
}, 200);
|
}, 200);
|
||||||
|
@ -117,76 +120,76 @@ class LeducHoldemReplayView extends React.Component {
|
||||||
default:
|
default:
|
||||||
Message({
|
Message({
|
||||||
message: "Error in player's latest action",
|
message: "Error in player's latest action",
|
||||||
type: "error",
|
type: 'error',
|
||||||
showClose: true
|
showClose: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
gameInfo.turn++;
|
gameInfo.turn++;
|
||||||
if(gameInfo.round !== 0 && gameInfo.turn === this.moveHistory[gameInfo.round].length){
|
if (gameInfo.round !== 0 && gameInfo.turn === this.moveHistory[gameInfo.round].length) {
|
||||||
gameInfo.gameStatus = "over";
|
gameInfo.gameStatus = 'over';
|
||||||
this.setState({gameInfo: gameInfo});
|
this.setState({ gameInfo: gameInfo });
|
||||||
setTimeout(()=>{
|
setTimeout(() => {
|
||||||
// TODO: show winner
|
// TODO: show winner
|
||||||
this.setState({gameEndDialog: true, gameEndDialogText: ""});
|
this.setState({ gameEndDialog: true, gameEndDialogText: '' });
|
||||||
}, 200);
|
}, 200);
|
||||||
return gameInfo;
|
return gameInfo;
|
||||||
}
|
}
|
||||||
gameInfo.currentPlayer = (gameInfo.currentPlayer+1)%2;
|
gameInfo.currentPlayer = (gameInfo.currentPlayer + 1) % 2;
|
||||||
gameInfo.considerationTime = this.initConsiderationTime;
|
gameInfo.considerationTime = this.initConsiderationTime;
|
||||||
gameInfo.completedPercent += 100.0 / this.moveHistoryTotalLength;
|
gameInfo.completedPercent += 100.0 / this.moveHistoryTotalLength;
|
||||||
gameInfo.gameStatus = "playing";
|
gameInfo.gameStatus = 'playing';
|
||||||
this.setState({ gameInfo: gameInfo });
|
this.setState({ gameInfo: gameInfo });
|
||||||
}else{
|
} else {
|
||||||
Message({
|
Message({
|
||||||
message: "Mismatch in current player & move history",
|
message: 'Mismatch in current player & move history',
|
||||||
type: "error",
|
type: 'error',
|
||||||
showClose: true
|
showClose: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if current state is new to game state history, push it to the game state history array
|
// if current state is new to game state history, push it to the game state history array
|
||||||
if(gameInfo.turn === this.gameStateHistory[gameInfo.round].length){
|
if (gameInfo.turn === this.gameStateHistory[gameInfo.round].length) {
|
||||||
this.gameStateHistory[gameInfo.round].push(gameInfo);
|
this.gameStateHistory[gameInfo.round].push(gameInfo);
|
||||||
}else{
|
} else {
|
||||||
Message({
|
Message({
|
||||||
message: "Inconsistent game state history length and turn number",
|
message: 'Inconsistent game state history length and turn number',
|
||||||
type: "error",
|
type: 'error',
|
||||||
showClose: true
|
showClose: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return gameInfo;
|
return gameInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStateTimer(){
|
gameStateTimer() {
|
||||||
this.gameStateTimeout = setTimeout(()=>{
|
this.gameStateTimeout = setTimeout(() => {
|
||||||
let currentConsiderationTime = this.state.gameInfo.considerationTime;
|
let currentConsiderationTime = this.state.gameInfo.considerationTime;
|
||||||
if(currentConsiderationTime > 0) {
|
if (currentConsiderationTime > 0) {
|
||||||
currentConsiderationTime -= this.considerationTimeDeduction * Math.pow(2, this.state.gameSpeed);
|
currentConsiderationTime -= this.considerationTimeDeduction * Math.pow(2, this.state.gameSpeed);
|
||||||
currentConsiderationTime = currentConsiderationTime < 0 ? 0 : currentConsiderationTime;
|
currentConsiderationTime = currentConsiderationTime < 0 ? 0 : currentConsiderationTime;
|
||||||
if(currentConsiderationTime === 0 && this.state.gameSpeed < 2){
|
if (currentConsiderationTime === 0 && this.state.gameSpeed < 2) {
|
||||||
let gameInfo = deepCopy(this.state.gameInfo);
|
let gameInfo = deepCopy(this.state.gameInfo);
|
||||||
gameInfo.toggleFade = "fade-out";
|
gameInfo.toggleFade = 'fade-out';
|
||||||
this.setState({gameInfo: gameInfo});
|
this.setState({ gameInfo: gameInfo });
|
||||||
}
|
}
|
||||||
let gameInfo = deepCopy(this.state.gameInfo);
|
let gameInfo = deepCopy(this.state.gameInfo);
|
||||||
gameInfo.considerationTime = currentConsiderationTime;
|
gameInfo.considerationTime = currentConsiderationTime;
|
||||||
this.setState({gameInfo: gameInfo});
|
this.setState({ gameInfo: gameInfo });
|
||||||
this.gameStateTimer();
|
this.gameStateTimer();
|
||||||
}else{
|
} else {
|
||||||
let gameInfo = this.generateNewState();
|
let gameInfo = this.generateNewState();
|
||||||
if(gameInfo.gameStatus === "over") return;
|
if (gameInfo.gameStatus === 'over') return;
|
||||||
this.gameStateTimer();
|
this.gameStateTimer();
|
||||||
gameInfo.gameStatus = "playing";
|
gameInfo.gameStatus = 'playing';
|
||||||
if(this.state.gameInfo.toggleFade === "fade-out") {
|
if (this.state.gameInfo.toggleFade === 'fade-out') {
|
||||||
gameInfo.toggleFade = "fade-in";
|
gameInfo.toggleFade = 'fade-in';
|
||||||
}
|
}
|
||||||
this.setState({gameInfo: gameInfo}, ()=>{
|
this.setState({ gameInfo: gameInfo }, () => {
|
||||||
// toggle fade in
|
// toggle fade in
|
||||||
if(this.state.gameInfo.toggleFade !== ""){
|
if (this.state.gameInfo.toggleFade !== '') {
|
||||||
setTimeout(()=>{
|
setTimeout(() => {
|
||||||
let gameInfo = deepCopy(this.state.gameInfo);
|
let gameInfo = deepCopy(this.state.gameInfo);
|
||||||
gameInfo.toggleFade = "";
|
gameInfo.toggleFade = '';
|
||||||
this.setState({gameInfo: gameInfo});
|
this.setState({ gameInfo: gameInfo });
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -198,36 +201,40 @@ class LeducHoldemReplayView extends React.Component {
|
||||||
const { name, agent0, agent1, index } = qs.parse(window.location.search);
|
const { name, agent0, agent1, index } = qs.parse(window.location.search);
|
||||||
const requestUrl = `${apiUrl}/tournament/replay?name=${name}&agent0=${agent0}&agent1=${agent1}&index=${index}`;
|
const requestUrl = `${apiUrl}/tournament/replay?name=${name}&agent0=${agent0}&agent1=${agent1}&index=${index}`;
|
||||||
// start full screen loading
|
// start full screen loading
|
||||||
this.setState({fullScreenLoading: true});
|
this.setState({ fullScreenLoading: true });
|
||||||
|
|
||||||
axios.get(requestUrl)
|
axios
|
||||||
.then(res => {
|
.get(requestUrl)
|
||||||
|
.then((res) => {
|
||||||
res = res.data;
|
res = res.data;
|
||||||
|
// for test use
|
||||||
|
if (typeof res === 'string') res = JSON.parse(res.replaceAll("'", '"').replaceAll('None', 'null'));
|
||||||
|
|
||||||
if (res.moveHistory.length === 0) {
|
if (res.moveHistory.length === 0) {
|
||||||
Message({
|
Message({
|
||||||
message: "Empty move history",
|
message: 'Empty move history',
|
||||||
type: "error",
|
type: 'error',
|
||||||
showClose: true,
|
showClose: true,
|
||||||
});
|
});
|
||||||
this.setState({fullScreenLoading: false});
|
this.setState({ fullScreenLoading: false });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// init replay info
|
// init replay info
|
||||||
this.moveHistory = res.moveHistory;
|
this.moveHistory = res.moveHistory;
|
||||||
this.moveHistoryTotalLength = this.moveHistory.reduce((count, round) => count + round.length, 0) - 1;
|
this.moveHistoryTotalLength = this.moveHistory.reduce((count, round) => count + round.length, 0) - 1;
|
||||||
let gameInfo = deepCopy(this.initGameState);
|
let gameInfo = deepCopy(this.initGameState);
|
||||||
gameInfo.gameStatus = "playing";
|
gameInfo.gameStatus = 'playing';
|
||||||
gameInfo.playerInfo = res.playerInfo;
|
gameInfo.playerInfo = res.playerInfo;
|
||||||
gameInfo.hands = res.initHands;
|
gameInfo.hands = res.initHands;
|
||||||
gameInfo.currentPlayer = res.moveHistory[0][0].playerIdx;
|
gameInfo.currentPlayer = res.moveHistory[0][0].playerIdx;
|
||||||
// the other player is big blind, should have 2 unit in pot
|
// the other player is big blind, should have 2 unit in pot
|
||||||
gameInfo.pot[(res.moveHistory[0][0].playerIdx + 1) % 2] = 2;
|
gameInfo.pot[(res.moveHistory[0][0].playerIdx + 1) % 2] = 2;
|
||||||
gameInfo.publicCard = res.publicCard;
|
gameInfo.publicCard = res.publicCard;
|
||||||
if(this.gameStateHistory.length !== 0 && this.gameStateHistory[0].length === 0){
|
if (this.gameStateHistory.length !== 0 && this.gameStateHistory[0].length === 0) {
|
||||||
this.gameStateHistory[gameInfo.round].push(gameInfo);
|
this.gameStateHistory[gameInfo.round].push(gameInfo);
|
||||||
}
|
}
|
||||||
this.setState({gameInfo: gameInfo, fullScreenLoading: false}, ()=>{
|
this.setState({ gameInfo: gameInfo, fullScreenLoading: false }, () => {
|
||||||
if(this.gameStateTimeout){
|
if (this.gameStateTimeout) {
|
||||||
window.clearTimeout(this.gameStateTimeout);
|
window.clearTimeout(this.gameStateTimeout);
|
||||||
this.gameStateTimeout = null;
|
this.gameStateTimeout = null;
|
||||||
}
|
}
|
||||||
|
@ -235,114 +242,195 @@ class LeducHoldemReplayView extends React.Component {
|
||||||
this.gameStateTimer();
|
this.gameStateTimer();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
Message({
|
Message({
|
||||||
message: `Error in getting replay data: ${err}`,
|
message: `Error in getting replay data: ${err}`,
|
||||||
type: "error",
|
type: 'error',
|
||||||
showClose: true
|
showClose: true,
|
||||||
});
|
});
|
||||||
this.setState({fullScreenLoading: false});
|
this.setState({ fullScreenLoading: false });
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
pauseReplay(){
|
pauseReplay() {
|
||||||
if(this.gameStateTimeout){
|
if (this.gameStateTimeout) {
|
||||||
window.clearTimeout(this.gameStateTimeout);
|
window.clearTimeout(this.gameStateTimeout);
|
||||||
this.gameStateTimeout = null;
|
this.gameStateTimeout = null;
|
||||||
}
|
}
|
||||||
let gameInfo = deepCopy(this.state.gameInfo);
|
let gameInfo = deepCopy(this.state.gameInfo);
|
||||||
gameInfo.gameStatus = "paused";
|
gameInfo.gameStatus = 'paused';
|
||||||
this.setState({ gameInfo: gameInfo });
|
this.setState({ gameInfo: gameInfo });
|
||||||
}
|
}
|
||||||
|
|
||||||
resumeReplay(){
|
resumeReplay() {
|
||||||
this.gameStateTimer();
|
this.gameStateTimer();
|
||||||
let gameInfo = deepCopy(this.state.gameInfo);
|
let gameInfo = deepCopy(this.state.gameInfo);
|
||||||
gameInfo.gameStatus = "playing";
|
gameInfo.gameStatus = 'playing';
|
||||||
this.setState({ gameInfo: gameInfo });
|
this.setState({ gameInfo: gameInfo });
|
||||||
}
|
}
|
||||||
|
|
||||||
changeGameSpeed(newVal){
|
changeGameSpeed(newVal) {
|
||||||
this.setState({gameSpeed: newVal});
|
this.setState({ gameSpeed: newVal });
|
||||||
}
|
}
|
||||||
|
|
||||||
gameStatusButton(status){
|
gameStatusButton(status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "ready":
|
case 'ready':
|
||||||
return <Button className={"status-button"} variant={"contained"} startIcon={<PlayArrowRoundedIcon />} color="primary" onClick={()=>{this.startReplay()}}>Start</Button>;
|
return (
|
||||||
case "playing":
|
<Button
|
||||||
return <Button className={"status-button"} variant={"contained"} startIcon={<PauseCircleOutlineRoundedIcon />} color="secondary" onClick={()=>{this.pauseReplay()}}>Pause</Button>;
|
className={'status-button'}
|
||||||
case "paused":
|
variant={'contained'}
|
||||||
return <Button className={"status-button"} variant={"contained"} startIcon={<PlayArrowRoundedIcon />} color="primary" onClick={()=>{this.resumeReplay()}}>Resume</Button>;
|
startIcon={<PlayArrowRoundedIcon />}
|
||||||
case "over":
|
color="primary"
|
||||||
return <Button className={"status-button"} variant={"contained"} startIcon={<ReplayRoundedIcon />} color="primary" onClick={()=>{this.startReplay()}}>Restart</Button>;
|
onClick={() => {
|
||||||
|
this.startReplay();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
case 'playing':
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={'status-button'}
|
||||||
|
variant={'contained'}
|
||||||
|
startIcon={<PauseCircleOutlineRoundedIcon />}
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
this.pauseReplay();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Pause
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
case 'paused':
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={'status-button'}
|
||||||
|
variant={'contained'}
|
||||||
|
startIcon={<PlayArrowRoundedIcon />}
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
this.resumeReplay();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Resume
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
case 'over':
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={'status-button'}
|
||||||
|
variant={'contained'}
|
||||||
|
startIcon={<ReplayRoundedIcon />}
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
this.startReplay();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
alert(`undefined game status: ${status}`);
|
alert(`undefined game status: ${status}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
computeProbabilityItem(idx){
|
computeProbabilityItem(action) {
|
||||||
if(this.state.gameInfo.gameStatus !== "ready"){
|
if (this.state.gameInfo.gameStatus !== 'ready') {
|
||||||
let currentMove = null;
|
let currentMove = null;
|
||||||
if(this.state.gameInfo.turn !== this.moveHistory[this.state.gameInfo.round].length){
|
if (this.state.gameInfo.turn !== this.moveHistory[this.state.gameInfo.round].length) {
|
||||||
currentMove = this.moveHistory[this.state.gameInfo.round][this.state.gameInfo.turn];
|
currentMove = this.moveHistory[this.state.gameInfo.round][this.state.gameInfo.turn];
|
||||||
}
|
}
|
||||||
let style = {};
|
let style = {};
|
||||||
style["backgroundColor"] = currentMove !== null ? `rgba(63, 81, 181, ${currentMove.probabilities[idx].probability})` : "#bdbdbd";
|
let probabilities = null;
|
||||||
|
let probabilityItemType = null;
|
||||||
|
if (currentMove) {
|
||||||
|
if (Array.isArray(currentMove.info)) {
|
||||||
|
probabilityItemType = 'Rule';
|
||||||
|
} else {
|
||||||
|
if ('probs' in currentMove.info) {
|
||||||
|
probabilityItemType = 'Probability';
|
||||||
|
probabilities = currentMove.info.probs[action];
|
||||||
|
} else if ('values' in currentMove.info) {
|
||||||
|
probabilityItemType = 'Expected payoff';
|
||||||
|
probabilities = currentMove.info.values[action];
|
||||||
|
} else {
|
||||||
|
probabilityItemType = 'Rule';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// style["backgroundColor"] = currentMove !== null ? `rgba(63, 81, 181, ${currentMove.probabilities[idx].probability})` : "#bdbdbd";
|
||||||
|
style['backgroundColor'] = currentMove !== null ? `#fff` : '#bdbdbd';
|
||||||
return (
|
return (
|
||||||
<div className={"playing"} style={style}>
|
<div className={'playing'} style={style}>
|
||||||
<div className="probability-move">
|
<div className="probability-move">
|
||||||
{currentMove !== null ?
|
{currentMove !== null ? (
|
||||||
<img src={require('../../assets/images/Actions/' + currentMove.probabilities[idx].move + (currentMove.probabilities[idx].probability < 0 ? "_u" : "") + '.png')} alt={currentMove.probabilities[idx].move} height="30%" width="30%" />
|
<img
|
||||||
:
|
src={require('../../assets/images/Actions/' +
|
||||||
<NotInterestedIcon fontSize="large" />}
|
action +
|
||||||
|
(probabilities === undefined || probabilities === null ? '_u' : '') +
|
||||||
|
'.png')}
|
||||||
|
alt={action}
|
||||||
|
height="30%"
|
||||||
|
width="30%"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<NotInterestedIcon fontSize="large" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{currentMove !== null ?
|
{currentMove !== null ? (
|
||||||
(<div className={"non-card"}>
|
<div className={'non-card'}>
|
||||||
{
|
{probabilities === undefined ? (
|
||||||
currentMove.probabilities[idx].probability === -1 ?
|
|
||||||
<span>Illegal</span>
|
<span>Illegal</span>
|
||||||
:
|
) : probabilityItemType === 'Rule' ? (
|
||||||
currentMove.probabilities[idx].probability === -2 ?
|
|
||||||
<span>Rule Based</span>
|
<span>Rule Based</span>
|
||||||
:
|
) : (
|
||||||
<span>{`Probability ${(currentMove.probabilities[idx].probability * 100).toFixed(2)}%`}</span>
|
<span>
|
||||||
}
|
{probabilityItemType === 'Probability'
|
||||||
</div>) : ""}
|
? `Probability: ${(probabilities * 100).toFixed(2)}%`
|
||||||
|
: `Expected payoff: ${probabilities.toFixed(4)}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}else {
|
} else {
|
||||||
return <span className={"waiting"}>Waiting...</span>
|
return <span className={'waiting'}>Waiting...</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go2PrevGameState() {
|
go2PrevGameState() {
|
||||||
let gameInfo;
|
let gameInfo;
|
||||||
if(this.state.gameInfo.turn === 0 && this.state.gameInfo.round !== 0){
|
if (this.state.gameInfo.turn === 0 && this.state.gameInfo.round !== 0) {
|
||||||
let prevRound = this.gameStateHistory[this.state.gameInfo.round-1];
|
let prevRound = this.gameStateHistory[this.state.gameInfo.round - 1];
|
||||||
gameInfo = deepCopy(prevRound[prevRound.length-1]);
|
gameInfo = deepCopy(prevRound[prevRound.length - 1]);
|
||||||
}else{
|
} else {
|
||||||
gameInfo = deepCopy(this.gameStateHistory[this.state.gameInfo.round][this.state.gameInfo.turn - 1]);
|
gameInfo = deepCopy(this.gameStateHistory[this.state.gameInfo.round][this.state.gameInfo.turn - 1]);
|
||||||
}
|
}
|
||||||
gameInfo.gameStatus = "paused";
|
gameInfo.gameStatus = 'paused';
|
||||||
gameInfo.toggleFade = "";
|
gameInfo.toggleFade = '';
|
||||||
this.setState({gameInfo: gameInfo});
|
this.setState({ gameInfo: gameInfo });
|
||||||
}
|
}
|
||||||
|
|
||||||
go2NextGameState() {
|
go2NextGameState() {
|
||||||
let gameInfo = this.generateNewState();
|
let gameInfo = this.generateNewState();
|
||||||
if(gameInfo.gameStatus === "over") return;
|
if (gameInfo.gameStatus === 'over') return;
|
||||||
gameInfo.gameStatus = "paused";
|
gameInfo.gameStatus = 'paused';
|
||||||
gameInfo.toggleFade = "";
|
gameInfo.toggleFade = '';
|
||||||
this.setState({gameInfo: gameInfo});
|
this.setState({ gameInfo: gameInfo });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCloseGameEndDialog() {
|
handleCloseGameEndDialog() {
|
||||||
this.setState({gameEndDialog: false, gameEndDialogText: ""});
|
this.setState({ gameEndDialog: false, gameEndDialogText: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
render(){
|
render() {
|
||||||
let sliderValueText = (value) => {
|
let sliderValueText = (value) => {
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
@ -374,34 +462,44 @@ class LeducHoldemReplayView extends React.Component {
|
||||||
{
|
{
|
||||||
value: 3,
|
value: 3,
|
||||||
label: 'x8',
|
label: 'x8',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Dialog
|
<Dialog
|
||||||
open={this.state.gameEndDialog}
|
open={this.state.gameEndDialog}
|
||||||
onClose={()=>{this.handleCloseGameEndDialog()}}
|
onClose={() => {
|
||||||
|
this.handleCloseGameEndDialog();
|
||||||
|
}}
|
||||||
aria-labelledby="alert-dialog-title"
|
aria-labelledby="alert-dialog-title"
|
||||||
aria-describedby="alert-dialog-description"
|
aria-describedby="alert-dialog-description"
|
||||||
>
|
>
|
||||||
<DialogTitle id="alert-dialog-title" style={{"width": "200px"}}>{"Game Ends!"}</DialogTitle>
|
<DialogTitle id="alert-dialog-title" style={{ width: '200px' }}>
|
||||||
|
{'Game Ends!'}
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText id="alert-dialog-description">
|
<DialogContentText id="alert-dialog-description">
|
||||||
{this.state.gameEndDialogText}
|
{this.state.gameEndDialogText}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={()=>{this.handleCloseGameEndDialog()}} color="primary" autoFocus>
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
this.handleCloseGameEndDialog();
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
OK
|
OK
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<div className={"leduc-view-container"}>
|
<div className={'leduc-view-container'}>
|
||||||
<Layout.Row style={{"height": "540px"}}>
|
<Layout.Row style={{ height: '540px' }}>
|
||||||
<Layout.Col style={{"height": "100%"}} span="17">
|
<Layout.Col style={{ height: '100%' }} span="17">
|
||||||
<div style={{"height": "100%"}}>
|
<div style={{ height: '100%' }}>
|
||||||
<Paper className={"leduc-gameboard-paper"} elevation={3}>
|
<Paper className={'leduc-gameboard-paper'} elevation={3}>
|
||||||
<LeducHoldemGameBoard
|
<LeducHoldemGameBoard
|
||||||
playerInfo={this.state.gameInfo.playerInfo}
|
playerInfo={this.state.gameInfo.playerInfo}
|
||||||
hands={this.state.gameInfo.hands}
|
hands={this.state.gameInfo.hands}
|
||||||
|
@ -417,30 +515,21 @@ class LeducHoldemReplayView extends React.Component {
|
||||||
</Paper>
|
</Paper>
|
||||||
</div>
|
</div>
|
||||||
</Layout.Col>
|
</Layout.Col>
|
||||||
<Layout.Col span="7" style={{"height": "100%"}}>
|
<Layout.Col span="7" style={{ height: '100%' }}>
|
||||||
<Paper className={"leduc-probability-paper"} elevation={3}>
|
<Paper className={'leduc-probability-paper'} elevation={3}>
|
||||||
<div className={"probability-player"}>
|
<div className={'probability-player'}>
|
||||||
{
|
{this.state.gameInfo.playerInfo.length > 0 ? (
|
||||||
this.state.gameInfo.playerInfo.length > 0 ?
|
|
||||||
<span>Current Player: {this.state.gameInfo.currentPlayer}</span>
|
<span>Current Player: {this.state.gameInfo.currentPlayer}</span>
|
||||||
:
|
) : (
|
||||||
<span>Waiting...</span>
|
<span>Waiting...</span>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className={"probability-table"}>
|
<div className={'probability-table'}>
|
||||||
<div className={"probability-item"}>
|
<div className={'probability-item'}>{this.computeProbabilityItem('call')}</div>
|
||||||
{this.computeProbabilityItem(0)}
|
<div className={'probability-item'}>{this.computeProbabilityItem('check')}</div>
|
||||||
</div>
|
<div className={'probability-item'}>{this.computeProbabilityItem('raise')}</div>
|
||||||
<div className={"probability-item"}>
|
<div className={'probability-item'}>{this.computeProbabilityItem('fold')}</div>
|
||||||
{this.computeProbabilityItem(1)}
|
|
||||||
</div>
|
|
||||||
<div className={"probability-item"}>
|
|
||||||
{this.computeProbabilityItem(2)}
|
|
||||||
</div>
|
|
||||||
<div className={"probability-item"}>
|
|
||||||
{this.computeProbabilityItem(3)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Layout.Col>
|
</Layout.Col>
|
||||||
|
@ -450,60 +539,77 @@ class LeducHoldemReplayView extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<Loading loading={this.state.fullScreenLoading}>
|
<Loading loading={this.state.fullScreenLoading}>
|
||||||
<div className="game-controller">
|
<div className="game-controller">
|
||||||
<Paper className={"game-controller-paper"} elevation={3}>
|
<Paper className={'game-controller-paper'} elevation={3}>
|
||||||
<Layout.Row style={{"height": "51px"}}>
|
<Layout.Row style={{ height: '51px' }}>
|
||||||
<Layout.Col span="7" style={{"height": "51px", "lineHeight": "48px"}}>
|
<Layout.Col span="7" style={{ height: '51px', lineHeight: '48px' }}>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={this.state.gameInfo.gameStatus !== "paused" || (this.state.gameInfo.round === 0 && this.state.gameInfo.turn === 0)}
|
disabled={
|
||||||
onClick={()=>{this.go2PrevGameState()}}
|
this.state.gameInfo.gameStatus !== 'paused' ||
|
||||||
>
|
(this.state.gameInfo.round === 0 && this.state.gameInfo.turn === 0)
|
||||||
<SkipPreviousIcon />
|
}
|
||||||
</Button>
|
onClick={() => {
|
||||||
{ this.gameStatusButton(this.state.gameInfo.gameStatus) }
|
this.go2PrevGameState();
|
||||||
<Button
|
}}
|
||||||
variant="contained"
|
>
|
||||||
color="primary"
|
<SkipPreviousIcon />
|
||||||
disabled={this.state.gameInfo.gameStatus !== "paused"}
|
</Button>
|
||||||
onClick={()=>{this.go2NextGameState()}}
|
{this.gameStatusButton(this.state.gameInfo.gameStatus)}
|
||||||
>
|
<Button
|
||||||
<SkipNextIcon />
|
variant="contained"
|
||||||
</Button>
|
color="primary"
|
||||||
</div>
|
disabled={this.state.gameInfo.gameStatus !== 'paused'}
|
||||||
</Layout.Col>
|
onClick={() => {
|
||||||
<Layout.Col span="1" style={{"height": "100%", "width": "1px"}}>
|
this.go2NextGameState();
|
||||||
<Divider orientation="vertical" />
|
}}
|
||||||
</Layout.Col>
|
>
|
||||||
<Layout.Col span="3" style={{"height": "51px", "lineHeight": "51px", "marginLeft": "-1px", "marginRight": "-1px"}}>
|
<SkipNextIcon />
|
||||||
<div style={{"textAlign": "center"}}>{`Turn ${this.state.gameInfo.turn}`}</div>
|
</Button>
|
||||||
</Layout.Col>
|
|
||||||
<Layout.Col span="1" style={{"height": "100%", "width": "1px"}}>
|
|
||||||
<Divider orientation="vertical" />
|
|
||||||
</Layout.Col>
|
|
||||||
<Layout.Col span="14">
|
|
||||||
<div>
|
|
||||||
<label className={"form-label-left"}>Game Speed</label>
|
|
||||||
<div style={{"marginLeft": "100px", "marginRight": "10px"}}>
|
|
||||||
<Slider
|
|
||||||
value={this.state.gameSpeed}
|
|
||||||
getAriaValueText={sliderValueText}
|
|
||||||
onChange={(e, newVal)=>{this.changeGameSpeed(newVal)}}
|
|
||||||
aria-labelledby="discrete-slider-custom"
|
|
||||||
step={1}
|
|
||||||
min={-3}
|
|
||||||
max={3}
|
|
||||||
track={false}
|
|
||||||
valueLabelDisplay="off"
|
|
||||||
marks={gameSpeedMarks}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Layout.Col>
|
||||||
</Layout.Col>
|
<Layout.Col span="1" style={{ height: '100%', width: '1px' }}>
|
||||||
</Layout.Row>
|
<Divider orientation="vertical" />
|
||||||
</Paper>
|
</Layout.Col>
|
||||||
</div>
|
<Layout.Col
|
||||||
|
span="3"
|
||||||
|
style={{
|
||||||
|
height: '51px',
|
||||||
|
lineHeight: '51px',
|
||||||
|
marginLeft: '-1px',
|
||||||
|
marginRight: '-1px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center' }}>{`Turn ${this.state.gameInfo.turn}`}</div>
|
||||||
|
</Layout.Col>
|
||||||
|
<Layout.Col span="1" style={{ height: '100%', width: '1px' }}>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
</Layout.Col>
|
||||||
|
<Layout.Col span="14">
|
||||||
|
<div>
|
||||||
|
<label className={'form-label-left'}>Game Speed</label>
|
||||||
|
<div style={{ marginLeft: '100px', marginRight: '10px' }}>
|
||||||
|
<Slider
|
||||||
|
value={this.state.gameSpeed}
|
||||||
|
getAriaValueText={sliderValueText}
|
||||||
|
onChange={(e, newVal) => {
|
||||||
|
this.changeGameSpeed(newVal);
|
||||||
|
}}
|
||||||
|
aria-labelledby="discrete-slider-custom"
|
||||||
|
step={1}
|
||||||
|
min={-3}
|
||||||
|
max={3}
|
||||||
|
track={false}
|
||||||
|
valueLabelDisplay="off"
|
||||||
|
marks={gameSpeedMarks}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout.Col>
|
||||||
|
</Layout.Row>
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
</Loading>
|
</Loading>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue