diff --git a/.gitignore b/.gitignore index 4d29575..84e5c0c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +migrations/ +db.sqlite3 diff --git a/README.md b/README.md index 3970716..beec914 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,40 @@ **NOTE: This project is under final tesing. The one in the repo only supports the visualization of some sampled data. The full version will be available soon!** +# Server Setup +Install dependencies: +``` +pip install -r requirements.txt +``` +Migrate the databases: +``` +cd server +python manage.py makemigrations +python manage.py migrate +``` +Run server: +``` +python manage.py runserver +``` +The default URL is [http://127.0.0.1:8000/](http://127.0.0.1:8000/) + +# REST API +The definitions of the fields are as follows: +* `eval_num`: Integer. The number of evaluation times. +* `name`: String. The name of the environment. +* `agent0`: String. Model name. +* `agent1`: String. Model name. +* `win`: Boolean. True if model in the first seat wins. +* `payoff`: Float. The payoff of the agent in the first seat. +* `index`: Integer. The index of the game of the same environent and same agent. It is in the range \[0, eval\_num-1\] + +| type | Resource | Parameters | Description | +|------|---------------------------|------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------| +| GET | tournament/launch | `eval\_num`, `name` | Launch tournment on the game. Each pair of models will play `eval\_num` times. Results will be saved in database. | +| GET | tournament/query\_game | `name`, `index`, `agent0`, `agent1`, `win`, `payoff` | Query the games with the given parameters | +| GET | tournament/query\_payoff | `name`, `agent0`, `agent1`, `payoff` | Query the payoffs with the given parameters | +| GET | tournament/replay | `name`, `agent0`, `agent1`, `index` | Return the replay data (only support Leduc Holdem for now) | + +# AAA This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). ## Available Scripts diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e074cb5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +rlcard +Django +tqdm +tensorflow==1.14 diff --git a/server/manage.py b/server/manage.py new file mode 100755 index 0000000..1c81878 --- /dev/null +++ b/server/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/server/server/__init__.py b/server/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/server/__pycache__/__init__.cpython-35.pyc b/server/server/__pycache__/__init__.cpython-35.pyc new file mode 100644 index 0000000..68c34cf Binary files /dev/null and b/server/server/__pycache__/__init__.cpython-35.pyc differ diff --git a/server/server/__pycache__/settings.cpython-35.pyc b/server/server/__pycache__/settings.cpython-35.pyc new file mode 100644 index 0000000..e7383c0 Binary files /dev/null and b/server/server/__pycache__/settings.cpython-35.pyc differ diff --git a/server/server/__pycache__/urls.cpython-35.pyc b/server/server/__pycache__/urls.cpython-35.pyc new file mode 100644 index 0000000..a7ae457 Binary files /dev/null and b/server/server/__pycache__/urls.cpython-35.pyc differ diff --git a/server/server/__pycache__/wsgi.cpython-35.pyc b/server/server/__pycache__/wsgi.cpython-35.pyc new file mode 100644 index 0000000..8e99008 Binary files /dev/null and b/server/server/__pycache__/wsgi.cpython-35.pyc differ diff --git a/server/server/settings.py b/server/server/settings.py new file mode 100644 index 0000000..11bb659 --- /dev/null +++ b/server/server/settings.py @@ -0,0 +1,121 @@ +""" +Django settings for server project. + +Generated by 'django-admin startproject' using Django 2.2.12. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.2/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '-t@mf4fi)gfxzv5lm8qkg)*5u^brj--y*ul2&ryqdem(xin8(!' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'tournament', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'server.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'server.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/2.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/2.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.2/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/server/server/urls.py b/server/server/urls.py new file mode 100644 index 0000000..835cfb8 --- /dev/null +++ b/server/server/urls.py @@ -0,0 +1,22 @@ +"""server URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path('tournament/', include('tournament.urls')), + path('admin/', admin.site.urls), +] diff --git a/server/server/wsgi.py b/server/server/wsgi.py new file mode 100644 index 0000000..b238574 --- /dev/null +++ b/server/server/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for server project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') + +application = get_wsgi_application() diff --git a/server/tournament/.views.py.swp b/server/tournament/.views.py.swp new file mode 100644 index 0000000..3278d92 Binary files /dev/null and b/server/tournament/.views.py.swp differ diff --git a/server/tournament/__init__.py b/server/tournament/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/tournament/__pycache__/__init__.cpython-35.pyc b/server/tournament/__pycache__/__init__.cpython-35.pyc new file mode 100644 index 0000000..5c65b9c Binary files /dev/null and b/server/tournament/__pycache__/__init__.cpython-35.pyc differ diff --git a/server/tournament/__pycache__/admin.cpython-35.pyc b/server/tournament/__pycache__/admin.cpython-35.pyc new file mode 100644 index 0000000..a912275 Binary files /dev/null and b/server/tournament/__pycache__/admin.cpython-35.pyc differ diff --git a/server/tournament/__pycache__/models.cpython-35.pyc b/server/tournament/__pycache__/models.cpython-35.pyc new file mode 100644 index 0000000..ff93652 Binary files /dev/null and b/server/tournament/__pycache__/models.cpython-35.pyc differ diff --git a/server/tournament/__pycache__/tournament.cpython-35.pyc b/server/tournament/__pycache__/tournament.cpython-35.pyc new file mode 100644 index 0000000..1a72ad4 Binary files /dev/null and b/server/tournament/__pycache__/tournament.cpython-35.pyc differ diff --git a/server/tournament/__pycache__/urls.cpython-35.pyc b/server/tournament/__pycache__/urls.cpython-35.pyc new file mode 100644 index 0000000..3c9280e Binary files /dev/null and b/server/tournament/__pycache__/urls.cpython-35.pyc differ diff --git a/server/tournament/__pycache__/views.cpython-35.pyc b/server/tournament/__pycache__/views.cpython-35.pyc new file mode 100644 index 0000000..1825b4e Binary files /dev/null and b/server/tournament/__pycache__/views.cpython-35.pyc differ diff --git a/server/tournament/admin.py b/server/tournament/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/server/tournament/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/server/tournament/apps.py b/server/tournament/apps.py new file mode 100644 index 0000000..cd8eb1b --- /dev/null +++ b/server/tournament/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TournamentConfig(AppConfig): + name = 'tournament' diff --git a/server/tournament/models.py b/server/tournament/models.py new file mode 100644 index 0000000..e33e1b2 --- /dev/null +++ b/server/tournament/models.py @@ -0,0 +1,37 @@ +from django.db import models + +class Game(models.Model): + # The name of the game + name = models.CharField(max_length=100) + + # The ID of repeated games + index = models.CharField(max_length=100) + + # The first agent + agent0 = models.CharField(max_length=100) + + # The second agent + agent1 = models.CharField(max_length=100) + + # Whether the first agent wins + win = models.BooleanField() + + # The payoff of the first agent + payoff = models.FloatField() + + # The JSON file + replay = models.TextField(blank=True) + +class Payoff(models.Model): + # The name of the game + name = models.CharField(max_length=100) + + # The first agent + agent0 = models.CharField(max_length=100) + + # The second agent + agent1 = models.CharField(max_length=100) + + # The average payoff of the first agent + payoff = models.FloatField() + diff --git a/server/tournament/rlcard_wrap/__init__.py b/server/tournament/rlcard_wrap/__init__.py new file mode 100644 index 0000000..5bf495f --- /dev/null +++ b/server/tournament/rlcard_wrap/__init__.py @@ -0,0 +1,15 @@ +import rlcard +from .leduc_holdem_random_model import LeducHoldemRandomModelSpec + + +# Register Leduc Holdem Random Model +rlcard.models.registration.model_registry.model_specs['leduc-holdem-random'] = LeducHoldemRandomModelSpec() + +# The models we are concerned +MODEL_IDS = {} +MODEL_IDS['leduc-holdem'] = [ + 'leduc-holdem-random', + 'leduc-holdem-cfr', + 'leduc-holdem-rule-v1', + ] + diff --git a/server/tournament/rlcard_wrap/__pycache__/__init__.cpython-35.pyc b/server/tournament/rlcard_wrap/__pycache__/__init__.cpython-35.pyc new file mode 100644 index 0000000..7fb4e1f Binary files /dev/null and b/server/tournament/rlcard_wrap/__pycache__/__init__.cpython-35.pyc differ diff --git a/server/tournament/rlcard_wrap/__pycache__/leduc_holdem_random_model.cpython-35.pyc b/server/tournament/rlcard_wrap/__pycache__/leduc_holdem_random_model.cpython-35.pyc new file mode 100644 index 0000000..bf08cfe Binary files /dev/null and b/server/tournament/rlcard_wrap/__pycache__/leduc_holdem_random_model.cpython-35.pyc differ diff --git a/server/tournament/rlcard_wrap/__pycache__/tournament.cpython-35.pyc b/server/tournament/rlcard_wrap/__pycache__/tournament.cpython-35.pyc new file mode 100644 index 0000000..c728737 Binary files /dev/null and b/server/tournament/rlcard_wrap/__pycache__/tournament.cpython-35.pyc differ diff --git a/server/tournament/rlcard_wrap/leduc_holdem_random_model.py b/server/tournament/rlcard_wrap/leduc_holdem_random_model.py new file mode 100644 index 0000000..a478222 --- /dev/null +++ b/server/tournament/rlcard_wrap/leduc_holdem_random_model.py @@ -0,0 +1,46 @@ +# A wrap for rlcard +# Here, we include a random model as the default baseline +import rlcard +from rlcard.agents import RandomAgent +from rlcard.models.model import Model + +class LeducHoldemRandomModelSpec(object): + def __init__(self): + self.model_id = 'leduc-holdem-random' + self._entry_point = LeducHoldemRandomModel + + def load(self): + model = self._entry_point() + return model + +class LeducHoldemRandomModel(Model): + ''' A random model + ''' + + def __init__(self): + ''' Load random model + ''' + env = rlcard.make('leduc-holdem') + self.agent = RandomAgent(action_num=env.action_num) + self.player_num = env.player_num + + @property + def agents(self): + ''' Get a list of agents for each position in a the game + + Returns: + agents (list): A list of agents + + Note: Each agent should be just like RL agent with step and eval_step + functioning well. + ''' + return [self.agent for _ in range(self.player_num)] + + @property + def use_raw(self): + ''' Indicate whether use raw state and action + + Returns: + use_raw (boolean): True if using raw state and action + ''' + return False diff --git a/server/tournament/tests.py b/server/tournament/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/server/tournament/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/server/tournament/tournament.py b/server/tournament/tournament.py new file mode 100644 index 0000000..0b52a84 --- /dev/null +++ b/server/tournament/tournament.py @@ -0,0 +1,117 @@ +import os +import json +from tqdm import tqdm +import numpy as np + +from .rlcard_wrap import rlcard + +class Tournament(object): + + def __init__(self, game, model_ids, evaluate_num=100): + """ Defalt for two player games + For Dou Dizhu, the two peasants use the same model + """ + self.game = game + self.model_ids = model_ids + self.evaluate_num = evaluate_num + # Load the models + self.models = [rlcard.models.load(model_id) for model_id in model_ids] + + def launch(self): + """ Currently for two-player game only + """ + model_num = len(self.model_ids) + games_data = [] + payoffs_data = [] + for i in range(model_num): + for j in range(model_num): + if j == i: + continue + print(self.game, '-', self.model_ids[i], 'VS', self.model_ids[j]) + data, payoffs, wins = tournament(self.game, [self.models[i].agents[0], self.models[j].agents[1]], self.evaluate_num) + mean_payoff = np.mean(payoffs) + print('Average payoff:', mean_payoff) + print() + + for k in range(len(data)): + game_data = {} + game_data['name'] = self.game + game_data['index'] = k + game_data['agent0'] = self.model_ids[i] + game_data['agent1'] = self.model_ids[j] + game_data['win'] = wins[k] + game_data['replay'] = data[k] + game_data['payoff'] = payoffs[k] + + games_data.append(game_data) + + payoff_data = {} + payoff_data['name'] = self.game + payoff_data['agent0'] = self.model_ids[i] + payoff_data['agent1'] = self.model_ids[j] + payoff_data['payoff'] = mean_payoff + payoffs_data.append(payoff_data) + return games_data, payoffs_data + +def tournament(game, agents, num): + env = rlcard.make(game, config={'allow_raw_data': True}) + env.set_agents(agents) + payoffs = [] + json_data = [] + wins = [] + for _ in tqdm(range(num)): + data = {} + data['playerInfo'] = [{'id': i, 'index': i} for i in range(env.player_num)] + state, player_id = env.reset() + perfect = env.get_perfect_information() + data['initHands'] = perfect['hand_cards'] + data['moveHistory'] = [[]] + while not env.is_over(): + action, probs = env.agents[player_id].eval_step(state) + history = {} + history['playerIdx'] = player_id + if env.agents[player_id].use_raw: + history['move'] = action + else: + history['move'] = env._decode_action(action) + + probabilities = [] + for i, a in enumerate(env.actions): + if len(probs) == 0: + p = -2 + elif a in state['raw_legal_actions']: + p = probs[i] + else: + p = -1 + probabilities.append({'move':a, 'probability': p}) + history['probabilities'] = probabilities + data['moveHistory'][0].append(history) + state, player_id = env.step(action, env.agents[player_id].use_raw) + perfect = env.get_perfect_information() + data['publicCard'] = perfect['public_card'] + data = json.dumps(data) + #data = json.dumps(data, indent=2, sort_keys=True) + json_data.append(data) + if env.get_payoffs()[0] > 0: + wins.append(True) + else: + wins.append(False) + payoffs.append(env.get_payoffs()[0]) + return json_data, payoffs, wins + +if __name__=='__main__': + game = 'leduc-holdem' + model_ids = ['leduc-holdem-random', 'leduc-holdem-rule-v1', 'leduc-holdem-cfr'] + t = Tournament(game, model_ids) + games_data = t.launch() + print(len(games_data)) + print(games_data[0]) + #root_path = './models' + #agent1 = LeducHoldemDQNModel1(root_path) + #agent2 = LeducHoldemRandomModel(root_path) + #agent3 = LeducHoldemRuleModel() + #agent4 = LeducHoldemCFRModel(root_path) + #agent5 = LeducHoldemDQNModel2(root_path) + #t = Tournament(agent1, agent2, agent3, agent4, agent5, 'leduc-holdem') + ##t.competition() + #t.evaluate() diff --git a/server/tournament/urls.py b/server/tournament/urls.py new file mode 100644 index 0000000..ebd2841 --- /dev/null +++ b/server/tournament/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path('replay', views.replay, name='replay'), + path('launch', views.launch, name='launch'), + path('query_payoff', views.query_payoff, name='query_payoff'), + path('query_game', views.query_game, name='query_game'), + ] diff --git a/server/tournament/views.py b/server/tournament/views.py new file mode 100644 index 0000000..cb19633 --- /dev/null +++ b/server/tournament/views.py @@ -0,0 +1,60 @@ +from django.shortcuts import render +from django.http import HttpResponse +from django.db import transaction +from django.core import serializers + +from .models import Game, Payoff + +from .rlcard_wrap import rlcard, MODEL_IDS +from .tournament import Tournament + +def replay(request): + if request.method == 'GET': + name = request.GET['name'] + agent0 = request.GET['agent0'] + agent1 = request.GET['agent1'] + index = request.GET['index'] + g = Game.objects.get(name=name, agent0=agent0, agent1=agent1, index=index) + json_data = g.replay + return HttpResponse(json_data) + +def query_game(request): + if request.method == 'GET': + filter_dict = {key: request.GET.get(key) for key in dict(request.GET).keys()} + result = Game.objects.filter(**filter_dict) + result = serializers.serialize('json', result, fields=('name', 'index', 'agent0', 'agent1', 'win', 'payoff')) + return HttpResponse(result) + +def query_payoff(request): + if request.method == 'GET': + filter_dict = {key: request.GET.get(key) for key in dict(request.GET).keys()} + result = Payoff.objects.filter(**filter_dict) + result = serializers.serialize('json', result) + return HttpResponse(result) + + +@transaction.atomic +def launch(request): + if request.method == 'GET': + eval_num = int(request.GET['eval_num']) + game = request.GET['name'] + games_data, payoffs_data = Tournament(game, MODEL_IDS[game], eval_num).launch() + Game.objects.filter(name=game).delete() + Payoff.objects.filter(name=game).delete() + for game_data in games_data: + g = Game(name=game_data['name'], + index=game_data['index'], + agent0=game_data['agent0'], + agent1=game_data['agent1'], + win=game_data['win'], + payoff=game_data['payoff'], + replay=game_data['replay']) + g.save() + for payoff_data in payoffs_data: + p = Payoff(name=payoff_data['name'], + agent0=payoff_data['agent0'], + agent1=payoff_data['agent1'], + payoff=payoff_data['payoff']) + p.save() + return HttpResponse(1) + diff --git a/server/index.js b/server_bak/index.js similarity index 100% rename from server/index.js rename to server_bak/index.js diff --git a/server/package.json b/server_bak/package.json similarity index 100% rename from server/package.json rename to server_bak/package.json diff --git a/server/sample_data/doudizhu_random.json b/server_bak/sample_data/doudizhu_random.json similarity index 100% rename from server/sample_data/doudizhu_random.json rename to server_bak/sample_data/doudizhu_random.json diff --git a/server/sample_data/sample_doudizhu-test.json b/server_bak/sample_data/sample_doudizhu-test.json similarity index 100% rename from server/sample_data/sample_doudizhu-test.json rename to server_bak/sample_data/sample_doudizhu-test.json diff --git a/server/sample_data/sample_doudizhu.json b/server_bak/sample_data/sample_doudizhu.json similarity index 100% rename from server/sample_data/sample_doudizhu.json rename to server_bak/sample_data/sample_doudizhu.json diff --git a/server/sample_data/sample_leduc_holdem-test.json b/server_bak/sample_data/sample_leduc_holdem-test.json similarity index 100% rename from server/sample_data/sample_leduc_holdem-test.json rename to server_bak/sample_data/sample_leduc_holdem-test.json diff --git a/server/sample_data/sample_leduc_holdem.json b/server_bak/sample_data/sample_leduc_holdem.json similarity index 100% rename from server/sample_data/sample_leduc_holdem.json rename to server_bak/sample_data/sample_leduc_holdem.json