Browse Source

Split up application structure, implement route owner tests, fix route owner methods

feature_tests
Burathar 5 years ago
parent
commit
58b354a240
  1. 13
      app/__init__.py
  2. 19
      app/errors.py
  3. 72
      app/forms.py
  4. 2
      app/models.py
  5. 216
      app/routes.py
  6. 10
      app/templates/base.html
  7. 2
      app/templates/errors/403.html
  8. 2
      app/templates/errors/404.html
  9. 2
      app/templates/errors/405.html
  10. 2
      app/templates/errors/500.html
  11. 12
      app/templates/game_dashboard.html
  12. 2
      app/templates/index.html
  13. 13
      app/templates/login.html
  14. 4
      app/templates/objective.html
  15. 2
      app/templates/player.html
  16. 13
      app/templates/register.html
  17. 1
      config.py
  18. 0
      tests/__init__.py
  19. 34
      tests/test_routes.py
  20. 2
      the_hunt.py

13
app/__init__.py

@ -17,7 +17,7 @@ db = SQLAlchemy() @@ -17,7 +17,7 @@ db = SQLAlchemy()
bootstrap = Bootstrap()
migrate = Migrate()
login = LoginManager()
login.login_view = 'login'
login.login_view = 'auth.login'
login.login_message = 'Please log in to access this page.'
moment = Moment()
@ -32,6 +32,15 @@ def create_app(config_class=Config): @@ -32,6 +32,15 @@ def create_app(config_class=Config):
login.init_app(app)
moment.init_app(app)
from app.errors import bp as errors_bp
app.register_blueprint(errors_bp)
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
from app.main import bp as main_bp
app.register_blueprint(main_bp)
if not app.debug and not app.testing:
if app.config['MAIL_SERVER']:
auth = None
@ -69,4 +78,4 @@ def create_app(config_class=Config): @@ -69,4 +78,4 @@ def create_app(config_class=Config):
return app
from app import routes, models, errors
from app import models

19
app/errors.py

@ -1,19 +0,0 @@ @@ -1,19 +0,0 @@
from flask import render_template
from app import app, db
@app.errorhandler(403)
def forbidden_error(error):
return render_template('403.html'), 403
@app.errorhandler(404)
def not_found_error(error):
return render_template('404.html'), 404
@app.errorhandler(405)
def method_not_allowed_error(error):
return render_template('405.html'), 405
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('500.html'), 500

72
app/forms.py

@ -1,72 +0,0 @@ @@ -1,72 +0,0 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, DateTimeField, BooleanField, HiddenField, FloatField, SelectField
from wtforms.validators import DataRequired, EqualTo, ValidationError, Length, NumberRange
from pytz import timezone
from app.models import Player, Objective, Role
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me', default=True)
submit = SubmitField('Sign In')
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=0, max=64)])
password = PasswordField('Password', validators=[DataRequired(), Length(min=0, max=128)])
password2 = PasswordField(
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')
def validate_username(self, username):
player = Player.query.filter_by(name=username.data).first()
if player is not None:
raise ValidationError('Please use a different username.')
class CreateGameForm(FlaskForm):
game_name = StringField('Game Name', validators=[DataRequired(), Length(min=0, max=64)])
start_time_disabled = BooleanField('No start time')
start_time = DateTimeField(id='datetimepicker_start', format="%d-%m-%Y %H:%M")
end_time_disabled = BooleanField('No end time')
end_time = DateTimeField(id='datetimepicker_end', format="%d-%m-%Y %H:%M")
timezone = HiddenField(validators=[DataRequired()])
submit = SubmitField('Create')
def validate_start_time(self, start_time):
self.date_time_validator(self.start_time_disabled, start_time)
def validate_end_time(self, end_time):
self.date_time_validator(self.end_time_disabled, end_time)
def date_time_validator(self, disabled, date_time):
if disabled.data:
date_time.data = None
return
clientzone = timezone(self.timezone.data)
date_time_utc = clientzone.localize(date_time.data).astimezone(timezone('UTC'))
date_time.data = date_time_utc
class ObjectiveForm(FlaskForm):
objective_name = StringField('Objective Name', validators=[Length(min=0, max=64)])
latitude = FloatField('Latitude', validators=[DataRequired(), NumberRange(min=-90, max=90)])
longitude = FloatField('Longitude', validators=[DataRequired(), NumberRange(min=-180, max=180)])
submit = SubmitField('Save')
def validate_objective_name(self, objective_name):
if objective_name.data == '': return
objective = Objective.query.filter_by(name=objective_name.data).first()
if objective is not None:
raise ValidationError('Please use a different name.')
class PlayerUpdateForm(FlaskForm):
role = SelectField('Player Role', choices=[('none', 'none'), ('owner', 'owner'), ('hunter', 'hunter'), ('bunny', 'bunny')], validators=[DataRequired()])
submit = SubmitField('Update')
class PlayerAddForm(FlaskForm):
name = StringField('Player Name', validators=[DataRequired(), Length(min=0, max=64)])
role = SelectField('Player Role', choices=[('none', 'none'), ('owner', 'owner'), ('hunter', 'hunter'), ('bunny', 'bunny')], validators=[DataRequired()])
submit_add = SubmitField('Create')
class PlayerCreateForm(FlaskForm):
name = StringField('Player Name', validators=[DataRequired(), Length(min=0, max=64)])
role = SelectField('Player Role', choices=[('none', 'none'), ('owner', 'owner'), ('hunter', 'hunter'), ('bunny', 'bunny')], validators=[DataRequired()])
submit_create = SubmitField('Create')

2
app/models.py

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
from enum import Enum
from werkzeug.security import generate_password_hash, check_password_hash
from app import app, db, login
from . import db, login
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.sql import func
from secrets import token_hex

216
app/routes.py

@ -1,216 +0,0 @@ @@ -1,216 +0,0 @@
import json
import qrcode
from flask import render_template, flash, redirect, url_for, request, abort, send_file
from flask_login import login_user, logout_user, current_user, login_required
from sqlalchemy import and_
from io import BytesIO
from app import app, db
from app.models import Player, Game, Role, GamePlayer, Objective, ObjectiveMinimalEncoder, LocationEncoder
from app.forms import LoginForm, RegistrationForm, CreateGameForm, ObjectiveForm, PlayerAddForm, PlayerCreateForm, PlayerUpdateForm
@app.route('/')
@app.route('/index')
@login_required
def index():
return render_template("index.html", title='Home')
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
player = Player.query.filter_by(name=form.username.data).first()
if player is None or not player.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(player, remember=form.remember_me.data)
return redirect(url_for('index'))
return render_template('login.html', title='Sign In', form=form)
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('index'))
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = RegistrationForm()
if form.validate_on_submit():
player = Player(name=form.username.data)
player.set_password(form.password.data)
player.set_auth_hash()
db.session.add(player)
db.session.commit()
flash('Congratulations, you are now a registered user!')
return redirect(url_for('login'))
return render_template('register.html', title='Register', form=form)
@app.route('/create_game', methods=['GET', 'POST'])
@login_required
def create_game():
print(current_user.is_authenticated)
form = CreateGameForm()
if form.validate_on_submit():
game = Game(name=form.game_name.data, start_time=form.start_time.data, end_time=form.end_time.data)
game.game_players.append(GamePlayer(player=current_user, role=Role['owner'])) #check if this works, otherwise use 'owner'
db.session.add(game)
db.session.commit()
flash(f"'{game.name}' had been created!")
return redirect(url_for('game_dashboard', game_name=game.name))
return render_template('create_game.html', title='Create Game', form=form)
@app.route('/game/<game_name>/dashboard')
@login_required
def game_dashboard(game_name):
#game = Game.query.filter(Game.game_players.any(and_(GamePlayer.player.has(Player.name == current_user.name), GamePlayer.role == 'owner'))).first_or_404()
game = Game.query.filter_by(name=game_name).first_or_404()
if not is_game_owner(game):
abort(403)
return render_template('game_dashboard.html', title = 'Game Dashboard', game=game, json=json, objective_encoder=ObjectiveMinimalEncoder, location_encoder=LocationEncoder)
@app.route('/game/<game_name>/addplayer', methods=['GET', 'POST'])
@login_required
def add_player(game_name):
game = Game.query.filter_by(name=game_name).first_or_404()
if not is_game_owner(game):
abort(403)
form_add = PlayerAddForm()
form_create = PlayerCreateForm()
if form_add.submit_add.data and form_add.validate_on_submit():
player = Player.query.filter_by(form_add.name.data).first_or_404()
game.game_players.append(GamePlayer(player=player, role=Role[form_create.role.data]))
return redirect(url_for('game_dashboard', game_name=game.name))
if form_create.submit_create.data and form_create.validate_on_submit():
player = Player(name=form_create.name.data)
player.set_auth_hash()
game.game_players.append(GamePlayer(player=player, role=Role[form_create.role.data]))
db.session.commit()
return redirect(url_for('game_dashboard', game_name=game.name))
return render_template('add_player.html', title=f'Add Player for {game_name}', form_add=form_add, form_create=form_create, game=game)
@app.route('/game/<game_name>/removeplayer/<player_name>')
@login_required
def remove_player(game_name, player_name):
game = Game.query.filter_by(name=game_name).first_or_404()
if not is_game_owner(game):
abort(403)
player = Player.query.filter(and_(Player.name == player_name, Player.games.contains(game))).first_or_404()
game.players.remove(player)
db.session.commit()
return redirect(url_for('game_dashboard', game_name=game.name))
@app.route('/game/<game_name>/player/<player_name>', methods=['GET', 'POST'])
@login_required
def game_player(game_name, player_name):
game = Game.query.filter_by(name=game_name).first_or_404()
if not is_game_owner(game):
abort(403)
player = Player.query.filter(and_(Player.name == player_name, Player.games.contains(game))).first_or_404()
gameplayer = [gameplayer for gameplayer in player.player_games if gameplayer.game == game][0]
form = PlayerUpdateForm(role=gameplayer.role.name)
if form.validate_on_submit():
gameplayer.role = Role[form.role.data]
db.session.commit()
return redirect(url_for('game_dashboard', game_name=game.name))
return render_template('player.html', title=f'{player.name} in {game_name}', game=game, player=player, form=form, json=json, location_encoder=LocationEncoder)
@app.route('/player/<auth_hash>/qrcode.png')
@login_required
def player_qrcode(auth_hash):
player = Player.query.filter_by(auth_hash=auth_hash).first_or_404()
#if not is_player_game_owner(player):
# abort(403)
img = generate_qr_code(url_for('player', auth_hash=auth_hash, _external=True))
return serve_pil_image(img)
@app.route('/player/<auth_hash>')
@login_required
def player(auth_hash):
player = Player.query.filter_by(auth_hash=auth_hash).first_or_404()
return render_template('player.html',title=f'Player: {player.name}', player=player)
def is_game_owner(game):
return current_user in [gameplayer.player for gameplayer in game.game_players if gameplayer.role == Role.owner]
def is_player_game_owner(player):
return current_user in [gameplayer.player for gameplayer in [game for game in player.games].game_players if game_player.role == Role.owner]
def is_objective_owner(objective):
return current_user in [gameplayer.player for gameplayer in objective.game.game_players if gameplayer.role == Role.owner]
@app.route('/game/<game_name>/add_objective', methods=['GET', 'POST'])
@login_required
def add_objective(game_name):
game = Game.query.filter_by(name=game_name).first_or_404()
if not is_game_owner(game):
abort(403)
form = ObjectiveForm()
objective = Objective(name='', latitude=52.0932, longitude=5.12405)
if form.validate_on_submit():
objective = Objective(name=form.objective_name.data, longitude=form.longitude.data, latitude=form.latitude.data)
objective.set_hash()
game.objectives.append(objective)
db.session.commit()
flash(f"Objective has been added!")
return redirect(url_for('game_dashboard', game_name=game.name))
return render_template('objective.html', title=f'Add Objective for {game_name}', form=form, objective=objective, owner=True)
@app.route('/objective/<objective_hash>/delete', methods=['GET'])
@login_required
def delete_objective(objective_hash):
objective = Objective.query.filter_by(hash=objective_hash).first_or_404()
if not is_objective_owner(objective):
abort(403)
if is_objective_owner(objective):
db.session.delete(objective)
db.session.commit()
return redirect(url_for('game_dashboard', game_name=objective.game.name))
def generate_qr_code(url):
qr = qrcode.QRCode(
version=None,
error_correction=qrcode.constants.ERROR_CORRECT_M,
box_size=30,
border=4,
)
qr.add_data(url)
qr.make(fit=True)
return qr.make_image(fill_color='black', back_color='white')
# Source: https://stackoverflow.com/questions/7877282/how-to-send-image-generated-by-pil-to-browser
def serve_pil_image(pil_img):
img_io = BytesIO()
pil_img.save(img_io, 'PNG', quality=70)
img_io.seek(0)
return send_file(img_io, mimetype='image/png')
@app.route('/objective/<objective_hash>/qrcode.png')
@login_required
def objective_qrcode(objective_hash):
objective = Objective.query.filter_by(hash=objective_hash).first_or_404()
if not is_objective_owner(objective):
abort(403)
img = generate_qr_code(url_for('objective', objective_hash=objective.hash, _external=True))
return serve_pil_image(img)
@app.route('/objective/<objective_hash>', methods=['GET', 'POST'])
@login_required
def objective(objective_hash):
objective = Objective.query.filter_by(hash=objective_hash).first_or_404()
owner = is_objective_owner(objective)
qrcode = generate_qr_code(objective) if owner else None
form = ObjectiveForm()
if form.submit.data and form.validate() and owner:
objective.name = form.objective_name.data
objective.longitude = form.longitude.data
objective.latitude = form.latitude.data
db.session.commit()
return redirect(url_for('game_dashboard', game_name=objective.game.name))
return render_template('objective.html', title='Objective view', objective=objective, owner=owner, form=form, qrcode=qrcode)

10
app/templates/base.html

@ -14,20 +14,20 @@ @@ -14,20 +14,20 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ url_for('index') }}">The Hunt</a>
<a class="navbar-brand" href="{{ url_for('main.index') }}">The Hunt</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li><a href="{{ url_for('index') }}">Home</a></li>
<li><a href="{{ url_for('main.index') }}">Home</a></li>
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('create_game') }}">Create Game</a></li>
<li><a href="{{ url_for('main.create_game') }}">Create Game</a></li>
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_anonymous %}
<li><a href="{{ url_for('login') }}">Login</a></li>
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
{% else %}
<li><a href="{{ url_for('logout') }}">Logout</a></li>
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
{% endif %}
</ul>
</div>

2
app/templates/403.html → app/templates/errors/403.html

@ -3,5 +3,5 @@ @@ -3,5 +3,5 @@
{% block app_content %}
<h1>Forbidden</h1>
<h2>You don't have permission to do that</h2>
<p><a href="{{ url_for('index') }}">Back</a></p>
<p><a href="{{ url_for('main.index') }}">Back</a></p>
{% endblock %}

2
app/templates/404.html → app/templates/errors/404.html

@ -2,5 +2,5 @@ @@ -2,5 +2,5 @@
{% block app_content %}
<h1>File Not Found</h1>
<p><a href="{{ url_for('index') }}">Back</a></p>
<p><a href="{{ url_for('main.index') }}">Back</a></p>
{% endblock %}

2
app/templates/405.html → app/templates/errors/405.html

@ -3,5 +3,5 @@ @@ -3,5 +3,5 @@
{% block app_content %}
<h1>Method Not Allowed</h1>
<h2>The method is not allowed for the requested URL.</h2>
<p><a href="{{ url_for('index') }}">Back</a></p>
<p><a href="{{ url_for('main.index') }}">Back</a></p>
{% endblock %}

2
app/templates/500.html → app/templates/errors/500.html

@ -3,5 +3,5 @@ @@ -3,5 +3,5 @@
{% block app_content %}
<h1>An unexpected error has occurred</h1>
<p>The administrator has been notified. Sorry for the inconvenience!</p>
<p><a href="{{ url_for('index') }}">Back</a></p>
<p><a href="{{ url_for('main.index') }}">Back</a></p>
{% endblock %}

12
app/templates/game_dashboard.html

@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
<h1>{{ game.name }} Dashboard</h1>
<h2>Players:</h2>
<p><a href="{{ url_for('add_player', game_name = game.name) }}">Add player</a></p>
<p><a href="{{ url_for('main.add_player', game_name = game.name) }}">Add player</a></p>
<div class="table-responsive">
<table class="table">
<thead>
@ -27,7 +27,7 @@ @@ -27,7 +27,7 @@
<tbody>
{% for player in game.players %}
<tr>
<td><a href="{{ url_for('game_player', game_name = game.name, player_name = player.name) }}">{{ player.name }}</a></td>
<td><a href="{{ url_for('main.game_player', game_name = game.name, player_name = player.name) }}">{{ player.name }}</a></td>
{% for gameplayer in player.player_games if gameplayer.game == game %}
<td>{{ gameplayer.role.name }}</td>
{% endfor %}
@ -38,7 +38,7 @@ @@ -38,7 +38,7 @@
{% if location %}{{ moment(location.timestamp).fromNow()}}: {% endif %}
{{ location }}
{% endwith %}</td>
<td><a href="{{ url_for('remove_player', game_name=game.name, player_name=player.name) }}">
<td><a href="{{ url_for('main.remove_player', game_name=game.name, player_name=player.name) }}">
<button class="btn btn-danger">Delete</button></a>
</td>
</tr>
@ -47,7 +47,7 @@ @@ -47,7 +47,7 @@
</table>
</div>
<h2>Objectives:</h2>
<p><a href="{{ url_for('add_objective', game_name = game.name) }}">Add new objective</a></p>
<p><a href="{{ url_for('main.add_objective', game_name = game.name) }}">Add new objective</a></p>
{% if game.objectives %}
<div class="table-responsive">
<table class="table">
@ -68,8 +68,8 @@ @@ -68,8 +68,8 @@
<td>{{ objective.latitude }}</td>
<td>{{ objective.longitude }}</td>
<td>{{ objective.found_by|length }}</td>
<td><a href="{{ url_for('objective', objective_hash = objective.hash) }}">{{ objective.hash }}</a></td>
<td><a href="{{ url_for('delete_objective', objective_hash = objective.hash) }}">
<td><a href="{{ url_for('main.objective', objective_hash = objective.hash) }}">{{ objective.hash }}</a></td>
<td><a href="{{ url_for('main.delete_objective', objective_hash = objective.hash) }}">
<button class="btn btn-danger">Delete</button></a>
</td>
</tr>

2
app/templates/index.html

@ -19,7 +19,7 @@ @@ -19,7 +19,7 @@
<tbody>
{% for game in current_user.games %}
<tr>
<td><a href="{{ url_for('game_dashboard', game_name = game.name) }}">{{ game.name }}</a></td>
<td><a href="{{ url_for('main.game_dashboard', game_name = game.name) }}">{{ game.name }}</a></td>
<td>{{ game.state.name}}</td>
<td>{{ game.start_time }}</td>
<td>{{ game.end_time }}</td>

13
app/templates/login.html

@ -1,13 +0,0 @@ @@ -1,13 +0,0 @@
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Sign In</h1>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form, button_map={'submit': 'primary'}) }}
</div>
</div>
<br>
<p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>
{% endblock %}

4
app/templates/objective.html

@ -34,13 +34,13 @@ @@ -34,13 +34,13 @@
{{ wtf.form_field(form.submit, class='btn btn-primary') }}
</form>
{% if objective.hash %}
<a href="{{ url_for('delete_objective', objective_hash = objective.hash) }}">
<a href="{{ url_for('main.delete_objective', objective_hash = objective.hash) }}">
<button class="btn btn-danger">Delete</button></a>
{% endif %}
</div>
{% if objective.hash %}
<div class="row">
<img src="{{ url_for('objective_qrcode', objective_hash=objective.hash) }}" alt="qr_code_failed", width="100%">
<img src="{{ url_for('main.objective_qrcode', objective_hash=objective.hash) }}" alt="qr_code_failed", width="100%">
</div>
{% endif %}
{% else %}

2
app/templates/player.html

@ -23,7 +23,7 @@ @@ -23,7 +23,7 @@
</div>
{% if player.auth_hash %}
<div class="row">
<img src="{{ url_for('player_qrcode', auth_hash=player.auth_hash) }}" alt="qr_code_failed", width="100%">
<img src="{{ url_for('main.player_qrcode', auth_hash=player.auth_hash) }}" alt="qr_code_failed", width="100%">
</div>
{% endif %}
</div>

13
app/templates/register.html

@ -1,13 +0,0 @@ @@ -1,13 +0,0 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Register</h1>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form, button_map={'submit': 'primary'}) }}
</div>
</div>
<br>
<p>Already have an account? <a href="{{ url_for('login') }}">Click to Sign In!</a></p>
{% endblock %}

1
config.py

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import os
from dotenv import load_dotenv
from pathlib import Path
basedir = Path(__file__).parent.absolute()
load_dotenv(basedir / '.env')

0
tests/__init__.py

34
tests/test_routes.py

@ -1,34 +0,0 @@ @@ -1,34 +0,0 @@
import unittest
from app import create_app, db
from app.models import Player, Game, Role, GamePlayer, Objective, ObjectiveMinimalEncoder, LocationEncoder
from config import Config
class TestConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite://'
class RoutesCase(unittest.TestCase):
def setUp(self):
self.app = create_app(TestConfig)
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_is_game_owner(self):
g = Game(name='TestGame')
p1 = Player(name='Henk')
p2 = Player(name='Alfred')
g.players.append(p1)
g.players.append(p2)
db.session.append(g)
db.session.commit()
self.assertEqual(True, True)
self.assertFalse('Foo'.isupper())
if __name__ == '__main__':
unittest.main(verbosity=2)

2
the_hunt.py

@ -9,4 +9,4 @@ def make_shell_context(): @@ -9,4 +9,4 @@ def make_shell_context():
return {'db': db, 'Game' : Game, 'Player' : Player, 'Objective' : Objective,
'Location' : Location, 'Notification' : Notification, 'GamePlayer' : GamePlayer,
'PlayerFoundObjective' : PlayerFoundObjective, 'NotificationPlayer' : NotificationPlayer,
'PlayerCaughtPlayer' : PlayerCaughtPlayer, 'Role' : Role, 'GameState' : GameState}
'PlayerCaughtPlayer' : PlayerCaughtPlayer, 'Role' : Role, 'GameState' : GameState}
Loading…
Cancel
Save