├── players
├── __init__.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ ├── delete_generated_players.py
│ │ └── generate_players.py
├── migrations
│ ├── __init__.py
│ ├── 0006_game_static_data.py
│ ├── 0002_auto_20160601_1914.py
│ ├── 0001_initial.py
│ ├── 0004_auto_20160808_1511.py
│ ├── 0005_auto_20160808_1545.py
│ └── 0003_auto_20160802_1418.py
├── templatetags
│ ├── __init__.py
│ └── players_utils.py
├── avatar_examples
│ ├── __init__.py
│ ├── dumb_avatar.py
│ ├── attacking_avatar.py
│ ├── winner_avatar.py
│ └── health_seeker_avatar.py
├── static
│ ├── css
│ │ ├── program.css
│ │ └── watch.css
│ └── js
│ │ ├── program.js
│ │ └── watch
│ │ ├── world-controls.js
│ │ └── world-viewer.js
├── admin.py
├── templates
│ ├── players
│ │ ├── statistics.html
│ │ ├── add_game.html
│ │ ├── home.html
│ │ ├── dropdown.html
│ │ ├── program.html
│ │ ├── watch.html
│ │ └── base.html
│ └── registration
│ │ └── login.html
├── app_settings.py
├── forms.py
├── urls.py
├── autoconfig.py
├── models.py
└── views.py
├── aimmo-game
├── tests
│ ├── __init__.py
│ ├── test_simulation
│ │ ├── avatar
│ │ │ ├── __init__.py
│ │ │ └── test_avatar_wrapper.py
│ │ ├── __init__.py
│ │ ├── test_direction.py
│ │ ├── test_location.py
│ │ ├── test_effects.py
│ │ ├── test_pickups.py
│ │ ├── dummy_avatar.py
│ │ ├── maps.py
│ │ ├── test_worker_manager.py
│ │ ├── test_game_state.py
│ │ ├── test_action.py
│ │ ├── test_map_generator.py
│ │ └── test_turn_manager.py
│ └── test_service.py
├── simulation
│ ├── __init__.py
│ ├── avatar
│ │ ├── __init__.py
│ │ ├── avatar_appearance.py
│ │ ├── fog_of_war.py
│ │ ├── avatar_manager.py
│ │ └── avatar_wrapper.py
│ ├── event.py
│ ├── direction.py
│ ├── location.py
│ ├── effects.py
│ ├── game_state.py
│ ├── pickups.py
│ ├── world_state.py
│ ├── turn_manager.py
│ ├── action.py
│ ├── map_generator.py
│ └── worker_manager.py
├── requirements.txt
├── Dockerfile
├── setup.py
└── service.py
├── aimmo-game-creator
├── tests
│ ├── __init__.py
│ └── test_worker_manager.py
├── requirements.txt
├── Dockerfile
├── setup.py
├── update.sh
└── service.py
├── aimmo-game-worker
├── tests
│ ├── __init__.py
│ ├── simulation
│ │ ├── __init__.py
│ │ └── test_world_map.py
│ └── test_initialise.py
├── simulation
│ ├── __init__.py
│ ├── avatar_state.py
│ ├── direction.py
│ ├── event.py
│ ├── location.py
│ ├── action.py
│ └── world_map.py
├── requirements.txt
├── Dockerfile
├── run.sh
├── setup.py
├── test-initialise.py
├── initialise.py
├── test-turn.py
└── service.py
├── minikube_requirements.txt
├── MANIFEST.in
├── example_project
├── example_project
│ ├── __init__.py
│ ├── wsgi.py
│ └── settings.py
└── manage.py
├── .gitattributes
├── aimmo-reverse-proxy
├── Dockerfile
└── nginx.conf
├── manifests-template
├── ingress.yaml
├── service-aimmo-reverse-proxy.yaml
├── deploy-aimmo-reverse-proxy.yaml
└── rc-aimmo-game-creator.yaml
├── run
├── test_settings.py
├── setup.cfg
├── render-manifests.py
├── setup.py
├── .gitignore
├── all_tests.py
├── .travis.yml
├── CONTRIBUTING.md
├── run.py
├── minikube.py
└── README.md
/players/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aimmo-game/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/players/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/players/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/players/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aimmo-game-creator/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aimmo-game-worker/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aimmo-game-worker/simulation/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/avatar/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/players/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aimmo-game-worker/tests/simulation/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_simulation/avatar/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/aimmo-game-worker/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | pykube
3 |
--------------------------------------------------------------------------------
/aimmo-game-creator/requirements.txt:
--------------------------------------------------------------------------------
1 | pykube
2 | eventlet
3 |
--------------------------------------------------------------------------------
/minikube_requirements.txt:
--------------------------------------------------------------------------------
1 | docker-py >= 1.10
2 | kubernetes
3 |
--------------------------------------------------------------------------------
/players/avatar_examples/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'Dan'
2 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | graft players/static
2 | graft players/templates
3 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_simulation/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'c.brett'
2 |
--------------------------------------------------------------------------------
/example_project/example_project/__init__.py:
--------------------------------------------------------------------------------
1 | '''example_project __init__'''
2 |
--------------------------------------------------------------------------------
/players/static/css/program.css:
--------------------------------------------------------------------------------
1 | #editor {
2 | min-height: 500px;
3 | }
4 |
--------------------------------------------------------------------------------
/aimmo-game/requirements.txt:
--------------------------------------------------------------------------------
1 | eventlet
2 | flask
3 | flask-socketio
4 | pykube
5 | requests
6 | six
7 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Force Unix line endings, which is necessary for shell scripts executed by docker worker
2 | *.sh text eol=lf
--------------------------------------------------------------------------------
/aimmo-game-creator/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3-onbuild
2 |
3 | MAINTAINER code@ocado.com
4 |
5 | CMD ["python", "./service.py"]
6 |
--------------------------------------------------------------------------------
/aimmo-game-worker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:2-onbuild
2 |
3 | MAINTAINER code@ocado.com
4 |
5 | CMD ["bash", "./run.sh", "0.0.0.0", "5000"]
6 |
--------------------------------------------------------------------------------
/aimmo-reverse-proxy/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx
2 | RUN mkdir -p /etc/nginx/html/ && touch /etc/nginx/html/index.html
3 | COPY nginx.conf /etc/nginx/nginx.conf
4 |
--------------------------------------------------------------------------------
/players/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from models import Avatar, Game
4 |
5 | admin.site.register(Avatar)
6 | admin.site.register(Game)
7 |
--------------------------------------------------------------------------------
/aimmo-game/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:2-onbuild
2 |
3 | MAINTAINER code@ocado.com
4 |
5 | ENV WORKER_MANAGER=kubernetes
6 |
7 | CMD ["python", "./service.py", "0.0.0.0", "5000"]
8 |
--------------------------------------------------------------------------------
/players/static/css/watch.css:
--------------------------------------------------------------------------------
1 | #watch-world-canvas {
2 | display: block;
3 | /*height: 95%;*/
4 | height: 800px;
5 |
6 | margin: 0px;
7 | padding: 0px;
8 | box-sizing: border-box;
9 | }
--------------------------------------------------------------------------------
/players/templates/players/statistics.html:
--------------------------------------------------------------------------------
1 | {% extends 'players/base.html' %}
2 |
3 | {% block nav-statistics-class %}active{% endblock %}
4 |
5 |
6 | {% block content %}
7 | stats
8 | {% endblock %}
9 |
--------------------------------------------------------------------------------
/manifests-template/ingress.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: extensions/v1beta1
2 | kind: Ingress
3 | metadata:
4 | name: aimmo-ingress
5 | spec:
6 | backend:
7 | serviceName: aimmo-reverse-proxy
8 | servicePort: 80
9 |
--------------------------------------------------------------------------------
/aimmo-game-worker/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #TODO: no longer needed?
4 |
5 | set -e
6 |
7 | dir=$(mktemp -d)
8 |
9 | python ./initialise.py $dir
10 |
11 | export PYTHONPATH=$dir:$PYTHONPATH
12 |
13 | exec python ./service.py $1 $2 $dir
14 |
--------------------------------------------------------------------------------
/players/app_settings.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 | #: URL function for locating the game server, takes one parameter `game`
4 | GAME_SERVER_LOCATION_FUNCTION = getattr(settings, 'AIMMO_GAME_SERVER_LOCATION_FUNCTION', None)
5 |
6 | MAX_LEVEL = 1
7 |
--------------------------------------------------------------------------------
/manifests-template/service-aimmo-reverse-proxy.yaml:
--------------------------------------------------------------------------------
1 | kind: Service
2 | apiVersion: v1
3 | metadata:
4 | name: aimmo-reverse-proxy
5 | spec:
6 | selector:
7 | app: aimmo-reverse-proxy
8 | ports:
9 | - protocol: TCP
10 | port: 80
11 | type: NodePort
12 |
--------------------------------------------------------------------------------
/players/forms.py:
--------------------------------------------------------------------------------
1 | from django.forms import ModelForm
2 |
3 | from players.models import Game
4 |
5 |
6 | class AddGameForm(ModelForm):
7 | class Meta:
8 | model = Game
9 | exclude = ['Main', 'owner', 'auth_token', 'completed', 'main_user', 'static_data']
10 |
--------------------------------------------------------------------------------
/players/avatar_examples/dumb_avatar.py:
--------------------------------------------------------------------------------
1 | class Avatar(object):
2 | def handle_turn(self, world_view, events):
3 | from simulation.action import MoveAction
4 | from simulation.direction import ALL_DIRECTIONS
5 | import random
6 |
7 | return MoveAction(random.choice(ALL_DIRECTIONS))
8 |
--------------------------------------------------------------------------------
/aimmo-game-worker/simulation/avatar_state.py:
--------------------------------------------------------------------------------
1 | from simulation.location import Location
2 |
3 |
4 | class AvatarState(object):
5 |
6 | def __init__(self, location, health, score, events):
7 | self.location = Location(**location)
8 | self.health = health
9 | self.score = score
10 | self.events = events
11 |
--------------------------------------------------------------------------------
/run:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | cd "${BASH_SOURCE%/*}"
4 |
5 | trap 'kill -- -$$' INT TERM
6 |
7 | pip install -e .
8 | ./example_project/manage.py migrate --noinput
9 | ./example_project/manage.py collectstatic --noinput
10 | ./example_project/manage.py runserver "$@" &
11 | sleep 2
12 | ./aimmo-game-creator/service.py &
13 |
14 | wait
15 |
--------------------------------------------------------------------------------
/example_project/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | import logging
11 | logging.basicConfig()
12 |
13 | execute_from_command_line(sys.argv)
14 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/avatar/avatar_appearance.py:
--------------------------------------------------------------------------------
1 | class AvatarAppearance:
2 | """
3 | The presentation aspect of an avatar.
4 | """
5 |
6 | def __init__(self, body_stroke, body_fill, eye_stroke, eye_fill):
7 | self.body_stroke = body_stroke
8 | self.body_fill = body_fill
9 | self.eye_stroke = eye_stroke
10 | self.eye_fill = eye_fill
11 |
--------------------------------------------------------------------------------
/players/templates/players/add_game.html:
--------------------------------------------------------------------------------
1 | {% extends 'players/base.html' %}
2 | {% load bootstrap_tags %}
3 |
4 | {% block content %}
5 |
Add a new game
6 |
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/aimmo-reverse-proxy/nginx.conf:
--------------------------------------------------------------------------------
1 | http {
2 | resolver 10.0.0.10;
3 | server {
4 | listen 80;
5 | location ~^/game/([0-9]+)/? {
6 | proxy_pass "http://game-$1.default.svc.cluster.local";
7 | proxy_http_version 1.1;
8 | proxy_set_header Upgrade $http_upgrade;
9 | proxy_set_header Connection "Upgrade";
10 | }
11 | }
12 | }
13 |
14 | events {
15 | }
16 |
--------------------------------------------------------------------------------
/aimmo-game-creator/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from setuptools import find_packages, setup
3 |
4 |
5 | setup(
6 | name='aimmo-game-creator',
7 | packages=find_packages(),
8 | include_package_data=True,
9 | install_requires=[
10 | 'eventlet',
11 | 'pykube',
12 | ],
13 | tests_require=[
14 | 'httmock',
15 | ],
16 | test_suite='tests',
17 | zip_safe=False,
18 | )
19 |
--------------------------------------------------------------------------------
/aimmo-game-creator/update.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 | kubectl delete rc aimmo-game-creator || true
4 | sleep 10
5 | kubectl delete rc -l app=aimmo-game
6 | sleep 10
7 | kubectl delete pod -l app=aimmo-game-worker
8 | kubectl delete service -l app=aimmo-game
9 | sleep 5
10 | kubectl create -f rc-aimmo-game-creator.yaml
11 | sleep 10
12 | kubectl get rc
13 | kubectl get pod
14 | kubectl get service
15 | kubectl get ingress
16 |
--------------------------------------------------------------------------------
/aimmo-game-worker/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from setuptools import find_packages, setup
3 |
4 |
5 | setup(
6 | name='aimmo-game-worker',
7 | packages=find_packages(),
8 | include_package_data=True,
9 | install_requires=[
10 | 'flask',
11 | 'requests',
12 | ],
13 | tests_require=[
14 | 'httmock',
15 | ],
16 | test_suite='tests',
17 | zip_safe=False,
18 | )
19 |
--------------------------------------------------------------------------------
/test_settings.py:
--------------------------------------------------------------------------------
1 | DATABASES = {
2 | 'default': {
3 | 'ENGINE': 'django.db.backends.sqlite3',
4 | },
5 | }
6 | INSTALLED_APPS = [
7 | 'django.contrib.admin',
8 | 'players',
9 | ]
10 | PIPELINE_ENABLED = False
11 | ROOT_URLCONF = 'django_autoconfig.autourlconf'
12 | STATIC_ROOT = '.tests_static/'
13 |
14 | from django_autoconfig.autoconfig import configure_settings # noqa: E402
15 | configure_settings(globals())
16 |
--------------------------------------------------------------------------------
/manifests-template/deploy-aimmo-reverse-proxy.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: extensions/v1beta1
2 | kind: Deployment
3 | metadata:
4 | name: aimmo-reverse-proxy
5 | spec:
6 | replicas: 1
7 | template:
8 | metadata:
9 | labels:
10 | app: aimmo-reverse-proxy
11 | spec:
12 | containers:
13 | - name: aimmo-reverse-proxy
14 | image: ocadotechnology/aimmo-reverse-proxy:AIMMO_VERSION
15 | ports:
16 | - containerPort: 80
17 |
--------------------------------------------------------------------------------
/players/templates/players/home.html:
--------------------------------------------------------------------------------
1 | {% extends 'players/base.html' %}
2 |
3 | {% block content %}
4 | Welcome to AI:MMO!
5 | AI:MMO is a Massively Multi-player Online game, where players
6 | create Artificially Intelligent programs to play on their behalf.
7 | Get started by trying level 1.
8 | {% endblock %}
9 |
--------------------------------------------------------------------------------
/aimmo-game/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from setuptools import find_packages, setup
3 |
4 |
5 | setup(
6 | name='aimmo-game',
7 | packages=find_packages(),
8 | include_package_data=True,
9 | install_requires=[
10 | 'eventlet',
11 | 'flask',
12 | 'flask-socketio',
13 | 'requests',
14 | 'six',
15 | 'pykube',
16 | ],
17 | tests_require=[
18 | 'httmock',
19 | ],
20 | test_suite='tests',
21 | zip_safe=False,
22 | )
23 |
--------------------------------------------------------------------------------
/players/migrations/0006_game_static_data.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('players', '0005_auto_20160808_1545'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='game',
16 | name='static_data',
17 | field=models.TextField(null=True, blank=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/players/management/commands/delete_generated_players.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from django.core.management import BaseCommand
3 |
4 |
5 | class Command(BaseCommand):
6 | # Show this when the user types help
7 | help = "Delete generated users"
8 |
9 | # A command must define handle()
10 | def handle(self, *args, **options):
11 | for user in User.objects.filter(username__startswith='zombie-'):
12 | self.stdout.write('Deleting %s' % user.get_username())
13 | user.delete()
14 |
--------------------------------------------------------------------------------
/players/migrations/0002_auto_20160601_1914.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('players', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.RemoveField(
15 | model_name='avatar',
16 | name='player',
17 | ),
18 | migrations.DeleteModel(
19 | name='Avatar',
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/players/templatetags/players_utils.py:
--------------------------------------------------------------------------------
1 | from django import template
2 |
3 | from players import app_settings
4 | from players.models import Game
5 |
6 | register = template.Library()
7 |
8 |
9 | @register.inclusion_tag('players/dropdown.html', takes_context=True)
10 | def game_dropdown_list(context, base_url):
11 | return {
12 | 'base_url': base_url,
13 | 'open_play_games': Game.objects.for_user(context.request.user).filter(levelattempt=None),
14 | 'level_numbers': xrange(1, app_settings.MAX_LEVEL+1),
15 | }
16 |
--------------------------------------------------------------------------------
/aimmo-game-creator/service.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import logging
3 | import os
4 |
5 | from worker_manager import WORKER_MANAGERS
6 |
7 |
8 | def main():
9 | logging.basicConfig(level=logging.DEBUG)
10 | WorkerManagerClass = WORKER_MANAGERS[os.environ.get('WORKER_MANAGER', 'local')]
11 | worker_manager = WorkerManagerClass(os.environ.get('GAME_API_URL',
12 | 'http://localhost:8000/players/api/games/'))
13 | worker_manager.run()
14 |
15 | if __name__ == '__main__':
16 | main()
17 |
--------------------------------------------------------------------------------
/aimmo-game-worker/simulation/direction.py:
--------------------------------------------------------------------------------
1 | class Direction(object):
2 |
3 | def __init__(self, x, y):
4 | self.x = x
5 | self.y = y
6 |
7 | def __repr__(self):
8 | return 'Direction(x={}, y={})'.format(self.x, self.y)
9 |
10 | def serialise(self):
11 | return {
12 | 'x': self.x,
13 | 'y': self.y,
14 | }
15 |
16 | NORTH = Direction(0, 1)
17 | EAST = Direction(1, 0)
18 | SOUTH = Direction(0, -1)
19 | WEST = Direction(-1, 0)
20 |
21 | ALL_DIRECTIONS = (NORTH, EAST, SOUTH, WEST)
22 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal = 1
3 |
4 | [coverage:run]
5 | source = players, aimmo-game, aimmo-game-worker, aimmo-game-creator
6 |
7 | [coverage:report]
8 | omit =
9 | players/_version.py
10 | *test-*
11 | */tests/*
12 | */python?.?/*
13 | */site-packages/nose/*
14 | *.egg/*
15 |
16 | [pep8]
17 | max-line-length = 160
18 |
19 | [versioneer]
20 | VCS = git
21 | style = pep440-pre
22 | versionfile_source = players/_version.py
23 | versionfile_build = players/_version.py
24 | tag_prefix =
25 | parentdir_prefix = aimmo-
26 |
--------------------------------------------------------------------------------
/players/templates/players/dropdown.html:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/aimmo-game-worker/test-initialise.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import sys
3 |
4 | import requests
5 |
6 | url = 'http://localhost:5001/initialise/'
7 |
8 | if len(sys.argv) > 1:
9 | avatar_file = sys.argv[1]
10 | else:
11 | avatar_file = '../players/avatar_examples/dumb_avatar.py'
12 |
13 | with open(avatar_file) as avatar_fileobj:
14 | avatar_data = avatar_fileobj.read()
15 |
16 | api_data = {
17 | 'code': avatar_data,
18 | 'options': {},
19 | }
20 |
21 | print 'Posting: ', api_data
22 | result = requests.post(url, json=api_data)
23 | print result.content
24 | result.raise_for_status()
25 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/event.py:
--------------------------------------------------------------------------------
1 | from collections import namedtuple
2 |
3 | ReceivedAttackEvent = namedtuple(
4 | 'ReceivedAttackEvent', ['attacking_avatar', 'damage_dealt'])
5 |
6 | PerformedAttackEvent = namedtuple(
7 | 'PerformedAttackEvent',
8 | ['attacked_avatar', 'target_location', 'damage_dealt'])
9 |
10 | FailedAttackEvent = namedtuple(
11 | 'FailedAttackEvent', ['target_location'])
12 |
13 | MovedEvent = namedtuple(
14 | 'MovedEvent', ['source_location', 'target_location'])
15 |
16 | FailedMoveEvent = namedtuple(
17 | 'FailedMoveEvent', ['source_location', 'target_location'])
18 |
--------------------------------------------------------------------------------
/aimmo-game-worker/simulation/event.py:
--------------------------------------------------------------------------------
1 | from collections import namedtuple
2 |
3 | ReceivedAttackEvent = namedtuple(
4 | 'ReceivedAttackEvent', ['attacking_avatar', 'damage_dealt'])
5 |
6 | PerformedAttackEvent = namedtuple(
7 | 'PerformedAttackEvent',
8 | ['attacked_avatar', 'target_location', 'damage_dealt'])
9 |
10 | FailedAttackEvent = namedtuple(
11 | 'FailedAttackEvent', ['target_location'])
12 |
13 | MovedEvent = namedtuple(
14 | 'MovedEvent', ['source_location', 'target_location'])
15 |
16 | FailedMoveEvent = namedtuple(
17 | 'FailedMoveEvent', ['source_location', 'target_location'])
18 |
--------------------------------------------------------------------------------
/render-manifests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | import sys
5 | import yaml
6 |
7 | try:
8 | os.makedirs(os.path.join(sys.argv[1], 'manifests'))
9 | except OSError:
10 | pass
11 |
12 | for filename in os.listdir(os.path.join(sys.argv[1], 'manifests-template')):
13 | content = open(os.path.join(sys.argv[1], 'manifests-template', filename)).read()
14 | content = content.replace('AIMMO_VERSION', sys.argv[2])
15 | content = content.replace('AIMMO_UI_URL', sys.argv[3])
16 | dest_filename = os.path.join(sys.argv[1], 'manifests', filename)
17 | with open(dest_filename, 'w') as fobj:
18 | fobj.write(content)
19 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/direction.py:
--------------------------------------------------------------------------------
1 | class Direction:
2 | def __init__(self, x, y):
3 | if abs(x) not in [0, 1]:
4 | raise ValueError
5 | if abs(y) not in [0, 1]:
6 | raise ValueError
7 | if abs(x) + abs(y) != 1:
8 | raise ValueError
9 | self.x = x
10 | self.y = y
11 |
12 | @property
13 | def dict(self):
14 | return {'x': self.x, 'y': self.y}
15 |
16 | def __repr__(self):
17 | return 'Direction(x={}, y={})'.format(self.x, self.y)
18 |
19 | NORTH = Direction(0, 1)
20 | EAST = Direction(1, 0)
21 | SOUTH = Direction(0, -1)
22 | WEST = Direction(-1, 0)
23 |
24 | ALL_DIRECTIONS = (NORTH, EAST, SOUTH, WEST)
25 |
--------------------------------------------------------------------------------
/aimmo-game-worker/simulation/location.py:
--------------------------------------------------------------------------------
1 | class Location(object):
2 | def __init__(self, x, y):
3 | self.x = x
4 | self.y = y
5 |
6 | def __add__(self, direction):
7 | return Location(self.x + direction.x, self.y + direction.y)
8 |
9 | def __sub__(self, direction):
10 | return Location(self.x - direction.x, self.y - direction.y)
11 |
12 | def __repr__(self):
13 | return 'Location({}, {})'.format(self.x, self.y)
14 |
15 | def __eq__(self, other):
16 | return self.x == other.x and self.y == other.y
17 |
18 | def __ne__(self, other):
19 | return not self == other
20 |
21 | def __hash__(self):
22 | return hash((self.x, self.y))
23 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/location.py:
--------------------------------------------------------------------------------
1 | class Location(object):
2 | def __init__(self, x, y):
3 | self.x = x
4 | self.y = y
5 |
6 | def __add__(self, direction):
7 | return Location(self.x + direction.x, self.y + direction.y)
8 |
9 | def __sub__(self, direction):
10 | return Location(self.x - direction.x, self.y - direction.y)
11 |
12 | def __repr__(self):
13 | return 'Location({}, {})'.format(self.x, self.y)
14 |
15 | def __eq__(self, other):
16 | return self.x == other.x and self.y == other.y
17 |
18 | def __ne__(self, other):
19 | return not self == other
20 |
21 | def __hash__(self):
22 | return hash((self.x, self.y))
23 |
24 | def serialise(self):
25 | return {'x': self.x, 'y': self.y}
26 |
--------------------------------------------------------------------------------
/players/templates/registration/login.html:
--------------------------------------------------------------------------------
1 | {% extends 'players/base.html' %}
2 |
3 | {% block content %}
4 | {% if form.errors %}
5 | Your username and password didn't match. Please try again.
6 | {% endif %}
7 |
8 |
24 | {% endblock %}
25 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from setuptools import find_packages, setup
3 |
4 | import versioneer
5 |
6 | setup(
7 | name='aimmo',
8 | cmdclass=versioneer.get_cmdclass(),
9 | packages=find_packages(),
10 | include_package_data=True,
11 | install_requires=[
12 | 'django >= 1.8.3, < 1.9.0',
13 | 'django-autoconfig >= 0.3.6, < 1.0.0',
14 | 'django-forms-bootstrap',
15 | 'django-js-reverse',
16 | 'eventlet',
17 | 'flask',
18 | 'flask-socketio',
19 | 'requests',
20 | 'six',
21 | 'pykube',
22 | ],
23 | tests_require=[
24 | 'django-setuptest',
25 | 'httmock',
26 | ],
27 | test_suite='setuptest.setuptest.SetupTestSuite',
28 | version=versioneer.get_version(),
29 | zip_safe=False,
30 | )
31 |
--------------------------------------------------------------------------------
/aimmo-game-worker/initialise.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import json
4 | import logging
5 | import os
6 | import sys
7 |
8 | import requests
9 |
10 | LOGGER = logging.getLogger(__name__)
11 |
12 |
13 | # TODO: Delete? Not used by LocalWorkerManager anymore. Is it used by anything else?
14 | def main(args, url):
15 | data_dir = args[1]
16 | LOGGER.debug('Data dir is %s', data_dir)
17 |
18 | data = requests.get(url).json()
19 |
20 | options = data['options']
21 | with open('{}/options.json'.format(data_dir), 'w') as options_file:
22 | json.dump(options, options_file)
23 |
24 | code = data['code']
25 | with open('{}/avatar.py'.format(data_dir), 'w') as avatar_file:
26 | avatar_file.write(code)
27 |
28 | if __name__ == '__main__':
29 | logging.basicConfig(level=logging.DEBUG)
30 | main(sys.argv, url=os.environ['DATA_URL'])
31 |
--------------------------------------------------------------------------------
/aimmo-game-worker/test-turn.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import requests
3 |
4 | url = 'http://localhost:5001/turn/'
5 |
6 | avatar = {'location': {'x': 0, 'y': 0}, 'health': 5, 'score': 0, 'events': []}
7 | api_data = {
8 | 'avatar_state': avatar,
9 | 'world_map': {
10 | 'cells': [
11 | {'location': {'x': 0, 'y': 0}, 'habitable': True,
12 | 'generates_score': True, 'avatar': avatar, 'pickup': None},
13 | {'location': {'x': 1, 'y': 0}, 'habitable': False,
14 | 'generates_score': False, 'avatar': None, 'pickup': None},
15 | {'location': {'x': -1, 'y': 0}, 'habitable': True, 'generates_score':
16 | False, 'avatar': avatar, 'pickup': {'health_restored': 3}},
17 | ],
18 | }
19 | }
20 |
21 | print 'Posting:', api_data
22 | result = requests.post(url, json=api_data)
23 | result.raise_for_status()
24 | print 'Output:', result.json()
25 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_service.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from unittest import TestCase
4 |
5 | from simulation.game_state import GameState
6 | from simulation.location import Location
7 | from simulation.turn_manager import state_provider
8 | from simulation.world_map import WorldMap
9 | from simulation.world_state import WorldState
10 |
11 | import service
12 |
13 | from .test_simulation.dummy_avatar import MoveEastDummy
14 | from .test_simulation.maps import MockPickup
15 | from .test_simulation.test_world_map import MockCell
16 |
17 | class SimpleAvatarManager(object):
18 | avatars = [MoveEastDummy(1, Location(0, -1))]
19 |
20 | # TODO: Write test for the new API...
21 | class TestService(TestCase):
22 | def test_healthy(self):
23 | service.app.config['TESTING'] = True
24 | self.app = service.app.test_client()
25 | response = self.app.get('/')
26 | self.assertEqual(response.data, 'HEALTHY')
27 |
--------------------------------------------------------------------------------
/aimmo-game-worker/simulation/action.py:
--------------------------------------------------------------------------------
1 | class Action(object):
2 |
3 | def serialise(self):
4 | raise NotImplementedError
5 |
6 |
7 | class WaitAction(Action):
8 |
9 | def serialise(self):
10 | return {
11 | 'action_type': 'wait',
12 | }
13 |
14 |
15 | class MoveAction(Action):
16 |
17 | def __init__(self, direction):
18 | self.direction = direction
19 |
20 | def serialise(self):
21 | return {
22 | 'action_type': 'move',
23 | 'options': {
24 | 'direction': self.direction.serialise()
25 | },
26 | }
27 |
28 |
29 | class AttackAction(Action):
30 |
31 | def __init__(self, direction):
32 | self.direction = direction
33 |
34 | def serialise(self):
35 | return {
36 | 'action_type': 'attack',
37 | 'options': {
38 | 'direction': self.direction.serialise()
39 | },
40 | }
41 |
--------------------------------------------------------------------------------
/manifests-template/rc-aimmo-game-creator.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ReplicationController
3 | metadata:
4 | name: aimmo-game-creator
5 | spec:
6 | replicas: 1
7 | # selector identifies the set of Pods that this
8 | # replication controller is responsible for managing
9 | selector:
10 | app: aimmo-game-creator
11 | # podTemplate defines the 'cookie cutter' used for creating
12 | # new pods when necessary
13 | template:
14 | metadata:
15 | labels:
16 | # Important: these labels need to match the selector above
17 | # The api server enforces this constraint.
18 | app: aimmo-game-creator
19 | spec:
20 | containers:
21 | - name: aimmo-game-creator
22 | image: ocadotechnology/aimmo-game-creator:AIMMO_VERSION
23 | ports:
24 | - containerPort: 80
25 | env:
26 | - name: IMAGE_SUFFIX
27 | value: AIMMO_VERSION
28 | - name: GAME_API_URL
29 | value: AIMMO_UI_URL
30 | - name: WORKER_MANAGER
31 | value: kubernetes
32 |
--------------------------------------------------------------------------------
/players/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 | from django.conf import settings
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='Avatar',
17 | fields=[
18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
19 | ('player', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
20 | ],
21 | ),
22 | migrations.CreateModel(
23 | name='Player',
24 | fields=[
25 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
26 | ('code', models.TextField()),
27 | ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
28 | ],
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_simulation/test_direction.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from unittest import TestCase
4 |
5 | from simulation.direction import Direction
6 |
7 |
8 | class TestDirection(TestCase):
9 | def test_good_data(self):
10 | d = Direction(0, 1)
11 | self.assertEqual(d.x, 0)
12 | self.assertEqual(d.y, 1)
13 |
14 | def test_high_x(self):
15 | with self.assertRaises(ValueError):
16 | Direction(1.5, 0)
17 |
18 | def test_low_x(self):
19 | with self.assertRaises(ValueError):
20 | Direction(-1.5, 0)
21 |
22 | def test_high_y(self):
23 | with self.assertRaises(ValueError):
24 | Direction(0, 1.5)
25 |
26 | def test_low_y(self):
27 | with self.assertRaises(ValueError):
28 | Direction(0, -1.5)
29 |
30 | def test_too_far(self):
31 | with self.assertRaises(ValueError):
32 | Direction(1, 1)
33 |
34 | def test_repr(self):
35 | txt = repr(Direction(1, 0))
36 | self.assertRegexpMatches(txt, 'x *= *1')
37 | self.assertRegexpMatches(txt, 'y *= *0')
38 |
--------------------------------------------------------------------------------
/players/templates/players/program.html:
--------------------------------------------------------------------------------
1 | {% extends 'players/base.html' %}
2 |
3 | {% block nav-program-class %}active{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
Program
8 |
Use the box below to program your Avatar. Save using the button on the right.
9 |
10 |
14 |
23 | {% endblock %}
24 |
25 | {% block scripts %}
26 |
29 |
30 |
31 |
32 |
33 | {% endblock %}
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | env/
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | *.egg-info/
23 | .installed.cfg
24 | *.egg
25 |
26 | # PyInstaller
27 | # Usually these files are written by a python script from a template
28 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
29 | *.manifest
30 | *.spec
31 |
32 | # Installer logs
33 | pip-log.txt
34 | pip-delete-this-directory.txt
35 |
36 | # Unit test / coverage reports
37 | htmlcov/
38 | .tox/
39 | .coverage
40 | .coverage.*
41 | .cache
42 | nosetests.xml
43 | coverage.xml
44 | *,cover
45 | pep8.txt
46 |
47 | # Translations
48 | *.mo
49 | *.pot
50 |
51 | # Django stuff:
52 | *.log
53 |
54 | # Sphinx documentation
55 | docs/_build/
56 |
57 | # PyBuilder
58 | target/
59 |
60 | # DB
61 | db.sqlite3
62 |
63 | # IntelliJ
64 | .idea
65 | *.iml
66 |
67 | # static folder
68 | example_project/example_project/static/
69 |
70 | # Created during minikube testing
71 | manifests
72 | test-bin/
73 |
--------------------------------------------------------------------------------
/aimmo-game-worker/tests/test_initialise.py:
--------------------------------------------------------------------------------
1 | from httmock import all_requests, with_httmock
2 | import initialise
3 | import json
4 | from os.path import join
5 | from tempfile import mkdtemp
6 | from unittest import TestCase
7 |
8 | CODE = "class Avatar: pass"
9 | OPTIONS = {'test': True}
10 |
11 |
12 | @all_requests
13 | def return_data(url, request):
14 | global url_requested
15 | url_requested = url
16 | return json.dumps({
17 | 'code': CODE,
18 | 'options': OPTIONS,
19 | })
20 |
21 |
22 | class TestInitialise(TestCase):
23 | @classmethod
24 | def setUpClass(cls):
25 | cls.TMP_DIR = mkdtemp()
26 |
27 | @with_httmock(return_data)
28 | def test_fetching_and_writing(self):
29 | args = ('', self.TMP_DIR)
30 | initialise.main(args, 'http://test')
31 | self.assertRegexpMatches(url_requested.geturl(), 'http://test/?')
32 | options_path = join(self.TMP_DIR, 'options.json')
33 | avatar_path = join(self.TMP_DIR, 'avatar.py')
34 | with open(options_path) as options_file:
35 | self.assertEqual(json.load(options_file), OPTIONS)
36 | with open(avatar_path) as avatar_file:
37 | self.assertEqual(avatar_file.read(), CODE)
38 |
--------------------------------------------------------------------------------
/aimmo-game-worker/service.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import json
3 | import logging
4 | import sys
5 |
6 | import flask
7 |
8 | from simulation.avatar_state import AvatarState
9 | from simulation.world_map import WorldMap
10 |
11 | app = flask.Flask(__name__)
12 | LOGGER = logging.getLogger(__name__)
13 |
14 | worker_avatar = None
15 |
16 |
17 | @app.route('/turn/', methods=['POST'])
18 | def process_turn():
19 | LOGGER.info('Calculating action')
20 | data = flask.request.get_json()
21 |
22 | world_map = WorldMap(**data['world_map'])
23 | avatar_state = AvatarState(**data['avatar_state'])
24 |
25 | action = worker_avatar.handle_turn(avatar_state, world_map)
26 |
27 | return flask.jsonify(action=action.serialise())
28 |
29 |
30 | def run(host, port, directory):
31 | logging.basicConfig(level=logging.DEBUG)
32 |
33 | with open('{}/options.json'.format(directory)) as option_file:
34 | options = json.load(option_file)
35 | from avatar import Avatar
36 | global worker_avatar
37 | worker_avatar = Avatar(**options)
38 |
39 | app.config['DEBUG'] = False
40 | app.run(host, port)
41 |
42 | if __name__ == '__main__':
43 | run(host=sys.argv[1], port=int(sys.argv[2]), directory=sys.argv[3])
44 |
--------------------------------------------------------------------------------
/players/templates/players/watch.html:
--------------------------------------------------------------------------------
1 | {% extends 'players/base.html' %}
2 |
3 | {% block nav-watch-class %}active{% endblock %}
4 |
5 | {% block styles %}
6 |
7 | {% endblock %}
8 |
9 | {% block scripts %}
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {% endblock %}
28 |
29 | {% block content %}
30 |
31 | {% endblock %}
32 |
--------------------------------------------------------------------------------
/players/avatar_examples/attacking_avatar.py:
--------------------------------------------------------------------------------
1 | class Avatar(object):
2 | def handle_turn(self, avatar_state, world_map):
3 | from simulation.action import MoveAction
4 | from simulation import direction
5 | self.avatar_state = avatar_state
6 |
7 | self.location = self.avatar_state.location
8 | directions = (direction.EAST, direction.SOUTH, direction.WEST, direction.NORTH)
9 | direction_of_other_avatar = next((d for d in directions if world_map.is_visible(self.location + d) and world_map.get_cell(self.location + d).avatar), None)
10 | if direction_of_other_avatar:
11 | from simulation.action import AttackAction
12 | return AttackAction(direction_of_other_avatar)
13 | import random
14 |
15 | direction_to_other_player = self.direction_to(next(cell.location for cell in world_map.all_cells() if cell.avatar and cell.location != avatar_state.location))
16 | if direction_to_other_player:
17 | return MoveAction(random.choice(directions + ((direction_to_other_player,) * 10)))
18 | return MoveAction(random.choice(directions))
19 |
20 | def direction_to(self, location):
21 | from simulation import direction
22 | vector_to = location - self.location
23 | if vector_to.x != 0:
24 | return direction.Direction(1 if vector_to.x > 0 else -1, 0)
25 | if vector_to.y != 0:
26 | return direction.Direction(0, 1 if vector_to.y > 0 else -1)
27 | return None
28 |
--------------------------------------------------------------------------------
/players/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 | from django.contrib.auth import views as auth_views
3 | from django.contrib.auth.decorators import login_required
4 | from django.views.generic import TemplateView
5 |
6 | from players import views
7 |
8 | urlpatterns = [
9 | url(r'^$', TemplateView.as_view(template_name='players/home.html'), name='aimmo/home'),
10 |
11 | url(r'^accounts/login/$', auth_views.login),
12 |
13 | url(r'^program/(?P[0-9]+)/$', login_required(views.ProgramView.as_view()), name='aimmo/program'),
14 | url(r'^program_level/(?P[0-9]+)/$', login_required(views.program_level), name='aimmo/program_level'),
15 | url(r'^watch/(?P[0-9]+)/$', login_required(views.watch_game), name='aimmo/watch'),
16 | url(r'^watch_level/(?P[0-9]+)/$', login_required(views.watch_level), name='aimmo/watch_level'),
17 | url(r'^statistics/$', TemplateView.as_view(template_name='players/statistics.html'), name='aimmo/statistics'),
18 |
19 | url(r'^api/code/(?P[0-9]+)/$', views.code, name='aimmo/code'),
20 | url(r'^api/games/$', views.list_games, name='aimmo/games'),
21 | url(r'^api/games/(?P[0-9]+)/$', views.get_game, name='aimmo/game_details'),
22 | url(r'^api/games/(?P[0-9]+)/complete/$', views.mark_game_complete, name='aimmo/complete_game'),
23 |
24 | url(r'^jsreverse/$', 'django_js_reverse.views.urls_js', name='aimmo/js_reverse'), # TODO: Pull request to make django_js_reverse.urls
25 | url(r'^games/new/$', views.add_game, name='aimmo/new_game'),
26 | ]
27 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_simulation/test_location.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from unittest import TestCase
4 |
5 | from simulation.location import Location
6 |
7 |
8 | class TestLocation(TestCase):
9 | def test_equal(self):
10 | loc_1 = Location(3, 3)
11 | loc_2 = Location(3, 3)
12 | self.assertEqual(loc_1, loc_2)
13 | self.assertFalse(loc_1 != loc_2)
14 |
15 | def test_x_not_equal(self):
16 | loc_1 = Location(3, 3)
17 | loc_2 = Location(4, 3)
18 | self.assertNotEqual(loc_1, loc_2)
19 | self.assertFalse(loc_1 == loc_2)
20 |
21 | def test_y_not_equal(self):
22 | loc_1 = Location(4, 4)
23 | loc_2 = Location(4, 3)
24 | self.assertNotEqual(loc_1, loc_2)
25 | self.assertFalse(loc_1 == loc_2)
26 |
27 | def test_add(self):
28 | loc_1 = Location(1, 2)
29 | loc_2 = Location(3, 4)
30 | expected = Location(4, 6)
31 | self.assertEqual(loc_1 + loc_2, expected)
32 |
33 | def test_sub(self):
34 | loc_1 = Location(1, 2)
35 | loc_2 = Location(3, 4)
36 | expected = Location(-2, -2)
37 | self.assertEqual(loc_1 - loc_2, expected)
38 |
39 | def test_hash_equal(self):
40 | loc_1 = Location(3, 3)
41 | loc_2 = Location(3, 3)
42 | self.assertEqual(hash(loc_1), hash(loc_2))
43 |
44 | def test_serialise(self):
45 | loc = Location(3, 9)
46 | expected = {'x': 3, 'y': 9}
47 | self.assertEqual(loc.serialise(), expected)
48 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/effects.py:
--------------------------------------------------------------------------------
1 | from abc import ABCMeta, abstractmethod
2 |
3 |
4 | class _Effect(object):
5 | __metaclass__ = ABCMeta
6 |
7 | def __init__(self, avatar):
8 | self._avatar = avatar
9 | self.is_expired = False
10 |
11 | @abstractmethod
12 | def on_turn(self):
13 | raise NotImplementedError()
14 |
15 |
16 | class _TimedEffect(_Effect):
17 | __metaclass__ = ABCMeta
18 | EFFECT_TIME = 10
19 |
20 | def __init__(self, *args):
21 | super(_TimedEffect, self).__init__(*args)
22 | self._time_remaining = self.EFFECT_TIME
23 |
24 | def remove(self):
25 | self._avatar.effects.remove(self)
26 |
27 | def on_turn(self):
28 | self._time_remaining -= 1
29 | if self._time_remaining <= 0:
30 | self.is_expired = True
31 |
32 |
33 | class InvulnerabilityPickupEffect(_TimedEffect):
34 | def __init__(self, *args):
35 | super(InvulnerabilityPickupEffect, self).__init__(*args)
36 | self._avatar.resistance += 1000
37 |
38 | def remove(self):
39 | super(InvulnerabilityPickupEffect, self).remove()
40 | self._avatar.resistance -= 1000
41 |
42 |
43 | class DamagePickupEffect(_TimedEffect):
44 | def __init__(self, damage_boost, *args):
45 | self._damage_boost = damage_boost
46 | super(DamagePickupEffect, self).__init__(*args)
47 | self._avatar.attack_strength += self._damage_boost
48 |
49 | def remove(self):
50 | super(DamagePickupEffect, self).remove()
51 | self._avatar.attack_strength -= self._damage_boost
52 |
--------------------------------------------------------------------------------
/players/avatar_examples/winner_avatar.py:
--------------------------------------------------------------------------------
1 | class Avatar(object):
2 | def handle_turn(self, avatar_state, world_map):
3 | from simulation.action import MoveAction
4 | from simulation import direction
5 | import random
6 | from simulation.action import WaitAction
7 |
8 | self.world_map = world_map
9 | self.avatar_state = avatar_state
10 |
11 | if world_map.get_cell(avatar_state.location).generates_score:
12 | return WaitAction()
13 |
14 | possible_directions = self.get_possible_directions()
15 | directions_to_emphasise = [d for d in possible_directions if self.is_towards(d, self.get_closest_score_location())]
16 | return MoveAction(random.choice(possible_directions + (directions_to_emphasise * 5)))
17 |
18 | def is_towards(self, direction, location):
19 | if location:
20 | return self.distance_between(self.avatar_state.location, location) > \
21 | self.distance_between(self.avatar_state.location + direction, location)
22 | else:
23 | return False
24 |
25 | def distance_between(self, a, b):
26 | return abs(a.x - b.x) + abs(a.y - b.y)
27 |
28 | def get_closest_score_location(self):
29 | score_cells = list(self.world_map.score_cells())
30 | if score_cells:
31 | return min(score_cells, key=lambda cell: self.distance_between(cell.location, self.avatar_state.location)).location
32 | else:
33 | return None
34 |
35 | def get_possible_directions(self):
36 | from simulation import direction
37 | directions = (direction.EAST, direction.SOUTH, direction.WEST, direction.NORTH)
38 | return [d for d in directions if self.world_map.can_move_to(self.avatar_state.location + d)]
39 |
--------------------------------------------------------------------------------
/players/migrations/0004_auto_20160808_1511.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('players', '0003_auto_20160802_1418'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='game',
16 | name='obstacle_ratio',
17 | field=models.FloatField(default=0.1),
18 | ),
19 | migrations.AddField(
20 | model_name='game',
21 | name='pickup_spawn_chance',
22 | field=models.FloatField(default=0.02),
23 | ),
24 | migrations.AddField(
25 | model_name='game',
26 | name='score_despawn_chance',
27 | field=models.FloatField(default=0.02),
28 | ),
29 | migrations.AddField(
30 | model_name='game',
31 | name='start_height',
32 | field=models.IntegerField(default=11),
33 | ),
34 | migrations.AddField(
35 | model_name='game',
36 | name='start_width',
37 | field=models.IntegerField(default=11),
38 | ),
39 | migrations.AddField(
40 | model_name='game',
41 | name='target_num_cells_per_avatar',
42 | field=models.FloatField(default=16),
43 | ),
44 | migrations.AddField(
45 | model_name='game',
46 | name='target_num_pickups_per_avatar',
47 | field=models.FloatField(default=0.5),
48 | ),
49 | migrations.AddField(
50 | model_name='game',
51 | name='target_num_score_locations_per_avatar',
52 | field=models.FloatField(default=0.5),
53 | ),
54 | ]
55 |
--------------------------------------------------------------------------------
/players/avatar_examples/health_seeker_avatar.py:
--------------------------------------------------------------------------------
1 | class Avatar(object):
2 | def handle_turn(self, avatar_state, world_map):
3 | from simulation.action import MoveAction
4 | from simulation import direction
5 | import random
6 | from simulation.action import WaitAction
7 |
8 | self.world_map = world_map
9 | self.avatar_state = avatar_state
10 |
11 | if world_map.get_cell(avatar_state.location).generates_score:
12 | return WaitAction()
13 |
14 | possible_directions = self.get_possible_directions()
15 | directions_to_emphasise = [d for d in possible_directions if self.is_towards(d, self.get_closest_pickup_location())]
16 | return MoveAction(random.choice(possible_directions + (directions_to_emphasise * 5)))
17 |
18 | def is_towards(self, direction, location):
19 | if location:
20 | return self.distance_between(self.avatar_state.location, location) > \
21 | self.distance_between(self.avatar_state.location + direction, location)
22 | else:
23 | return None
24 |
25 | def distance_between(self, a, b):
26 | return abs(a.x - b.x) + abs(a.y - b.y)
27 |
28 | def get_closest_pickup_location(self):
29 | pickup_cells = list(self.world_map.pickup_cells())
30 | if pickup_cells:
31 | c = min(pickup_cells, key=lambda cell: self.distance_between(cell.location, self.avatar_state.location))
32 | print 'targetting', c
33 | return c.location
34 | else:
35 | return None
36 |
37 | def get_possible_directions(self):
38 | from simulation import direction
39 | directions = (direction.EAST, direction.SOUTH, direction.WEST, direction.NORTH)
40 | return [d for d in directions if self.world_map.can_move_to(self.avatar_state.location + d)]
41 |
--------------------------------------------------------------------------------
/all_tests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Run all tests for the project.
3 |
4 | Usage:
5 | run_tests.py [--coverage]
6 |
7 | Optional arguments:
8 | -c, --coverage compute the coverage while running tests.
9 | """
10 |
11 | import os
12 | import subprocess
13 | import sys
14 |
15 | BASE_DIR = os.path.abspath(os.path.dirname(__file__))
16 | APPS = ('', 'aimmo-game/', 'aimmo-game-worker/', 'aimmo-game-creator/')
17 |
18 |
19 | def print_help():
20 | print(globals()['__docstring__'])
21 |
22 |
23 | def main():
24 | if '--help' in sys.argv or '-h' in sys.argv:
25 | print_help()
26 | sys.exit(0)
27 | else:
28 | compute_coverage = '--coverage' in sys.argv or '-c' in sys.argv
29 | sys.exit(run_tests(compute_coverage))
30 |
31 |
32 | def run_tests(compute_coverage):
33 | def app_name(app):
34 | return 'players' if app == '' else app
35 |
36 | failed_apps = []
37 | for app in APPS:
38 | print('Testing {}'.format(app))
39 |
40 | dir = os.path.join(BASE_DIR, app)
41 | if compute_coverage and app != '':
42 | result = subprocess.call(['coverage', 'run', '--concurrency=eventlet', '--source=.', 'setup.py', 'test'], cwd=dir)
43 | else:
44 | result = subprocess.call([sys.executable, 'setup.py', 'test'], cwd=dir)
45 | if result != 0:
46 | print('Tests failed: '.format(result))
47 | failed_apps.append(app_name(app))
48 | if compute_coverage:
49 | coverage_files = [app + '.coverage' for app in APPS]
50 | subprocess.call(['coverage', 'combine'] + coverage_files, cwd=BASE_DIR)
51 | if failed_apps:
52 | print('The app(s) %s had failed tests' % ', '.join(failed_apps))
53 | return 1
54 | else:
55 | print('All tests ran successfully')
56 | return 0
57 |
58 |
59 | if __name__ == '__main__':
60 | main()
61 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_simulation/test_effects.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import abc
4 | from unittest import TestCase
5 |
6 | from simulation import effects
7 | from .dummy_avatar import DummyAvatar
8 |
9 |
10 | class _BaseCases(object):
11 | class BaseTimedEffectTestCase(TestCase):
12 | __metaclass__ = abc.ABCMeta
13 |
14 | @abc.abstractmethod
15 | def make_effect(self, avatar):
16 | pass
17 |
18 | def setUp(self):
19 | self.avatar = DummyAvatar(1, None)
20 | self.effect = self.make_effect(self.avatar)
21 | self.avatar.effects.add(self.effect)
22 |
23 | def assertNoEffects(self):
24 | self.assertEqual(len(list(self.avatar.effects)), 0)
25 |
26 | def test_effect_removed(self):
27 | self.effect.remove()
28 | self.assertNoEffects()
29 |
30 | def test_effect_expires(self):
31 | for _ in xrange(10):
32 | self.effect.on_turn()
33 | self.assertTrue(self.effect.is_expired)
34 |
35 |
36 | class TestInvulnerabilityEffect(_BaseCases.BaseTimedEffectTestCase):
37 | def make_effect(self, *args):
38 | return effects.InvulnerabilityPickupEffect(*args)
39 |
40 | def test_resistance_increases(self):
41 | self.assertEqual(self.avatar.resistance, 1000)
42 |
43 | def test_resistance_decreases(self):
44 | self.effect.remove()
45 | self.assertEqual(self.avatar.resistance, 0)
46 |
47 |
48 | class TestDamageEffect(_BaseCases.BaseTimedEffectTestCase):
49 | def make_effect(self, *args):
50 | return effects.DamagePickupEffect(5, *args)
51 |
52 | def test_damage_increases(self):
53 | self.assertEqual(self.avatar.attack_strength, 6)
54 |
55 | def test_damage_decreases(self):
56 | self.effect.remove()
57 | self.assertEqual(self.avatar.attack_strength, 1)
58 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/game_state.py:
--------------------------------------------------------------------------------
1 | from simulation.avatar import fog_of_war
2 |
3 |
4 | class GameState(object):
5 | """
6 | Encapsulates the entire game state, including avatars, their code, and the world.
7 | """
8 | def __init__(self, world_map, avatar_manager, completion_check_callback=lambda: None):
9 | self.world_map = world_map
10 | self.avatar_manager = avatar_manager
11 | self._completion_callback = completion_check_callback
12 | self.main_avatar_id = None
13 |
14 | def get_state_for(self, avatar_wrapper, fog_of_war=fog_of_war):
15 | processed_world_map = fog_of_war.apply_fog_of_war(self.world_map, avatar_wrapper)
16 | return {
17 | 'avatar_state': avatar_wrapper.serialise(),
18 | 'world_map': {
19 | 'cells': [cell.serialise() for cell in processed_world_map.all_cells()]
20 | }
21 | }
22 |
23 | def add_avatar(self, user_id, worker_url, location=None):
24 | location = self.world_map.get_random_spawn_location() if location is None else location
25 | avatar = self.avatar_manager.add_avatar(user_id, worker_url, location)
26 | self.world_map.get_cell(location).avatar = avatar
27 |
28 | def remove_avatar(self, user_id):
29 | try:
30 | avatar = self.avatar_manager.get_avatar(user_id)
31 | except KeyError:
32 | return
33 | self.world_map.get_cell(avatar.location).avatar = None
34 | self.avatar_manager.remove_avatar(user_id)
35 |
36 | def _update_effects(self):
37 | for avatar in self.avatar_manager.active_avatars:
38 | avatar.update_effects()
39 |
40 | def update_environment(self):
41 | self._update_effects()
42 | num_avatars = len(self.avatar_manager.active_avatars)
43 | self.world_map.update(num_avatars)
44 |
45 | def is_complete(self):
46 | return self._completion_callback(self)
47 |
48 | def get_main_avatar(self):
49 | return self.avatar_manager.avatars_by_id[self.main_avatar_id]
50 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | dist: trusty
3 | group: edge
4 | language: python
5 | python:
6 | - 2.7
7 | env:
8 | global:
9 | - secure: "TNhj8oXrtBzCkkJDA8qbDmqUggK9Kbsy8Itgi0+VmXB+3bVRornZMS38ppaFz+BVOTdL80ZvN2s/OPV106QjFv0Hx1MmWAw4kNST2QBtxFXBHRKYtr7NtBN0jr11el1fG83YdpeZYQbc5aqbJ4OPz2GpfhGfDVhVGPjMFMKXI5XbTbbl+HCEL67ywozt964LhpuuXTaX7jgYFiJUtcwkYRUaDYY3ryJvMSOx95AjKyRMNC5JlgAqbJuYsOTm1eVZtfQ1jYVvd/NuAHOMDNpZWvcIaxTuc3k4XZh4UPryuRJWfjgjIq6kua7Q6ho6W2GbDgN2b/9lIldkTR8QfLSnCNLJIg6KJZ2gmIQg7u+nZHemdugo9XkvfmKXfB/t3HChFX1HNtS4gSeIn874IynLHx3UJ1lxm7BdDbF4Jjijffj5uWDGqVj3/Myd2jdFTJCoLJXvYI7la6ouMzXW5aDFhy2UXK2A3q7aBbaD64+U1R7YPGIyvfAd7NCF11vtRvJGI/fNjO5S1EuSacrQm7CiXu0rd0L5EOSU85XNTQsWN6xxJKEcc8Hx9YLRXkmR7gK9LoEPTUwFbfVXBUvnOsZav3MOBBxzj4+eLxkx2B1vbY2Lx5yPAqyWwi3vet46NEZUIKgqK+xRYQKj6dj3OF1gx7LOcyhpyevdpTZotiEx0C4=" # SNAP_API_AUTH
10 | git:
11 | depth: 9999999 # Building untagged builds needs enough depth to get the latest tag
12 | install:
13 | - pip install .
14 | - pip install coveralls
15 | script:
16 | - python all_tests.py --coverage
17 | after_success:
18 | - coveralls
19 | deploy:
20 | provider: pypi
21 | user: ocadotechnology
22 | password:
23 | secure: "dX2M4PpyGwa1bjpJN/Wk3EORWRXuTS14ZQEQ7Ndqk/WnOZlNqD5t/WiITTdqBWoR/ScvQiGBzR/VdRLZllkGrhupoMrtkfCldaxEI3/wbbwW9CiLuwyR/V5xgVj9TA+PtXErjgAqG9KRtyaBEOaC20t13Uc6vuIc9e6aXkzhQ9hf3vVMlnABOZP3f/2R3+sJQMLylGPCz/6BAs2U+nyxGJCpfq0aJK7H2aVPUKtM1/nscuhvsrl8yU+RWYS3idgfXDuPlwqKvRL9xbEm1DC3ByCFwwzAGCPlUtzYlx6Ttrg8Jv6S1cgsjxijzV5QqU+k1JhmY3jDqqmMROFAaY3wJ6038xUbn4zlgvXgtCayiebJukSMR7tJoVrs6ao26QPsbLNZdReDzSXJR05pgB4I8gGTlIKeZE8zOS75hkv0CdMjmIsQhTeMKqQcATuN8QszYRP4uO7vG+3I7hNSK8HcqxHtNLEDq2/QKDAVBve0R5CeEvLMnSxj7UydO+HuJUgickjHuJUmLZ+7iR9i1p6G0MUpKq/n8izSponvShu0lDshrNWJ9PYuhRD4Yvnp4/FCSxdY0+u7pHJsbluGcIvaAlwoa5O01Seu2pVpox8hVTIhn9LuR0wP7Ed4Nxper7cMNgugqb8J9BybSlMjRlBYmI7FQePCdsu1ETNI9JDHAug="
24 | distributions: "bdist_wheel sdist"
25 | on:
26 | repo: ocadotechnology/aimmo
27 | after_deploy:
28 | - "curl -d POST -v https://semaphoreci.com/api/v1/projects/${SEMAPHORE_PROJECT_ID}/master/build?auth_token=${SEMAPHORE_API_AUTH}"
29 |
--------------------------------------------------------------------------------
/players/migrations/0005_auto_20160808_1545.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.conf import settings
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12 | ('players', '0004_auto_20160808_1511'),
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='LevelAttempt',
18 | fields=[
19 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
20 | ('level_number', models.IntegerField()),
21 | ],
22 | ),
23 | migrations.AddField(
24 | model_name='game',
25 | name='completed',
26 | field=models.BooleanField(default=False),
27 | ),
28 | migrations.AddField(
29 | model_name='game',
30 | name='generator',
31 | field=models.CharField(default=b'Main', max_length=20, choices=[(b'Main', b'Open World'), (b'Level1', b'Level 1')]),
32 | ),
33 | migrations.AddField(
34 | model_name='game',
35 | name='main_user',
36 | field=models.ForeignKey(related_name='games_for_user', blank=True, to=settings.AUTH_USER_MODEL, null=True),
37 | ),
38 | migrations.AlterField(
39 | model_name='game',
40 | name='owner',
41 | field=models.ForeignKey(related_name='owned_games', blank=True, to=settings.AUTH_USER_MODEL, null=True),
42 | ),
43 | migrations.AddField(
44 | model_name='levelattempt',
45 | name='game',
46 | field=models.OneToOneField(to='players.Game'),
47 | ),
48 | migrations.AddField(
49 | model_name='levelattempt',
50 | name='user',
51 | field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
52 | ),
53 | migrations.AlterUniqueTogether(
54 | name='levelattempt',
55 | unique_together=set([('level_number', 'user')]),
56 | ),
57 | ]
58 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## So you want to **clone** the project and figure out **what** to do...
2 | * The good practice is to:
3 | * Fork the project on your account
4 | * Clone your repo using HTTPS
5 | * Work on a new git branch.
6 | * Need help with [git](https://git-scm.com/docs/gittutorial)?
7 | Anyway you can't use Ocado Technology's master.
8 | * The [issues are listed on ocadotechnology/aimmo](https://github.com/ocadotechnology/aimmo/issues).
9 | It's even better if you're using [ZenHub](https://www.zenhub.com/) because it will allow you to look at a [Kanban-ish board](https://github.com/ocadotechnology/aimmo/issues#boards) for the project.
10 |
11 | ## Now you want to **test** your changes and **run** the project locally...
12 | * To work on the project, you can use whichever editor you like. Lots here like [IntelliJ or PyCharm](https://www.jetbrains.com/), for instance.
13 | * As said in the [readme](https://github.com/ocadotechnology/aimmo), you should set up a virtual environment.
14 | * e.g. the first time, `mkvirtualenv -a path/to/aimmo aimmo`
15 | * and thereafter: `workon aimmo`
16 | * You can test your change by running the test suite - you can go to the root of the project and type: `python example_project/manage.py test` ; but Travis uses `python setup.py test` (will also install stuff in your virtualenv needed for the tests)
17 | * To manually test things and run the project, `./run` in the root.
18 |
19 | ## Great, you can **commit**, open a **Pull Request**, and we'll **review** it...
20 | * Then you can commit! On a new branch for a new Pull Request please.
21 | * If your commit resolves a GitHub issue, please include “fixes #123” in the commit message.
22 | * Then you can push to your forked repo, and create a pull request from your branch to ocadotechnology's master branch.
23 | * Some tests will run automatically: Travis will run the automated tests, coverage will test the test coverage. Please fix found issues, then repush on your branch - it will rerun the tests.
24 | * Do not accept a PR yourself - at least someone else should review your code and approve it first.
25 | * Some old PRs will need to see the branch rebased on the current master
26 | * When a PR is accepted, **congrats!** It will be merged on master.
27 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/avatar/fog_of_war.py:
--------------------------------------------------------------------------------
1 | import copy
2 |
3 | from simulation.location import Location
4 | from simulation.world_map import WorldMap
5 |
6 |
7 | def apply_fog_of_war(world_map, avatar_wrapper):
8 | """
9 | Takes a world state and an avatar and returns a personalised view of the world.
10 | :param world_map: the state of the game
11 | :param avatar_wrapper: the application's view of the avatar
12 | :return: a world state tailored to the given avatar
13 | """
14 |
15 | location = avatar_wrapper.location
16 | no_fog_distance = avatar_wrapper.fog_of_war_modifier + world_map.get_no_fog_distance()
17 | partial_fog_distance = avatar_wrapper.fog_of_war_modifier + world_map.get_partial_fog_distance()
18 |
19 | lower_x = max(location.x - partial_fog_distance, world_map.min_x())
20 | lower_y = max(location.y - partial_fog_distance, world_map.min_y())
21 | upper_x = min(location.x + partial_fog_distance, world_map.max_x())
22 | upper_y = min(location.y + partial_fog_distance, world_map.max_y())
23 |
24 | grid = {}
25 | for x in range(lower_x, upper_x + 1):
26 | for y in range(lower_y, upper_y + 1):
27 | cell_location = Location(x, y)
28 | if world_map.is_on_map(cell_location):
29 | x_dist = abs(cell_location.x - location.x)
30 | y_dist = abs(cell_location.y - location.y)
31 | if should_partially_fog(no_fog_distance, partial_fog_distance, x_dist, y_dist):
32 | grid[location] = partially_fog_cell(world_map.get_cell(cell_location))
33 | else:
34 | grid[location] = world_map.get_cell(cell_location)
35 | return WorldMap(grid, world_map.settings)
36 |
37 |
38 | def should_partially_fog(no_fog_distance, partial_fog_distance, x_dist, y_dist):
39 | return x_dist > no_fog_distance or y_dist > no_fog_distance
40 |
41 |
42 | def partially_fog_cell(cell):
43 | partially_fogged_cell = copy.deepcopy(cell)
44 | partially_fogged_cell.habitable = True
45 | partially_fogged_cell.avatar = None
46 | partially_fogged_cell.pickup = None
47 | partially_fogged_cell.partially_fogged = True
48 | return partially_fogged_cell
49 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_simulation/test_pickups.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import abc
4 | from unittest import TestCase
5 |
6 | from simulation import effects, pickups
7 | from .dummy_avatar import DummyAvatar
8 | from .maps import MockCell
9 |
10 |
11 | class _BaseCases(object):
12 | class BasePickupTestCase(TestCase):
13 | __metaclass__ = abc.ABCMeta
14 |
15 | def setUp(self):
16 | self.avatar = DummyAvatar(1, None)
17 | self.cell = MockCell()
18 | self.pickup = self.pickup_class(self.cell)
19 |
20 | def apply_pickup(self):
21 | self.pickup.apply(self.avatar)
22 |
23 | def test_pickup_removed(self):
24 | self.apply_pickup()
25 | self.assertIs(self.cell.pickup, None)
26 |
27 | @abc.abstractproperty
28 | def pickup_class(self):
29 | pass
30 |
31 | class BasePickupEffectTestCase(BasePickupTestCase):
32 | __metaclass__ = abc.ABCMeta
33 |
34 | @abc.abstractproperty
35 | def effect_class(self):
36 | pass
37 |
38 | def test_effect_added(self):
39 | self.apply_pickup()
40 | self.assertEqual(len(self.avatar.effects), 1)
41 | self.assertIsInstance(list(self.avatar.effects)[0], self.effect_class)
42 |
43 |
44 | class TestHealthPickup(_BaseCases.BasePickupTestCase):
45 | pickup_class = pickups.HealthPickup
46 |
47 | def test_health_increases(self):
48 | self.apply_pickup()
49 | self.assertEqual(self.avatar.health, 8)
50 |
51 | def test_serialise(self):
52 | self.assertEqual(self.pickup.serialise(), {'type': 'health', 'health_restored': 3})
53 |
54 |
55 | class TestInvulnerabilityPickup(_BaseCases.BasePickupEffectTestCase):
56 | pickup_class = pickups.InvulnerabilityPickup
57 | effect_class = effects.InvulnerabilityPickupEffect
58 |
59 | def test_serialise(self):
60 | self.assertEqual(self.pickup.serialise(), {'type': 'invulnerability'})
61 |
62 |
63 | class TestDamagePickup(_BaseCases.BasePickupEffectTestCase):
64 | pickup_class = pickups.DamagePickup
65 | effect_class = effects.DamagePickupEffect
66 |
67 | def test_serialise(self):
68 | self.assertEqual(self.pickup.serialise(), {'type': 'damage', 'damage_boost': 5})
69 |
--------------------------------------------------------------------------------
/aimmo-game-worker/simulation/world_map.py:
--------------------------------------------------------------------------------
1 | from .avatar_state import AvatarState
2 | from .location import Location
3 |
4 |
5 | class Cell(object):
6 |
7 | """
8 | Any position on the world grid.
9 | """
10 |
11 | def __init__(self, location, avatar=None, **kwargs):
12 | self.location = Location(**location)
13 | if avatar:
14 | self.avatar = AvatarState(**avatar)
15 | for (key, value) in kwargs.items():
16 | setattr(self, key, value)
17 |
18 | def __repr__(self):
19 | return 'Cell({} h={} s={} a={} p={})'.format(
20 | self.location,
21 | getattr(self, 'habitable', 0),
22 | self.generates_score,
23 | getattr(self, 'avatar', 0),
24 | getattr(self, 'pickup', 0))
25 |
26 | def __eq__(self, other):
27 | return self.location == other.location
28 |
29 | def __ne__(self, other):
30 | return not self == other
31 |
32 |
33 | class WorldMap(object):
34 |
35 | """
36 | The non-player world state.
37 | """
38 |
39 | def __init__(self, cells):
40 | self.cells = {}
41 | for cell_data in cells:
42 | cell = Cell(**cell_data)
43 | self.cells[cell.location] = cell
44 |
45 | def all_cells(self):
46 | return self.cells.values()
47 |
48 | def score_cells(self):
49 | return (c for c in self.all_cells() if c.generates_score)
50 |
51 | def pickup_cells(self):
52 | return (c for c in self.all_cells() if getattr(c, 'pickup', False))
53 |
54 | def partially_fogged_cells(self):
55 | return (c for c in self.all_cells() if c.partially_fogged)
56 |
57 | def is_visible(self, location):
58 | return location in self.cells
59 |
60 | def get_cell(self, location):
61 | cell = self.cells[location]
62 | assert cell.location == location, 'location lookup mismatch: arg={}, found={}'.format(
63 | location, cell.location)
64 | return cell
65 |
66 | def can_move_to(self, target_location):
67 | try:
68 | cell = self.get_cell(target_location)
69 | except KeyError:
70 | return False
71 | return getattr(cell, 'habitable', False) and not getattr(cell, 'avatar', False)
72 |
73 | def __repr__(self):
74 | return repr(self.cells)
75 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/avatar/avatar_manager.py:
--------------------------------------------------------------------------------
1 | from simulation.avatar.avatar_wrapper import AvatarWrapper
2 | from simulation.avatar.avatar_appearance import AvatarAppearance
3 | import copy
4 |
5 | class AvatarManager(object):
6 | """
7 | Stores all game avatars. An avatar can belong to on of the following three lists:
8 | - avatars_by_id: If the avatar is on the game.
9 | - created_avatars_by_id: If the avatar has ust been created. The avatar is put in
10 | this list at first then when the client know about it it's but to avatars_by_id.
11 | - deleted_avatars_by_id: Before completely removing the avatar from the list, the
12 | avatar is put in this list and then, when the client knows that it has to delete
13 | it from the scene, it's removed completely.
14 | """
15 |
16 | def __init__(self):
17 | self.avatars_by_id = {}
18 | self.avatars_to_create_by_id = {}
19 | self.avatars_to_delete_by_id = {}
20 |
21 | def add_avatar(self, player_id, worker_url, location):
22 | avatar = AvatarWrapper(player_id, location, worker_url, AvatarAppearance("#000", "#ddd", "#777", "#fff"))
23 | self.avatars_to_create_by_id[player_id] = avatar
24 | return avatar
25 |
26 | def get_avatar(self, user_id):
27 | return self.avatars_by_id[user_id]
28 |
29 | def remove_avatar(self, user_id):
30 | if user_id in self.avatars_by_id:
31 | self.avatars_to_delete_by_id[user_id] = copy.deepcopy(self.avatars_by_id[user_id])
32 | del self.avatars_by_id[user_id]
33 |
34 | @property
35 | def avatars(self):
36 | return self.avatars_by_id.viewvalues()
37 |
38 | # Returns the newly created avatars and then puts them in the normal avatars list.
39 | def avatars_to_create(self):
40 | avatars_to_create_array = list(self.avatars_to_create_by_id.viewvalues())[:]
41 | self.avatars_by_id.update(dict(self.avatars_to_create_by_id))
42 | self.avatars_to_create_by_id = {}
43 |
44 | return avatars_to_create_array
45 |
46 | # Returns the avatars that need to be removed from the scene.
47 | def avatars_to_delete(self):
48 | avatars_to_delete_array = list(self.avatars_to_delete_by_id.viewvalues())[:]
49 | self.avatars_to_delete_by_id = {}
50 |
51 | return avatars_to_delete_array
52 |
53 | @property
54 | def active_avatars(self):
55 | return [player for player in self.avatars]
56 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/pickups.py:
--------------------------------------------------------------------------------
1 | from abc import ABCMeta, abstractmethod, abstractproperty
2 | import effects
3 |
4 |
5 | class _Pickup(object):
6 | __metaclass__ = ABCMeta
7 |
8 | def __init__(self, cell):
9 | self.cell = cell
10 |
11 | def __str__(self):
12 | return self.__class__.__name__
13 |
14 | def delete(self):
15 | self.cell.pickup = None
16 |
17 | def apply(self, avatar):
18 | self._apply(avatar)
19 | self.delete()
20 |
21 | @abstractmethod
22 | def _apply(self, avatar):
23 | raise NotImplementedError()
24 |
25 | @abstractmethod
26 | def serialise(self):
27 | raise NotImplementedError()
28 |
29 |
30 | class HealthPickup(_Pickup):
31 | def __init__(self, cell, health_restored=3):
32 | super(HealthPickup, self).__init__(cell)
33 | self.health_restored = health_restored
34 |
35 | def __repr__(self):
36 | return 'HealthPickup(health_restored={})'.format(self.health_restored)
37 |
38 | def serialise(self):
39 | return {
40 | 'type': 'health',
41 | 'health_restored': self.health_restored,
42 | }
43 |
44 | def _apply(self, avatar):
45 | avatar.health += self.health_restored
46 |
47 |
48 | class _PickupEffect(_Pickup):
49 | __metaclass__ = ABCMeta
50 |
51 | def __init__(self, *args):
52 | super(_PickupEffect, self).__init__(*args)
53 | self.params = []
54 |
55 | @abstractproperty
56 | def EFFECT(self):
57 | raise NotImplementedError()
58 |
59 | def _apply(self, avatar):
60 | self.params.append(avatar)
61 | avatar.effects.add(self.EFFECT(*self.params))
62 |
63 |
64 | class InvulnerabilityPickup(_PickupEffect):
65 | EFFECT = effects.InvulnerabilityPickupEffect
66 |
67 | def serialise(self):
68 | return {
69 | 'type': 'invulnerability',
70 | }
71 |
72 |
73 | class DamagePickup(_PickupEffect):
74 | EFFECT = effects.DamagePickupEffect
75 |
76 | def __init__(self, *args):
77 | super(DamagePickup, self).__init__(*args)
78 | self.damage_boost = 5
79 | self.params.append(self.damage_boost)
80 |
81 | def __repr__(self):
82 | return 'DamagePickup(damage_boost={})'.format(self.damage_boost)
83 |
84 | def serialise(self):
85 | return {
86 | 'type': 'damage',
87 | 'damage_boost': self.damage_boost,
88 | }
89 |
90 |
91 | ALL_PICKUPS = (
92 | HealthPickup,
93 | InvulnerabilityPickup,
94 | DamagePickup,
95 | )
96 |
--------------------------------------------------------------------------------
/example_project/example_project/wsgi.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Code for Life
3 | #
4 | # Copyright (C) 2015, Ocado Innovation Limited
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU Affero General Public License as
8 | # published by the Free Software Foundation, either version 3 of the
9 | # License, or (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU Affero General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU Affero General Public License
17 | # along with this program. If not, see .
18 | #
19 | # ADDITIONAL TERMS – Section 7 GNU General Public Licence
20 | #
21 | # This licence does not grant any right, title or interest in any “Ocado” logos,
22 | # trade names or the trademark “Ocado” or any other trademarks or domain names
23 | # owned by Ocado Innovation Limited or the Ocado group of companies or any other
24 | # distinctive brand features of “Ocado” as may be secured from time to time. You
25 | # must not distribute any modification of this program using the trademark
26 | # “Ocado” or claim any affiliation or association with Ocado or its employees.
27 | #
28 | # You are not authorised to use the name Ocado (or any of its trade names) or
29 | # the names of any author or contributor in advertising or for publicity purposes
30 | # pertaining to the distribution of this program, without the prior written
31 | # authorisation of Ocado.
32 | #
33 | # Any propagation, distribution or conveyance of this program must include this
34 | # copyright notice and these terms. You must not misrepresent the origins of this
35 | # program; modified versions of the program must be marked as such and not
36 | # identified as the original program.
37 | """
38 | WSGI config for example_project project.
39 |
40 | This module contains the WSGI application used by Django's development server
41 | and any production WSGI deployments. It should expose a module-level variable
42 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
43 | this application via the ``WSGI_APPLICATION`` setting.
44 |
45 | Usually you will have the standard Django WSGI application here, but it also
46 | might make sense to replace the whole Django WSGI application with a custom one
47 | that later delegates to the Django one. For example, you could introduce WSGI
48 | middleware here, or combine a Django application with an application of another
49 | framework.
50 |
51 | """
52 | import os
53 |
54 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
55 |
56 | from django.core.wsgi import get_wsgi_application
57 | application = get_wsgi_application()
58 |
--------------------------------------------------------------------------------
/players/autoconfig.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Code for Life
3 | #
4 | # Copyright (C) 2015, Ocado Innovation Limited
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU Affero General Public License as
8 | # published by the Free Software Foundation, either version 3 of the
9 | # License, or (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU Affero General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU Affero General Public License
17 | # along with this program. If not, see .
18 | #
19 | # ADDITIONAL TERMS – Section 7 GNU General Public Licence
20 | #
21 | # This licence does not grant any right, title or interest in any “Ocado” logos,
22 | # trade names or the trademark “Ocado” or any other trademarks or domain names
23 | # owned by Ocado Innovation Limited or the Ocado group of companies or any other
24 | # distinctive brand features of “Ocado” as may be secured from time to time. You
25 | # must not distribute any modification of this program using the trademark
26 | # “Ocado” or claim any affiliation or association with Ocado or its employees.
27 | #
28 | # You are not authorised to use the name Ocado (or any of its trade names) or
29 | # the names of any author or contributor in advertising or for publicity purposes
30 | # pertaining to the distribution of this program, without the prior written
31 | # authorisation of Ocado.
32 | #
33 | # Any propagation, distribution or conveyance of this program must include this
34 | # copyright notice and these terms. You must not misrepresent the origins of this
35 | # program; modified versions of the program must be marked as such and not
36 | # identified as the original program.
37 | '''Players autoconfig'''
38 |
39 | DEFAULT_SETTINGS = {
40 | 'AUTOCONFIG_INDEX_VIEW': 'aimmo/home',
41 | 'STATIC_URL': '/static/',
42 | }
43 |
44 | SETTINGS = {
45 | 'INSTALLED_APPS': [
46 | 'django.contrib.auth',
47 | 'django.contrib.messages',
48 | 'django.contrib.staticfiles',
49 | 'django_js_reverse',
50 | ],
51 | 'TEMPLATES': [
52 | {
53 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
54 | 'APP_DIRS': True,
55 | 'OPTIONS': {
56 | 'context_processors': [
57 | 'django.template.context_processors.debug',
58 | 'django.template.context_processors.request',
59 | 'django.contrib.auth.context_processors.auth',
60 | 'django.contrib.messages.context_processors.messages',
61 | ]
62 | }
63 | }
64 | ],
65 | 'USE_TZ': True,
66 | }
67 |
--------------------------------------------------------------------------------
/aimmo-game/service.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import cPickle as pickle
3 | import logging
4 | import os
5 | import sys
6 |
7 | import eventlet
8 |
9 | eventlet.sleep()
10 | eventlet.monkey_patch()
11 |
12 | import flask
13 | from flask_socketio import SocketIO
14 |
15 | from simulation.turn_manager import state_provider
16 | from simulation import map_generator
17 | from simulation.avatar.avatar_manager import AvatarManager
18 | from simulation.turn_manager import ConcurrentTurnManager
19 | from simulation.worker_manager import WORKER_MANAGERS
20 | from simulation.world_state import WorldState
21 |
22 | app = flask.Flask(__name__)
23 | socketio = SocketIO()
24 |
25 | worker_manager = None
26 |
27 | # Every user has its own world state.
28 | world_state_manager = {}
29 |
30 | @socketio.on('connect')
31 | def world_init():
32 | socketio.emit('world-init')
33 |
34 | @socketio.on('disconnect')
35 | def exit_game():
36 | del world_state_manager[flask.session['id']]
37 |
38 | @socketio.on('client-ready')
39 | def client_ready(client_id):
40 | flask.session['id'] = client_id
41 | world_state = WorldState(state_provider)
42 | world_state_manager[client_id] = world_state
43 |
44 | def send_world_update():
45 | for world_state in world_state_manager.values():
46 | socketio.emit(
47 | 'world-update',
48 | world_state.get_updates(),
49 | broadcast=True,
50 | )
51 |
52 | @app.route('/')
53 | def healthcheck():
54 | return 'HEALTHY'
55 |
56 | @app.route('/player/')
57 | def player_data(player_id):
58 | player_id = int(player_id)
59 | return flask.jsonify({
60 | 'code': worker_manager.get_code(player_id),
61 | 'options': {}, # Game options
62 | 'state': None,
63 | })
64 |
65 | def run_game(port):
66 | global worker_manager
67 |
68 | print("Running game...")
69 | settings = pickle.loads(os.environ['settings'])
70 |
71 | api_url = os.environ.get('GAME_API_URL', 'http://localhost:8000/players/api/games/')
72 | generator = getattr(map_generator, settings['GENERATOR'])(settings)
73 | player_manager = AvatarManager()
74 | game_state = generator.get_game_state(player_manager)
75 |
76 | turn_manager = ConcurrentTurnManager(game_state=game_state, end_turn_callback=send_world_update, completion_url=api_url+'complete/')
77 | WorkerManagerClass = WORKER_MANAGERS[os.environ.get('WORKER_MANAGER', 'local')]
78 | worker_manager = WorkerManagerClass(game_state=game_state, users_url=api_url, port=port)
79 |
80 | worker_manager.start()
81 | turn_manager.start()
82 |
83 |
84 | if __name__ == '__main__':
85 | logging.basicConfig(level=logging.INFO)
86 |
87 | socketio.init_app(app, resource=os.environ.get('SOCKETIO_RESOURCE', 'socket.io'))
88 |
89 | run_game(int(sys.argv[2]))
90 |
91 | socketio.run(
92 | app,
93 | debug=False,
94 | host=sys.argv[1],
95 | port=int(sys.argv[2]),
96 | use_reloader=False,
97 | )
98 |
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import logging
3 | import os
4 | import signal
5 | import subprocess
6 | import sys
7 | import time
8 | import traceback
9 | from subprocess import CalledProcessError
10 |
11 | _SCRIPT_LOCATION = os.path.abspath(os.path.dirname(__file__))
12 | _MANAGE_PY = os.path.join(_SCRIPT_LOCATION, 'example_project', 'manage.py')
13 | _SERVICE_PY = os.path.join(_SCRIPT_LOCATION, 'aimmo-game-creator', 'service.py')
14 |
15 |
16 | if __name__ == '__main__':
17 | logging.basicConfig()
18 | sys.path.append(os.path.join(_SCRIPT_LOCATION, 'example_project'))
19 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
20 |
21 |
22 | def log(message):
23 | print >> sys.stderr, message
24 |
25 |
26 | def run_command(args, capture_output=False):
27 | try:
28 | if capture_output:
29 | return subprocess.check_output(args)
30 | else:
31 | subprocess.check_call(args)
32 | except CalledProcessError as e:
33 | log('Command failed with exit status %d: %s' % (e.returncode, ' '.join(args)))
34 | raise
35 |
36 |
37 | PROCESSES = []
38 |
39 |
40 | def run_command_async(args):
41 | p = subprocess.Popen(args)
42 | PROCESSES.append(p)
43 | return p
44 |
45 |
46 | def create_superuser_if_missing(username, password):
47 | from django.contrib.auth.models import User
48 | try:
49 | User.objects.get_by_natural_key(username)
50 | except User.DoesNotExist:
51 | log('Creating superuser %s with password %s' % (username, password))
52 | User.objects.create_superuser(username=username, email='admin@admin.com', password=password)
53 |
54 |
55 | def main(use_minikube):
56 |
57 | run_command(['pip', 'install', '-e', _SCRIPT_LOCATION])
58 | run_command(['python', _MANAGE_PY, 'migrate', '--noinput'])
59 | run_command(['python', _MANAGE_PY, 'collectstatic', '--noinput'])
60 |
61 | create_superuser_if_missing(username='admin', password='admin')
62 |
63 | server_args = []
64 | if use_minikube:
65 | # Import minikube here, so we can install the deps first
66 | run_command(['pip', 'install', '-r', os.path.join(_SCRIPT_LOCATION, 'minikube_requirements.txt')])
67 | import minikube
68 |
69 | minikube.start()
70 | server_args.append('0.0.0.0:8000')
71 | os.environ['AIMMO_MODE'] = 'minikube'
72 | else:
73 | time.sleep(2)
74 | game = run_command_async(['python', _SERVICE_PY, '127.0.0.1', '5000'])
75 | os.environ['AIMMO_MODE'] = 'threads'
76 | server = run_command_async(['python', _MANAGE_PY, 'runserver'] + server_args)
77 |
78 | try:
79 | game.wait()
80 | except NameError:
81 | pass
82 | server.wait()
83 |
84 |
85 | if __name__ == '__main__':
86 | try:
87 | main('--kube' in sys.argv or '-k' in sys.argv)
88 | except Exception as err:
89 | traceback.print_exc()
90 | raise
91 | finally:
92 | os.killpg(0, signal.SIGTERM)
93 | time.sleep(0.9)
94 | os.killpg(0, signal.SIGKILL)
95 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_simulation/dummy_avatar.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from simulation.action import MoveAction, WaitAction
4 | from simulation.avatar.avatar_manager import AvatarManager
5 | from simulation.avatar.avatar_wrapper import AvatarWrapper
6 | from simulation.direction import NORTH, EAST, SOUTH, WEST
7 |
8 |
9 | class DummyAvatar(AvatarWrapper):
10 | def __init__(self, player_id=1, initial_location=(0, 0)):
11 | # TODO: extract avatar state and state-altering methods into a new class.
12 | # The new class is to be shared between DummyAvatarRunner and AvatarRunner
13 | super(DummyAvatar, self).__init__(player_id, initial_location, None, None)
14 | self.times_died = 0
15 | self.attack_strength = 1
16 | self.effects = set()
17 | self.resistance = 0
18 |
19 | def decide_action(self, state_view):
20 | self._action = self.handle_turn(state_view)
21 | return True
22 |
23 | def handle_turn(self, state_view):
24 | raise NotImplementedError()
25 |
26 | def add_event(self, event):
27 | self.events.append(event)
28 |
29 | def die(self, respawn_loc):
30 | self.location = respawn_loc
31 | self.times_died += 1
32 |
33 | def serialise(self):
34 | return 'Dummy'
35 |
36 | def damage(self, amount):
37 | self.health -= amount
38 | return amount
39 |
40 |
41 | class WaitDummy(DummyAvatar):
42 | '''
43 | Avatar that always waits.
44 | '''
45 | def handle_turn(self, state_view):
46 | return WaitAction(self)
47 |
48 |
49 | class MoveDummy(DummyAvatar):
50 | '''
51 | Avatar that always moves in one direction.
52 | '''
53 | def __init__(self, player_id, initial_location, direction):
54 | super(MoveDummy, self).__init__(player_id, initial_location)
55 | self._direction = direction
56 |
57 | def handle_turn(self, state_view):
58 | return MoveAction(self, self._direction.dict)
59 |
60 |
61 | class MoveNorthDummy(MoveDummy):
62 | def __init__(self, player_id, initial_location):
63 | super(MoveNorthDummy, self).__init__(player_id, initial_location, NORTH)
64 |
65 |
66 | class MoveEastDummy(MoveDummy):
67 | def __init__(self, player_id, initial_location):
68 | super(MoveEastDummy, self).__init__(player_id, initial_location, EAST)
69 |
70 |
71 | class MoveSouthDummy(MoveDummy):
72 | def __init__(self, player_id, initial_location):
73 | super(MoveSouthDummy, self).__init__(player_id, initial_location, SOUTH)
74 |
75 |
76 | class MoveWestDummy(MoveDummy):
77 | def __init__(self, player_id, initial_location):
78 | super(MoveWestDummy, self).__init__(player_id, initial_location, WEST)
79 |
80 |
81 | class DummyAvatarManager(AvatarManager):
82 | def __init__(self, dummy_list=[]):
83 | super(DummyAvatarManager, self).__init__()
84 | self.dummy_list = dummy_list
85 |
86 | def add_avatar(self, player_id, worker_url, location):
87 | try:
88 | dummy = self.dummy_list.pop(0)
89 | except IndexError:
90 | dummy = WaitDummy
91 | self.avatars_by_id[player_id] = dummy(player_id, location)
92 | return self.avatars_by_id[player_id]
93 |
94 | def add_avatar_directly(self, avatar):
95 | self.avatars_by_id[avatar.player_id] = avatar
96 |
--------------------------------------------------------------------------------
/players/static/js/program.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | $( document ).ready(function() {
4 | //CONSTANTS
5 | const DANGER_CLASS = 'alert-danger';
6 | const SUCCESS_CLASS = 'alert-success';
7 | const defaultProgram = "print 'Sorry, could not retrieve saved data'\n";
8 |
9 |
10 | const StatusCode = Object.freeze({
11 | "SUCCESS": function(message) { showAlert('Success:
' + message, SUCCESS_CLASS); },
12 | "SERVER_ERROR": function(message) { showAlert(message, DANGER_CLASS); },
13 | "USER_ERROR": function(message) { showAlert('Your code has some problems:
' + message, DANGER_CLASS); },
14 | });
15 |
16 | //HELPER FUNCTIONS
17 | function showAlert (alertString, alertType) {
18 | if (alertType === DANGER_CLASS || alertType === SUCCESS_CLASS) {
19 | var alertText = $('#alerts');
20 | alertText.removeClass('alert-success alert-danger');
21 | alertText.addClass(alertType);
22 | alertText.html(alertString + '');
23 | $(".close").click(function(){
24 | alertText.hide();
25 | });
26 | alertText.show();
27 | }
28 | }
29 |
30 | // trigger extension
31 | ace.require("ace/ext/language_tools");
32 | var editor = ace.edit("editor");
33 | editor.setTheme("ace/theme/monokai");
34 | editor.getSession().setMode("ace/mode/python");
35 | // enable autocompletion and snippets
36 | editor.setOptions({
37 | enableBasicAutocompletion: true,
38 | enableSnippets: true,
39 | enableLiveAutocompletion: true
40 | });
41 |
42 | var checkStatus = function(data) {
43 | if (data != undefined && StatusCode[data.status] != undefined) {
44 | return StatusCode[data.status](data.message);
45 | } else {
46 | return showAlert('An unknown error has occurred whilst saving:', DANGER_CLASS);
47 | }
48 | };
49 |
50 | //EVENTS
51 | $('#saveBtn').click(function(event) {
52 | event.preventDefault();
53 | $.ajax({
54 | url: Urls['aimmo/code'](id=GAME_ID),
55 | type: 'POST',
56 | dataType: 'json',
57 | data: {code: editor.getValue(), csrfmiddlewaretoken: $('#saveForm input[name=csrfmiddlewaretoken]').val()},
58 | success: function(data) {
59 | $('#alerts').hide();
60 | checkStatus(data);
61 | },
62 | error: function(jqXHR, textStatus, errorThrown) {
63 | showAlert('An error has occurred whilst saving: ' + errorThrown, DANGER_CLASS);
64 | }
65 | });
66 | });
67 |
68 | //DISPLAY CODE
69 | $.ajax({
70 | url: Urls['aimmo/code'](id=GAME_ID),
71 | type: 'GET',
72 | dataType: 'text',
73 | success: function(data) {
74 | editor.setValue(data);
75 | editor.selection.moveCursorFileStart();
76 | editor.setReadOnly(false);
77 | },
78 | error: function(jqXHR, textStatus, errorThrown) {
79 | showAlert('Could not retrieve saved data', DANGER_CLASS);
80 | editor.setValue(defaultProgram);
81 | editor.selection.moveCursorFileStart();
82 | editor.setReadOnly(true);
83 | }
84 | });
85 |
86 | });
87 |
88 |
89 |
--------------------------------------------------------------------------------
/players/migrations/0003_auto_20160802_1418.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.conf import settings
5 | from django.db import migrations, models, transaction, IntegrityError
6 |
7 | import players.models
8 |
9 |
10 | def migrate_data_forward(apps, schema_editor):
11 | Player = apps.get_model("players", "Player")
12 | Avatar = apps.get_model("players", "Avatar")
13 | Game = apps.get_model("players", "Game")
14 |
15 | if Player.objects.count() == 0:
16 | return
17 | main_game = Game(pk=1, name="main")
18 | main_game.save()
19 |
20 | avatars = [Avatar(game=main_game, owner=player.user, code=player.code)
21 | for player in Player.objects.all()]
22 | Avatar.objects.bulk_create(avatars)
23 |
24 |
25 | def migrate_data_backward(apps, schema_editor):
26 | Player = apps.get_model("players", "Player")
27 | Avatar = apps.get_model("players", "Avatar")
28 |
29 | if Avatar.objects.count() == 0:
30 | return
31 | for avatar in Avatar.objects.all():
32 | player = Player(user=avatar.owner, code=avatar.code)
33 | try:
34 | with transaction.atomic():
35 | player.save()
36 | except IntegrityError:
37 | # Stuff doesn't map as can have more than one Avatar but only one Player
38 | pass
39 |
40 |
41 | class Migration(migrations.Migration):
42 |
43 | dependencies = [
44 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
45 | ('players', '0002_auto_20160601_1914'),
46 | ]
47 |
48 | operations = [
49 | migrations.CreateModel(
50 | name='Avatar',
51 | fields=[
52 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
53 | ('code', models.TextField()),
54 | ('auth_token', models.CharField(default=players.models.generate_auth_token, max_length=24)),
55 | ],
56 | ),
57 | migrations.CreateModel(
58 | name='Game',
59 | fields=[
60 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
61 | ('name', models.CharField(max_length=100)),
62 | ('auth_token', models.CharField(default=players.models.generate_auth_token, max_length=24)),
63 | ('public', models.BooleanField(default=True)),
64 | ('can_play', models.ManyToManyField(related_name='playable_games', to=settings.AUTH_USER_MODEL)),
65 | ('owner', models.ForeignKey(related_name='owned_games', to=settings.AUTH_USER_MODEL, null=True)),
66 | ],
67 | ),
68 | migrations.AddField(
69 | model_name='avatar',
70 | name='game',
71 | field=models.ForeignKey(to='players.Game'),
72 | ),
73 | migrations.AddField(
74 | model_name='avatar',
75 | name='owner',
76 | field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
77 | ),
78 | migrations.AlterUniqueTogether(
79 | name='avatar',
80 | unique_together=set([('owner', 'game')]),
81 | ),
82 | migrations.RunPython(
83 | migrate_data_forward,
84 | migrate_data_backward
85 | ),
86 | migrations.RemoveField(
87 | model_name='player',
88 | name='user',
89 | ),
90 | migrations.DeleteModel(
91 | name='Player',
92 | ),
93 | ]
94 |
--------------------------------------------------------------------------------
/aimmo-game-creator/tests/test_worker_manager.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import cPickle as pickle
4 | import unittest
5 | from json import dumps
6 |
7 | from httmock import HTTMock
8 |
9 | from worker_manager import WorkerManager
10 |
11 |
12 | class ConcreteWorkerManager(WorkerManager):
13 | def __init__(self, *args, **kwargs):
14 | self.final_workers = set()
15 | self.clear()
16 | super(ConcreteWorkerManager, self).__init__(*args, **kwargs)
17 |
18 | def clear(self):
19 | self.removed_workers = []
20 | self.added_workers = {}
21 |
22 | def create_worker(self, game_id, data):
23 | self.added_workers[game_id] = data
24 | self.final_workers.add(game_id)
25 |
26 | def remove_worker(self, game_id):
27 | self.removed_workers.append(game_id)
28 | try:
29 | self.final_workers.remove(game_id)
30 | except KeyError:
31 | pass
32 |
33 |
34 | class RequestMock(object):
35 | def __init__(self, num_games):
36 | self.value = self._generate_response(num_games)
37 | self.urls_requested = []
38 |
39 | def _generate_response(self, num_games):
40 | return {
41 | str(i): {
42 | 'name': 'Game %s' % i,
43 | 'settings': pickle.dumps({
44 | 'test': i,
45 | 'test2': 'Settings %s' % i,
46 | })
47 | } for i in xrange(num_games)
48 | }
49 |
50 | def __call__(self, url, request):
51 | self.urls_requested.append(url.geturl())
52 | return dumps(self.value)
53 |
54 |
55 | class TestWorkerManager(unittest.TestCase):
56 | def setUp(self):
57 | self.worker_manager = ConcreteWorkerManager('http://test/')
58 |
59 | def test_correct_url_requested(self):
60 | mocker = RequestMock(0)
61 | with HTTMock(mocker):
62 | self.worker_manager.update()
63 | self.assertEqual(len(mocker.urls_requested), 1)
64 | self.assertRegexpMatches(mocker.urls_requested[0], 'http://test/*')
65 |
66 | def test_workers_added(self):
67 | mocker = RequestMock(3)
68 | with HTTMock(mocker):
69 | self.worker_manager.update()
70 | self.assertEqual(len(self.worker_manager.final_workers), 3)
71 | self.assertEqual(len(list(self.worker_manager._data.get_games())), 3)
72 | for i in xrange(3):
73 | self.assertIn(str(i), self.worker_manager.final_workers)
74 | self.assertEqual(
75 | pickle.loads(str(self.worker_manager.added_workers[str(i)]['settings'])),
76 | {'test': i, 'test2': 'Settings %s' % i}
77 | )
78 | self.assertEqual(self.worker_manager.added_workers[str(i)]['name'], 'Game %s' % i)
79 |
80 | def test_remove_games(self):
81 | mocker = RequestMock(3)
82 | with HTTMock(mocker):
83 | self.worker_manager.update()
84 | del mocker.value['1']
85 | self.worker_manager.update()
86 | self.assertNotIn(1, self.worker_manager.final_workers)
87 |
88 | def test_added_workers_given_correct_url(self):
89 | mocker = RequestMock(3)
90 | with HTTMock(mocker):
91 | self.worker_manager.update()
92 | for i in xrange(3):
93 | self.assertEqual(
94 | self.worker_manager.added_workers[str(i)]['GAME_API_URL'],
95 | 'http://test/{}/'.format(i)
96 | )
97 | self.assertEqual(self.worker_manager.added_workers[str(i)]['name'], 'Game %s' % i)
98 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_simulation/maps.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from collections import defaultdict
4 |
5 | from simulation.location import Location
6 | from simulation.world_map import Cell, WorldMap
7 |
8 |
9 | class MockPickup(object):
10 | def __init__(self, name='', cell=None):
11 | self.applied_to = None
12 | self.name = name
13 | self.cell = None
14 |
15 | def apply(self, avatar):
16 | self.applied_to = avatar
17 | if self.cell:
18 | self.cell.pickup = None
19 |
20 | def serialise(self):
21 | return {'name': self.name}
22 |
23 |
24 | class MockCell(Cell):
25 | def __init__(self, location=1, habitable=True, generates_score=False,
26 | avatar=None, pickup=None, name=None, actions=[]):
27 | self.location = location
28 | self.habitable = habitable
29 | self.generates_score = generates_score
30 | self.avatar = avatar
31 | self.pickup = pickup
32 | self.name = name
33 | self.actions = actions
34 | self.partially_fogged = False
35 |
36 | def __eq__(self, other):
37 | return self is other
38 |
39 |
40 | class InfiniteMap(WorldMap):
41 | def __init__(self):
42 | self._cell_cache = {}
43 | [self.get_cell(Location(x, y)) for x in range(5) for y in range(5)]
44 | self.updates = 0
45 | self.num_avatars = None
46 | self.settings = defaultdict(lambda: 0)
47 |
48 | def is_on_map(self, target_location):
49 | self.get_cell(target_location)
50 | return True
51 |
52 | def all_cells(self):
53 | return (cell for cell in self._cell_cache.values())
54 |
55 | def get_cell(self, location):
56 | return self._cell_cache.setdefault(location, Cell(location))
57 |
58 | def update(self, num_avatars):
59 | self.updates += 1
60 | self.num_avatars = num_avatars
61 |
62 | @property
63 | def num_rows(self):
64 | return float('inf')
65 |
66 | @property
67 | def num_cols(self):
68 | return float('inf')
69 |
70 |
71 | class EmptyMap(WorldMap):
72 | def __init__(self):
73 | pass
74 |
75 | def get_random_spawn_location(self):
76 | return Location(10, 10)
77 |
78 | def can_move_to(self, target_location):
79 | return False
80 |
81 | def all_cells(self):
82 | return iter(())
83 |
84 | def get_cell(self, location):
85 | return Cell(location)
86 |
87 |
88 | class ScoreOnOddColumnsMap(InfiniteMap):
89 | def get_cell(self, location):
90 | default_cell = Cell(location, generates_score=(location.x % 2 == 1))
91 | return self._cell_cache.setdefault(location, default_cell)
92 |
93 |
94 | class AvatarMap(WorldMap):
95 | def __init__(self, avatar):
96 | self._avatar = avatar
97 | self._cell_cache = {}
98 |
99 | def get_cell(self, location):
100 | if location not in self._cell_cache:
101 | cell = Cell(location)
102 | cell.avatar = self._avatar
103 | self._cell_cache[location] = cell
104 | return self._cell_cache[location]
105 |
106 | def get_random_spawn_location(self):
107 | return Location(10, 10)
108 |
109 |
110 | class PickupMap(WorldMap):
111 | def __init__(self, pickup):
112 | self._pickup = pickup
113 |
114 | def get_cell(self, location):
115 | cell = Cell(location)
116 | cell.pickup = self._pickup
117 | return cell
118 |
--------------------------------------------------------------------------------
/players/models.py:
--------------------------------------------------------------------------------
1 | from base64 import urlsafe_b64encode
2 | from os import urandom
3 |
4 | from django.contrib.auth.models import User
5 | from django.db import models
6 |
7 | from players import app_settings
8 |
9 | GAME_GENERATORS = [
10 | ('Main', 'Open World'), # Default
11 | ] + [('Level%s' % i, 'Level %s' % i) for i in xrange(1, app_settings.MAX_LEVEL+1)]
12 |
13 |
14 | def generate_auth_token():
15 | return urlsafe_b64encode(urandom(16))
16 |
17 |
18 | class GameQuerySet(models.QuerySet):
19 | def for_user(self, user):
20 | if user.is_authenticated():
21 | return self.filter(models.Q(public=True) | models.Q(can_play=user))
22 | else:
23 | return self.filter(public=True)
24 |
25 | def exclude_inactive(self):
26 | return self.filter(completed=False)
27 |
28 |
29 | class Game(models.Model):
30 | name = models.CharField(max_length=100)
31 | auth_token = models.CharField(max_length=24, default=generate_auth_token)
32 | owner = models.ForeignKey(User, blank=True, null=True, related_name='owned_games')
33 | public = models.BooleanField(default=True)
34 | can_play = models.ManyToManyField(User, related_name='playable_games')
35 | completed = models.BooleanField(default=False)
36 | main_user = models.ForeignKey(User, blank=True, null=True, related_name='games_for_user')
37 | objects = GameQuerySet.as_manager()
38 | static_data = models.TextField(blank=True, null=True)
39 |
40 | # Game config
41 | generator = models.CharField(max_length=20, choices=GAME_GENERATORS, default=GAME_GENERATORS[0][0])
42 | target_num_cells_per_avatar = models.FloatField(default=16)
43 | target_num_score_locations_per_avatar = models.FloatField(default=0.5)
44 | score_despawn_chance = models.FloatField(default=0.02)
45 | target_num_pickups_per_avatar = models.FloatField(default=0.5)
46 | pickup_spawn_chance = models.FloatField(default=0.02)
47 | obstacle_ratio = models.FloatField(default=0.1)
48 | start_height = models.IntegerField(default=11)
49 | start_width = models.IntegerField(default=11)
50 |
51 | @property
52 | def is_active(self):
53 | return not self.completed
54 |
55 | def can_user_play(self, user):
56 | return self.public or user in self.can_play.all()
57 |
58 | def settings_as_dict(self):
59 | return {
60 | 'TARGET_NUM_CELLS_PER_AVATAR': self.target_num_cells_per_avatar,
61 | 'TARGET_NUM_SCORE_LOCATIONS_PER_AVATAR': self.target_num_score_locations_per_avatar,
62 | 'SCORE_DESPAWN_CHANCE': self.score_despawn_chance,
63 | 'TARGET_NUM_PICKUPS_PER_AVATAR': self.target_num_pickups_per_avatar,
64 | 'PICKUP_SPAWN_CHANCE': self.pickup_spawn_chance,
65 | 'OBSTACLE_RATIO': self.obstacle_ratio,
66 | 'START_HEIGHT': self.start_height,
67 | 'START_WIDTH': self.start_width,
68 | 'GENERATOR': self.generator,
69 | }
70 |
71 | def save(self, *args, **kwargs):
72 | super(Game, self).full_clean()
73 | super(Game, self).save(*args, **kwargs)
74 |
75 |
76 | class Avatar(models.Model):
77 | owner = models.ForeignKey(User)
78 | game = models.ForeignKey(Game)
79 | code = models.TextField()
80 | auth_token = models.CharField(max_length=24, default=generate_auth_token)
81 |
82 | class Meta:
83 | unique_together = ('owner', 'game')
84 |
85 |
86 | class LevelAttempt(models.Model):
87 | level_number = models.IntegerField()
88 | user = models.ForeignKey(User)
89 | game = models.OneToOneField(Game)
90 |
91 | class Meta:
92 | unique_together = ('level_number', 'user')
93 |
--------------------------------------------------------------------------------
/players/templates/players/base.html:
--------------------------------------------------------------------------------
1 | {% load players_utils %}
2 |
3 |
4 |
5 |
6 |
7 |
8 | {# The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags #}
9 |
10 | AI:MMO
11 |
12 |
13 | {# TODO: favicon #}
14 |
15 | {# bootstrap #}
16 | {# Latest compiled and minified CSS #}
17 |
18 |
19 | {# Optional theme #}
20 |
21 |
22 | {% block styles %}
23 | {% endblock %}
24 |
25 |
26 |
27 |
54 |
55 | {% block content %}
56 | {% endblock %}
57 |
58 |
59 |
60 |
61 | {# Latest compiled and minified JavaScript #}
62 |
63 |
64 |
65 |
66 | {# HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries #}
67 |
71 | {% block scripts %}
72 | {% endblock %}
73 |
74 |
75 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/avatar/avatar_wrapper.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import requests
3 |
4 | from simulation.action import ACTIONS, MoveAction, WaitAction
5 |
6 |
7 | LOGGER = logging.getLogger(__name__)
8 |
9 |
10 | class AvatarWrapper(object):
11 | """
12 | The application's view of a character, not to be confused with "Avatar",
13 | the player-supplied code.
14 | """
15 |
16 | def __init__(self, player_id, initial_location, worker_url, avatar_appearance):
17 | self.player_id = player_id
18 | self.location = initial_location
19 | self.health = 5
20 | self.score = 0
21 | self.events = []
22 | self.avatar_appearance = avatar_appearance
23 | self.worker_url = worker_url
24 | self.effects = set()
25 | self.resistance = 0
26 | self.attack_strength = 1
27 | self.fog_of_war_modifier = 0
28 | self._action = None
29 |
30 | def update_effects(self):
31 | effects_to_remove = set()
32 | for effect in self.effects:
33 | effect.on_turn()
34 | if effect.is_expired:
35 | effects_to_remove.add(effect)
36 | for effect in effects_to_remove:
37 | effect.remove()
38 |
39 | @property
40 | def action(self):
41 | return self._action
42 |
43 | @property
44 | def is_moving(self):
45 | return isinstance(self.action, MoveAction)
46 |
47 | def _fetch_action(self, state_view):
48 | return requests.post(self.worker_url, json=state_view).json()
49 |
50 | def _construct_action(self, data):
51 | action_data = data['action']
52 | action_type = action_data['action_type']
53 | action_args = action_data.get('options', {})
54 | action_args['avatar'] = self
55 | return ACTIONS[action_type](**action_args)
56 |
57 | def decide_action(self, state_view):
58 | try:
59 | data = self._fetch_action(state_view)
60 | action = self._construct_action(data)
61 |
62 | except (KeyError, ValueError) as err:
63 | LOGGER.info('Bad action data supplied: %s', err)
64 | except requests.exceptions.ConnectionError:
65 | LOGGER.info('Could not connect to worker, probably not ready yet')
66 | except Exception:
67 | LOGGER.exception("Unknown error while fetching turn data")
68 |
69 | else:
70 | self._action = action
71 | return True
72 |
73 | self._action = WaitAction(self)
74 | return False
75 |
76 | def clear_action(self):
77 | self._action = None
78 |
79 | def die(self, respawn_location):
80 | # TODO: extract settings for health and score loss on death
81 | self.health = 5
82 | self.score = max(0, self.score - 2)
83 | self.location = respawn_location
84 |
85 | def add_event(self, event):
86 | self.events.append(event)
87 |
88 | def damage(self, amount):
89 | applied_dmg = max(0, amount - self.resistance)
90 | self.health -= applied_dmg
91 | return applied_dmg
92 |
93 | def serialise(self):
94 | return {
95 | 'events': [
96 | # {
97 | # 'event_name': event.__class__.__name__.lower(),
98 | # 'event_options': event.__dict__,
99 | # } for event in self.events
100 | ],
101 | 'health': self.health,
102 | 'location': self.location.serialise(),
103 | 'score': self.score,
104 | }
105 |
106 | def __repr__(self):
107 | return 'Avatar(id={}, location={}, health={}, score={})'.format(self.player_id, self.location, self.health, self.score)
108 |
--------------------------------------------------------------------------------
/players/management/commands/generate_players.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import os
3 | import uuid
4 |
5 | from django.core.management import BaseCommand
6 | from django.test import RequestFactory
7 | from importlib import import_module
8 | from django.conf import settings
9 | from django.contrib.auth.models import User
10 | from players.views import code as code_view
11 |
12 |
13 | def _nth_dirname(path, n):
14 | for _ in xrange(n):
15 | path = os.path.dirname(path)
16 | return path
17 |
18 | _PLAYERS_DIRECTORY = _nth_dirname(__file__, 3)
19 | _AVATAR_CODES_DIRECTORY = os.path.join(_PLAYERS_DIRECTORY, 'avatar_examples')
20 |
21 | # Code file listing
22 |
23 |
24 | def _strip_prefix(prefix, string):
25 | if string.startswith(prefix):
26 | return string[len(prefix):]
27 |
28 |
29 | def _get_available_code_files(base_directory):
30 | for dirpath, dirnames, filenames in os.walk(base_directory):
31 | for f in filenames:
32 | if not f.startswith('_') and f.endswith('.py'):
33 | parent_dir = _strip_prefix(base_directory, dirpath)
34 | yield os.path.join(parent_dir, f)
35 |
36 | _AVATAR_CODES = list(_get_available_code_files(_AVATAR_CODES_DIRECTORY))
37 |
38 | # Code file loading
39 |
40 |
41 | def _load_code_file(filename):
42 | if not filename.endswith('.py'):
43 | filename = filename + '.py'
44 |
45 | filepath = os.path.join(_AVATAR_CODES_DIRECTORY, filename)
46 |
47 | with open(filepath) as f:
48 | return f.read()
49 |
50 |
51 | class LoadCodeAction(argparse.Action):
52 |
53 | def __init__(self, option_strings, dest, **kwargs):
54 | super(LoadCodeAction, self).__init__(option_strings, dest, **kwargs)
55 |
56 | def __call__(self, parser, namespace, values, option_string):
57 | values = _load_code_file(values)
58 | setattr(namespace, self.dest, values)
59 |
60 |
61 | class Command(BaseCommand):
62 | # Show this when the user types help
63 | help = "Generate users for the game"
64 |
65 | def __init__(self, *args, **kwargs):
66 | super(Command, self).__init__(*args, **kwargs)
67 | self.engine = import_module(settings.SESSION_ENGINE)
68 | self.request_factory = RequestFactory()
69 |
70 | def add_arguments(self, parser):
71 | parser.add_argument('num-users', type=int,
72 | help='Number of users to create')
73 | parser.add_argument('avatar-code', choices=_AVATAR_CODES,
74 | action=LoadCodeAction,
75 | help='The code to use for the avatar.')
76 |
77 | # A command must define handle()
78 | def handle(self, *args, **options):
79 | num_users = options['num-users']
80 | code = options['avatar-code']
81 |
82 | for _ in xrange(num_users):
83 | random_string = str(uuid.uuid4())[:8]
84 | username = 'zombie-%s' % random_string
85 | password = '123'
86 | user = self.create_user(username, password)
87 | self.post_code(user, code)
88 |
89 | def create_user(self, username, password):
90 | user = User.objects.create_user(username, 'user@example.com', password)
91 | self.stdout.write('Created user %s with password: %s' % (username, password))
92 | return user
93 |
94 | def post_code(self, user, player_code):
95 | request = self.request_factory.post('/any_path', data={'code': player_code})
96 | session_key = None
97 | request.session = self.engine.SessionStore(session_key)
98 | request.user = user
99 |
100 | response = code_view(request)
101 | if response.status_code == 200:
102 | self.stdout.write('Posted code for player %s' % user)
103 | else:
104 | raise Exception('Failed to submit code for player %s' % user)
105 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_simulation/test_worker_manager.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import unittest
4 | from json import dumps
5 |
6 | from httmock import HTTMock
7 |
8 | from simulation.avatar.avatar_manager import AvatarManager
9 | from simulation.game_state import GameState
10 | from simulation.worker_manager import WorkerManager
11 | from .maps import InfiniteMap
12 |
13 |
14 | class ConcreteWorkerManager(WorkerManager):
15 | def __init__(self, *args, **kwargs):
16 | self.final_workers = set()
17 | self.clear()
18 | super(ConcreteWorkerManager, self).__init__(*args, **kwargs)
19 |
20 | def clear(self):
21 | self.removed_workers = []
22 | self.added_workers = []
23 |
24 | def create_worker(self, player_id):
25 | self.added_workers.append(player_id)
26 | self.final_workers.add(player_id)
27 |
28 | def remove_worker(self, player_id):
29 | self.removed_workers.append(player_id)
30 | try:
31 | self.final_workers.remove(player_id)
32 | except KeyError:
33 | pass
34 |
35 |
36 | class RequestMock(object):
37 | def __init__(self, num_users):
38 | self.value = self._generate_response(num_users)
39 | self.urls_requested = []
40 |
41 | def _generate_response(self, num_users):
42 | return {
43 | 'main': {
44 | 'parameters': [],
45 | 'main_avatar': None,
46 | 'users': [{
47 | 'id': i,
48 | 'code': 'code for %s' % i,
49 | } for i in xrange(num_users)]
50 | }
51 | }
52 |
53 | def change_code(self, id, new_code):
54 | users = self.value['main']['users']
55 | for i in xrange(len(users)):
56 | if users[i]['id'] == id:
57 | users[i]['code'] = new_code
58 |
59 | def __call__(self, url, request):
60 | self.urls_requested.append(url.geturl())
61 | return dumps(self.value)
62 |
63 |
64 | class TestWorkerManager(unittest.TestCase):
65 | def setUp(self):
66 | self.game_state = GameState(InfiniteMap(), AvatarManager())
67 | self.worker_manager = ConcreteWorkerManager(self.game_state, 'http://test')
68 |
69 | def test_correct_url(self):
70 | mocker = RequestMock(0)
71 | with HTTMock(mocker):
72 | self.worker_manager.update()
73 | self.assertEqual(len(mocker.urls_requested), 1)
74 | self.assertRegexpMatches(mocker.urls_requested[0], 'http://test/*')
75 |
76 | def test_workers_added(self):
77 | mocker = RequestMock(3)
78 | with HTTMock(mocker):
79 | self.worker_manager.update()
80 | self.assertEqual(len(self.worker_manager.final_workers), 3)
81 | for i in xrange(3):
82 | self.assertIn(i, self.game_state.avatar_manager.avatars_to_create_by_id)
83 | self.assertIn(i, self.worker_manager.final_workers)
84 | self.assertEqual(self.worker_manager.get_code(i), 'code for %s' % i)
85 |
86 | def test_changed_code(self):
87 | mocker = RequestMock(4)
88 | with HTTMock(mocker):
89 | self.worker_manager.update()
90 | mocker.change_code(0, 'changed 0')
91 | mocker.change_code(2, 'changed 2')
92 | self.worker_manager.update()
93 |
94 | for i in xrange(4):
95 | self.assertIn(i, self.worker_manager.final_workers)
96 | self.assertIn(i, self.game_state.avatar_manager.avatars_to_create_by_id)
97 |
98 | for i in (1, 3):
99 | self.assertEqual(self.worker_manager.get_code(i), 'code for %s' % i)
100 | for i in (0, 2):
101 | self.assertIn(i, self.worker_manager.added_workers)
102 | self.assertIn(i, self.worker_manager.removed_workers)
103 | self.assertEqual(self.worker_manager.get_code(i), 'changed %s' % i)
104 |
105 | def test_remove_avatars(self):
106 | mocker = RequestMock(3)
107 | with HTTMock(mocker):
108 | self.worker_manager.update()
109 | del mocker.value['main']['users'][1]
110 | self.worker_manager.update()
111 | self.assertNotIn(1, self.worker_manager.final_workers)
112 | self.assertNotIn(1, self.game_state.avatar_manager.avatars_to_delete_by_id)
113 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/world_state.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from collections import defaultdict
3 |
4 | class MapFeature(Enum):
5 | HEALTH_POINT = 'health_point'
6 | SCORE_POINT = 'score_point'
7 | PICKUP = 'pickup'
8 | OBSTACLE = 'obstacle'
9 |
10 | class WorldState():
11 | """
12 | The world updates class serves as a buffer between the updates generated
13 | in different parts of the back-end and the data emitted by the socket.
14 |
15 | It contains all the parameters that need to be sent to the client every
16 | time the world is updated. These are:
17 | - Player (aka avatars) creation, deletion and update.
18 | - Changes in map features, i.e. creation and deletion of:
19 | * Health points.
20 | * Score points.
21 | * Pickups.
22 | * Obstacles.
23 | """
24 |
25 | def __init__(self, game_state):
26 | self.game_state = game_state
27 | self.players = defaultdict(dict)
28 | self.map_features = defaultdict(dict)
29 | self.clear_updates()
30 |
31 | def get_updates(self):
32 | self.refresh()
33 | updates = {
34 | 'players' : dict(self.players),
35 | 'map_features' : dict(self.map_features)
36 | }
37 | self.clear_updates()
38 |
39 | return updates
40 |
41 | def clear_updates(self):
42 | self.players = {
43 | 'update': [],
44 | 'create': [],
45 | 'delete': []
46 | }
47 | for map_feature in MapFeature:
48 | self.map_features[map_feature.value] = {
49 | 'create': [],
50 | 'delete': []
51 | }
52 |
53 | # Player updates.
54 |
55 | def create_player(self, player_data):
56 | # Player data: {id, x, y, rotation, health, score, appearance?}
57 | self.players["create"].append(player_data)
58 |
59 | def delete_player(self, player_id):
60 | # Player id: {id}
61 | self.players["delete"].append(player_id)
62 |
63 | def update_player(self, player_update):
64 | # Player_update: {id, x, y, rotation, health, score}
65 | self.players["update"].append(player_update)
66 |
67 | # Map features updates.
68 |
69 | def create_map_feature(self, map_feature, map_feature_data):
70 | self.map_features[map_feature]["create"].append(map_feature_data)
71 |
72 | def delete_map_feature(self, map_feature, map_feature_id):
73 | self.map_features[map_feature]["delete"].append(map_feature_id)
74 |
75 | # Refresh the world state. Basically gather information from the avatar manager
76 | # and the world map and organise it.
77 |
78 | def refresh(self):
79 | def player_dict(avatar):
80 | return {
81 | 'id' : avatar.player_id,
82 | 'x' : avatar.location.x,
83 | 'y' : avatar.location.y,
84 | 'score' : avatar.score,
85 | 'health': avatar.health
86 | }
87 |
88 | def map_feature_dict(map_feature):
89 | return {
90 | 'id' : hash(map_feature),
91 | 'x' : map_feature.location.x,
92 | 'y' : map_feature.location.y
93 | }
94 |
95 | with self.game_state as game_state:
96 | world = game_state.world_map
97 |
98 | # Refresh players dictionary.
99 |
100 | for player in game_state.avatar_manager.avatars:
101 | self.update_player(player_dict(player))
102 |
103 | for player in game_state.avatar_manager.avatars_to_create():
104 | self.create_player(player_dict(player))
105 |
106 | for player in game_state.avatar_manager.avatars_to_delete():
107 | self.delete_player(player_dict(player))
108 |
109 | # Refresh map features dictionary.
110 |
111 | for cell in world.cells_to_create():
112 |
113 | # Cell is an obstacle.
114 | if not cell.habitable:
115 | self.create_map_feature(MapFeature.OBSTACLE.value, map_feature_dict(cell))
116 | cell.created = True
117 |
118 | # Cell is a score point.
119 | if cell.generates_score:
120 | self.create_map_feature(MapFeature.SCORE_POINT.value, map_feature_dict(cell))
121 | cell.created = True
122 |
--------------------------------------------------------------------------------
/example_project/example_project/settings.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Code for Life
3 | #
4 | # Copyright (C) 2015, Ocado Innovation Limited
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU Affero General Public License as
8 | # published by the Free Software Foundation, either version 3 of the
9 | # License, or (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU Affero General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU Affero General Public License
17 | # along with this program. If not, see .
18 | #
19 | # ADDITIONAL TERMS – Section 7 GNU General Public Licence
20 | #
21 | # This licence does not grant any right, title or interest in any “Ocado” logos,
22 | # trade names or the trademark “Ocado” or any other trademarks or domain names
23 | # owned by Ocado Innovation Limited or the Ocado group of companies or any other
24 | # distinctive brand features of “Ocado” as may be secured from time to time. You
25 | # must not distribute any modification of this program using the trademark
26 | # “Ocado” or claim any affiliation or association with Ocado or its employees.
27 | #
28 | # You are not authorised to use the name Ocado (or any of its trade names) or
29 | # the names of any author or contributor in advertising or for publicity purposes
30 | # pertaining to the distribution of this program, without the prior written
31 | # authorisation of Ocado.
32 | #
33 | # Any propagation, distribution or conveyance of this program must include this
34 | # copyright notice and these terms. You must not misrepresent the origins of this
35 | # program; modified versions of the program must be marked as such and not
36 | # identified as the original program.
37 | """Django settings for example_project project."""
38 | import subprocess
39 |
40 | import os
41 |
42 | ALLOWED_HOSTS = ['*']
43 |
44 | DEBUG = True
45 |
46 | DATABASES = {
47 | 'default': {
48 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
49 | 'NAME': os.path.join(os.path.abspath(os.path.dirname(__file__)), 'db.sqlite3'), # Or path to database file if using sqlite3.
50 | }
51 | }
52 |
53 | USE_I18N = True
54 | USE_L10N = True
55 |
56 | TIME_ZONE = 'Europe/London'
57 | LANGUAGE_CODE = 'en-gb'
58 | STATIC_ROOT = os.path.join(os.path.dirname(__file__), 'static')
59 | STATIC_URL = '/static/'
60 | SECRET_KEY = 'not-a-secret'
61 |
62 | ROOT_URLCONF = 'django_autoconfig.autourlconf'
63 |
64 | WSGI_APPLICATION = 'example_project.wsgi.application'
65 |
66 | INSTALLED_APPS = (
67 | 'django.contrib.admin',
68 | 'django.contrib.auth',
69 | 'django.contrib.contenttypes',
70 | 'players',
71 | 'django_forms_bootstrap',
72 | )
73 |
74 | LOGGING = {
75 | 'version': 1,
76 | 'disable_existing_loggers': False,
77 | 'filters': {
78 | 'require_debug_false': {
79 | '()': 'django.utils.log.RequireDebugFalse'
80 | }
81 | },
82 | 'handlers': {
83 | 'console': {
84 | 'level': 'DEBUG',
85 | 'class': 'logging.StreamHandler'
86 | },
87 | },
88 | 'loggers': {
89 | 'views': {
90 | 'handlers': ['console'],
91 | 'level': 'DEBUG'
92 | },
93 | }
94 | }
95 |
96 | LOGIN_URL = '/players/accounts/login/'
97 |
98 | LOGIN_REDIRECT_URL = '/players/'
99 |
100 | MIDDLEWARE_CLASSES = [
101 | 'django.contrib.sessions.middleware.SessionMiddleware',
102 | 'django.middleware.locale.LocaleMiddleware',
103 | 'django.middleware.common.CommonMiddleware',
104 | 'django.middleware.csrf.CsrfViewMiddleware',
105 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
106 | 'django.contrib.messages.middleware.MessageMiddleware',
107 | ]
108 |
109 | def get_url(game):
110 | if os.environ.get('AIMMO_MODE', '') == 'minikube':
111 | return (os.environ['MINIKUBE_PROXY_URL'], "/game/%s/socket.io" % game)
112 | else:
113 | return ('http://localhost:%d' % (6001 + int(game) * 1000), '/socket.io')
114 |
115 | AIMMO_GAME_SERVER_LOCATION_FUNCTION = get_url
116 |
117 | try:
118 | from example_project.local_settings import * # pylint: disable=E0611
119 | except ImportError:
120 | pass
121 |
122 | from django_autoconfig import autoconfig
123 | autoconfig.configure_settings(globals())
124 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/turn_manager.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import time
3 | from threading import RLock
4 | from threading import Thread
5 |
6 | from simulation.action import PRIORITIES
7 |
8 | LOGGER = logging.getLogger(__name__)
9 |
10 |
11 | class GameStateProvider:
12 | """
13 | Thread-safe container for the world state.
14 |
15 | TODO: think about changing to snapshot rather than lock?
16 | """
17 |
18 | def __init__(self):
19 | self._game_state = None
20 | self._lock = RLock()
21 |
22 | def __enter__(self):
23 | self._lock.acquire()
24 | return self._game_state
25 |
26 | def __exit__(self, type, value, traceback):
27 | self._lock.release()
28 |
29 | def set_world(self, new_game_state):
30 | self._lock.acquire()
31 | self._game_state = new_game_state
32 | self._lock.release()
33 |
34 |
35 | state_provider = GameStateProvider()
36 |
37 |
38 | class TurnManager(Thread):
39 | """
40 | Game loop
41 | """
42 | daemon = True
43 |
44 | def __init__(self, game_state, end_turn_callback, completion_url):
45 | state_provider.set_world(game_state)
46 | self.end_turn_callback = end_turn_callback
47 | self._completion_url = completion_url
48 | super(TurnManager, self).__init__()
49 |
50 | def run_turn(self):
51 | raise NotImplementedError("Abstract method.")
52 |
53 | def _register_action(self, avatar):
54 | """
55 | Send an avatar its view of the game state and register its chosen action.
56 | """
57 | with state_provider as game_state:
58 | state_view = game_state.get_state_for(avatar)
59 |
60 | if avatar.decide_action(state_view):
61 | with state_provider as game_state:
62 | avatar.action.register(game_state.world_map)
63 |
64 | def _update_environment(self, game_state):
65 | num_avatars = len(game_state.avatar_manager.active_avatars)
66 | game_state.world_map.reconstruct_interactive_state(num_avatars)
67 |
68 | def _mark_complete(self):
69 | pass
70 | # TODO: Make completion request work. For now, we assume games don't finish.
71 | #from world_state import WorldState
72 | #requests.post(self._completion_url, json=world_state.get_update())
73 |
74 | def run(self):
75 | while True:
76 | try:
77 | self.run_turn()
78 |
79 | with state_provider as game_state:
80 | game_state.update_environment()
81 |
82 | self.end_turn_callback()
83 | except Exception:
84 | LOGGER.exception('Error while running turn')
85 |
86 | if game_state.is_complete():
87 | LOGGER.info('Game complete')
88 | self._mark_complete()
89 | time.sleep(0.5)
90 |
91 |
92 | class SequentialTurnManager(TurnManager):
93 | def run_turn(self):
94 | """
95 | Get and apply each avatar's action in turn.
96 | """
97 | with state_provider as game_state:
98 | avatars = game_state.avatar_manager.active_avatars
99 |
100 | for avatar in avatars:
101 | self._register_action(avatar)
102 | with state_provider as game_state:
103 | location_to_clear = avatar.action.target_location
104 | avatar.action.process(game_state.world_map)
105 | game_state.world_map.clear_cell_actions(location_to_clear)
106 |
107 |
108 | class ConcurrentTurnManager(TurnManager):
109 | def run_turn(self):
110 | """
111 | Concurrently get the intended actions from all avatars and register
112 | them on the world map. Then apply actions in order of priority.
113 | """
114 | with state_provider as game_state:
115 | avatars = game_state.avatar_manager.active_avatars
116 |
117 | threads = [Thread(target=self._register_action,
118 | args=(avatar,)) for avatar in avatars]
119 |
120 | [thread.start() for thread in threads]
121 | [thread.join() for thread in threads]
122 |
123 | # Waits applied first, then attacks, then moves.
124 | avatars.sort(key=lambda a: PRIORITIES[type(a.action)])
125 |
126 | locations_to_clear = {a.action.target_location for a in avatars
127 | if a.action is not None}
128 |
129 | for action in (a.action for a in avatars if a.action is not None):
130 | with state_provider as game_state:
131 | action.process(game_state.world_map)
132 |
133 | for location in locations_to_clear:
134 | with state_provider as game_state:
135 | game_state.world_map.clear_cell_actions(location)
136 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_simulation/test_game_state.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from unittest import TestCase
4 |
5 | from simulation.game_state import GameState
6 | from simulation.location import Location
7 | from .dummy_avatar import DummyAvatar
8 | from .dummy_avatar import DummyAvatarManager
9 | from .maps import InfiniteMap, AvatarMap, EmptyMap
10 |
11 |
12 | class FogToEmpty(object):
13 | def apply_fog_of_war(self, world_map, wrapper):
14 | return EmptyMap()
15 |
16 |
17 | class TestGameState(TestCase):
18 | def test_remove_non_existant_avatar(self):
19 | state = GameState(None, DummyAvatarManager())
20 | state.remove_avatar(10)
21 |
22 | def test_remove_avatar(self):
23 | world_map = InfiniteMap()
24 | manager = DummyAvatarManager()
25 | game_state = GameState(world_map, manager)
26 |
27 | avatar1 = DummyAvatar(1, Location(0, 0))
28 | avatar2 = DummyAvatar(2, Location(1, 1))
29 | avatar2.marked = True
30 |
31 | manager.add_avatar_directly(avatar1)
32 | world_map.get_cell(Location(0, 0)).avatar = avatar1
33 | manager.add_avatar_directly(avatar2)
34 | world_map.get_cell(Location(1, 1)).avatar = avatar2
35 |
36 | game_state.remove_avatar(1)
37 |
38 | self.assertNotIn(1, manager.avatars_by_id)
39 | self.assertEqual(world_map.get_cell(Location(0, 0)).avatar, None)
40 |
41 | self.assertTrue(manager.avatars_by_id[2].marked)
42 | self.assertTrue(world_map.get_cell(Location(1, 1)).avatar.marked)
43 |
44 | def game_state_with_two_avatars(self, world_map=None, avatar_manager=None):
45 | if world_map is None:
46 | world_map = EmptyMap()
47 | if avatar_manager is None:
48 | avatar_manager = DummyAvatarManager()
49 |
50 | avatar = DummyAvatar(1, (0, 0))
51 | other_avatar = DummyAvatar(2, (0, 0))
52 | other_avatar.marked = True
53 | avatar_manager.avatars_by_id[1] = avatar
54 | avatar_manager.avatars_by_id[2] = other_avatar
55 | game_state = GameState(world_map, avatar_manager)
56 | return (game_state, avatar, world_map, avatar_manager)
57 |
58 | self.assertTrue(avatar_manager.avatars_by_id[2].marked)
59 | self.assertNotIn(1, avatar_manager.avatars_by_id)
60 | self.assertEqual(world_map.get_cell((0, 0)).avatar, None)
61 |
62 | def test_add_avatar(self):
63 | state = GameState(AvatarMap(None), DummyAvatarManager())
64 | state.add_avatar(7, "")
65 | self.assertIn(7, state.avatar_manager.avatars_by_id)
66 | avatar = state.avatar_manager.avatars_by_id[7]
67 | self.assertEqual(avatar.location.x, 10)
68 | self.assertEqual(avatar.location.y, 10)
69 |
70 | def test_fog_of_war(self):
71 | state = GameState(InfiniteMap(), DummyAvatarManager())
72 | view = state.get_state_for(DummyAvatar(None, None), FogToEmpty())
73 | self.assertEqual(len(view['world_map']['cells']), 0)
74 | self.assertEqual(view['avatar_state'], 'Dummy')
75 |
76 | def test_updates_map(self):
77 | map = InfiniteMap()
78 | state = GameState(map, DummyAvatarManager())
79 | state.update_environment()
80 | self.assertEqual(map.updates, 1)
81 |
82 | def test_updates_map_with_correct_num_avatars(self):
83 | map = InfiniteMap()
84 | manager = DummyAvatarManager()
85 | manager.add_avatar(1, '', None)
86 | state = GameState(map, manager)
87 | state.update_environment()
88 | self.assertEqual(map.num_avatars, 1)
89 | manager.add_avatar(2, '', None)
90 | manager.add_avatar(3, '', None)
91 | state.update_environment()
92 | self.assertEqual(map.num_avatars, 3)
93 |
94 | def test_no_main_avatar_by_default(self):
95 | state = GameState(EmptyMap(), DummyAvatarManager())
96 | with self.assertRaises(KeyError):
97 | state.get_main_avatar()
98 |
99 | def test_get_main_avatar(self):
100 | (game_state, avatar, _, _) = self.game_state_with_two_avatars()
101 | game_state.main_avatar_id = avatar.player_id
102 | self.assertEqual(game_state.get_main_avatar(), avatar)
103 |
104 | def test_is_complete_calls_lambda(self):
105 | class LambdaTest(object):
106 | def __init__(self, return_value):
107 | self.return_value = return_value
108 |
109 | def __call__(self, game_state):
110 | self.game_state = game_state
111 | return self.return_value
112 |
113 | test = LambdaTest(True)
114 | game_state = GameState(EmptyMap(), DummyAvatarManager(), test)
115 | self.assertTrue(game_state.is_complete())
116 | test.return_value = False
117 | self.assertFalse(game_state.is_complete())
118 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_simulation/test_action.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import unittest
4 |
5 | from simulation import action
6 | from simulation import event
7 | from simulation.avatar.avatar_manager import AvatarManager
8 | from simulation.direction import EAST
9 | from simulation.game_state import GameState
10 | from simulation.location import Location
11 | from .dummy_avatar import MoveDummy
12 | from .maps import InfiniteMap, EmptyMap, AvatarMap
13 |
14 | ORIGIN = Location(x=0, y=0)
15 | EAST_OF_ORIGIN = Location(x=1, y=0)
16 | NORTH_OF_ORIGIN = Location(x=0, y=1)
17 |
18 |
19 | class TestAction(unittest.TestCase):
20 | def setUp(self):
21 | self.avatar = MoveDummy(1, ORIGIN, EAST)
22 | self.other_avatar = MoveDummy(2, EAST_OF_ORIGIN, EAST)
23 | self.avatar_manager = AvatarManager()
24 |
25 | def test_successful_move_north_action(self):
26 | game_state = GameState(InfiniteMap(), self.avatar_manager)
27 | action.MoveAction(self.avatar, {'x': 0, 'y': 1}).process(game_state.world_map)
28 |
29 | target_cell = game_state.world_map.get_cell(NORTH_OF_ORIGIN)
30 | self.assertEqual(self.avatar.location, NORTH_OF_ORIGIN)
31 | self.assertEqual(self.avatar, target_cell.avatar)
32 |
33 | self.assertEqual(self.avatar.events, [event.MovedEvent(ORIGIN, NORTH_OF_ORIGIN)])
34 |
35 | def test_successful_move_east_action(self):
36 | game_state = GameState(InfiniteMap(), self.avatar_manager)
37 | action.MoveAction(self.avatar, {'x': 1, 'y': 0}).process(game_state.world_map)
38 |
39 | self.assertEqual(self.avatar.location, EAST_OF_ORIGIN)
40 | self.assertEqual(self.avatar.events, [event.MovedEvent(ORIGIN, EAST_OF_ORIGIN)])
41 |
42 | def test_failed_move_action(self):
43 | game_state = GameState(EmptyMap(), self.avatar_manager)
44 | action.MoveAction(self.avatar, {'x': 0, 'y': 1}).process(game_state.world_map)
45 |
46 | self.assertEqual(self.avatar.location, ORIGIN)
47 | self.assertEqual(self.avatar.events, [event.FailedMoveEvent(ORIGIN, NORTH_OF_ORIGIN)])
48 |
49 | def test_successful_attack_action(self):
50 | game_state = GameState(AvatarMap(self.other_avatar), self.avatar_manager)
51 | action.AttackAction(self.avatar, {'x': 0, 'y': 1}).process(game_state.world_map)
52 |
53 | target_location = NORTH_OF_ORIGIN
54 | damage_dealt = 1
55 |
56 | self.assertEqual(self.avatar.location, ORIGIN)
57 | self.assertEqual(self.other_avatar.location, EAST_OF_ORIGIN)
58 | self.assertEqual(self.other_avatar.times_died, 0)
59 | self.assertEqual(self.other_avatar.health, 4)
60 |
61 | self.assertEqual(self.avatar.events,
62 | [event.PerformedAttackEvent(
63 | self.other_avatar,
64 | target_location,
65 | damage_dealt)])
66 | self.assertEqual(self.other_avatar.events,
67 | [event.ReceivedAttackEvent(self.avatar, damage_dealt)])
68 |
69 | def test_failed_attack_action(self):
70 | game_state = GameState(InfiniteMap(), self.avatar_manager)
71 | action.AttackAction(self.avatar, {'x': 0, 'y': 1}).process(game_state.world_map)
72 |
73 | target_location = NORTH_OF_ORIGIN
74 |
75 | self.assertEqual(self.avatar.location, ORIGIN)
76 | self.assertEqual(self.other_avatar.location, EAST_OF_ORIGIN)
77 | self.assertEqual(self.avatar.events, [event.FailedAttackEvent(target_location)])
78 | self.assertEqual(self.other_avatar.events, [])
79 |
80 | def test_avatar_dies(self):
81 | self.other_avatar.health = 1
82 | game_state = GameState(AvatarMap(self.other_avatar), self.avatar_manager)
83 | action.AttackAction(self.avatar, {'x': 0, 'y': 1}).process(game_state.world_map)
84 |
85 | target_location = NORTH_OF_ORIGIN
86 | damage_dealt = 1
87 | self.assertEqual(self.avatar.events,
88 | [event.PerformedAttackEvent(
89 | self.other_avatar,
90 | target_location,
91 | damage_dealt)])
92 | self.assertEqual(self.other_avatar.events,
93 | [event.ReceivedAttackEvent(self.avatar, damage_dealt)])
94 |
95 | self.assertEqual(self.avatar.location, ORIGIN)
96 | self.assertEqual(self.other_avatar.health, 0)
97 | self.assertEqual(self.other_avatar.times_died, 1)
98 | self.assertEqual(self.other_avatar.location, Location(10, 10))
99 |
100 | def test_no_move_in_wait(self):
101 | game_state = GameState(InfiniteMap(), self.avatar_manager)
102 | action.WaitAction(self.avatar).process(game_state.world_map)
103 | self.assertEqual(self.avatar.location, ORIGIN)
104 |
--------------------------------------------------------------------------------
/aimmo-game-worker/tests/simulation/test_world_map.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from unittest import TestCase
4 |
5 | from simulation.location import Location
6 | from simulation.world_map import WorldMap
7 |
8 |
9 | class TestWorldMap(TestCase):
10 | AVATAR = {'location': {'x': 0, 'y': 0}, 'health': True, 'score': 3, 'events': []}
11 |
12 | def _generate_cells(self, columns=3, rows=3):
13 | cells = [{
14 | 'location': {'x': x, 'y': y},
15 | 'habitable': True,
16 | 'generates_score': False,
17 | 'avatar': None,
18 | 'pickup': None,
19 | } for x in xrange(-columns / 2 + 1, 1 + columns / 2) for y in xrange(-rows / 2 + 1, 1 + rows / 2)]
20 | return cells
21 |
22 | def assertGridSize(self, map, expected_rows, expected_columns=None):
23 | if expected_columns is None:
24 | expected_columns = expected_rows
25 | self.assertEqual(len(list(map.all_cells())), expected_rows*expected_columns)
26 |
27 | def assertLocationsEqual(self, actual_cells, expected_locations):
28 | actual_cells = list(actual_cells)
29 | actual = frozenset(cell.location for cell in actual_cells)
30 | self.assertEqual(actual, frozenset(expected_locations))
31 | self.assertEqual(len(actual_cells), len(list(expected_locations)))
32 |
33 | def test_grid_size(self):
34 | map = WorldMap(self._generate_cells(1, 3))
35 | self.assertGridSize(map, 1, 3)
36 |
37 | def test_all_cells(self):
38 | map = WorldMap(self._generate_cells())
39 | self.assertLocationsEqual(map.all_cells(),
40 | [Location(x, y) for x in xrange(-1, 2) for y in xrange(-1, 2)])
41 |
42 | def test_score_cells(self):
43 | cells = self._generate_cells()
44 | cells[0]['generates_score'] = True
45 | cells[5]['generates_score'] = True
46 | map = WorldMap(cells)
47 | self.assertLocationsEqual(map.score_cells(), (Location(-1, -1), Location(0, 1)))
48 |
49 | def test_pickup_cells(self):
50 | cells = self._generate_cells()
51 | cells[0]['pickup'] = {'health_restored': 5}
52 | cells[8]['pickup'] = {'health_restored': 2}
53 | map = WorldMap(cells)
54 | self.assertLocationsEqual(map.pickup_cells(), (Location(-1, -1), Location(1, 1)))
55 |
56 | def test_location_is_visible(self):
57 | map = WorldMap(self._generate_cells())
58 | for x in (0, 1):
59 | for y in (0, 1):
60 | self.assertTrue(map.is_visible(Location(x, y)))
61 |
62 | def test_x_off_map_is_not_visible(self):
63 | map = WorldMap(self._generate_cells())
64 | for y in (0, 1):
65 | self.assertFalse(map.is_visible(Location(-2, y)))
66 | self.assertFalse(map.is_visible(Location(2, y)))
67 |
68 | def test_y_off_map_is_not_visible(self):
69 | map = WorldMap(self._generate_cells())
70 | for x in (0, 1):
71 | self.assertFalse(map.is_visible(Location(x, -2)))
72 | self.assertFalse(map.is_visible(Location(x, 2)))
73 |
74 | def test_get_valid_cell(self):
75 | map = WorldMap(self._generate_cells())
76 | for x in (0, 1):
77 | for y in (0, 1):
78 | location = Location(x, y)
79 | self.assertEqual(map.get_cell(location).location, location)
80 |
81 | def test_get_x_off_map(self):
82 | map = WorldMap(self._generate_cells())
83 | for y in (0, 1):
84 | with self.assertRaises(KeyError):
85 | map.get_cell(Location(-2, y))
86 | with self.assertRaises(KeyError):
87 | map.get_cell(Location(2, y))
88 |
89 | def test_get_y_off_map(self):
90 | map = WorldMap(self._generate_cells())
91 | for x in (0, 1):
92 | with self.assertRaises(KeyError):
93 | map.get_cell(Location(x, -2))
94 | with self.assertRaises(KeyError):
95 | map.get_cell(Location(x, 2))
96 |
97 | def test_can_move_to(self):
98 | map = WorldMap(self._generate_cells())
99 | target = Location(1, 1)
100 | self.assertTrue(map.can_move_to(target))
101 |
102 | def test_cannot_move_to_cell_off_grid(self):
103 | map = WorldMap(self._generate_cells())
104 | target = Location(4, 1)
105 | self.assertFalse(map.can_move_to(target))
106 |
107 | def test_cannot_move_to_uninhabitable_cell(self):
108 | cells = self._generate_cells()
109 | cells[0]['habitable'] = False
110 | map = WorldMap(cells)
111 | self.assertFalse(map.can_move_to(Location(-1, -1)))
112 |
113 | def test_cannot_move_to_habited_cell(self):
114 | cells = self._generate_cells()
115 | cells[1]['avatar'] = self.AVATAR
116 | map = WorldMap(cells)
117 | self.assertFalse(map.can_move_to(Location(-1, 0)))
118 |
--------------------------------------------------------------------------------
/players/static/js/watch/world-controls.js:
--------------------------------------------------------------------------------
1 | // World Manipulation
2 | const CONTROLS = Object.create({
3 | init: function (world, viewer) {
4 | this.world = world;
5 | this.viewer = viewer;
6 | },
7 | initialiseWorld: function (width, height, worldLayout, minX, minY, maxX, maxY) {
8 | this.world.width = width;
9 | this.world.height = height;
10 | this.world.layout = worldLayout;
11 | this.world.minX = minX;
12 | this.world.minY = minY;
13 | this.world.maxX = maxX;
14 | this.world.maxY = maxY;
15 |
16 | this.viewer.reDrawWorldLayout();
17 | },
18 | processUpdate: function (players, mapFeatures) {
19 | var i, j;
20 |
21 | // Create players.
22 | for (i = 0; i < players["create"].length; i++) {
23 | this.world.players.push(players["create"][i]);
24 | }
25 |
26 | // Delete players.
27 | for (i = 0; i < players["delete"].length; i++) {
28 | for (j = 0; j < this.world.players.length; j++) {
29 | if (this.world.players[j]["id"] === players["delete"][i]["id"]) {
30 | this.world.players.splice(j, 1);
31 | }
32 | }
33 | }
34 |
35 | // Update players.
36 | for (i = 0; i < players["update"].length; i++) {
37 | for (j = 0; j < this.world.players.length; j++) {
38 | if (this.world.players[j]["id"] === players["update"][i]["id"]) {
39 | this.world.players[j] = players["update"][i];
40 | }
41 | }
42 | }
43 |
44 | // Map features.
45 | var obstacles = mapFeatures["obstacle"];
46 | var scorePoints = mapFeatures["score_point"];
47 | var healthPoints = mapFeatures["health_point"];
48 | var pickups = mapFeatures["pickup"];
49 |
50 | // Create obstacles.
51 | for (i = 0; i < obstacles["create"].length; i++) {
52 | this.world.layout[obstacles["create"][i]["x"]][obstacles["create"][i]["y"]] = 1;
53 | }
54 |
55 | // Delete obstacles.
56 | for (i = 0; i < obstacles["delete"].length; i++) {
57 | this.world.layout[obstacles["delete"][i]["x"]][obstacles["delete"][i]["y"]] = 0;
58 | }
59 |
60 | // Create score points.
61 | for (i = 0; i < scorePoints["create"].length; i++) {
62 | this.world.layout[scorePoints["create"][i]["x"]][scorePoints["create"][i]["y"]] = 2;
63 | }
64 |
65 | // Delete score points.
66 | for (i = 0; i < scorePoints["delete"].length; i++) {
67 | this.world.layout[scorePoints["delete"][i]["x"]][scorePoints["delete"][i]["y"]] = 0;
68 | }
69 |
70 | // Create health points.
71 | for (i = 0; i < healthPoints["create"].length; i++) {
72 | this.world.layout[healthPoints["create"][i]["x"]][healthPoints["create"][i]["y"]] = 3;
73 | }
74 |
75 | // Delete health points.
76 | for (i = 0; i < healthPoints["delete"].length; i++) {
77 | this.world.layout[healthPoints["delete"][i]["x"]][healthPoints["delete"][i]["y"]] = 0;
78 | }
79 |
80 | // Create pickups.
81 | for (i = 0; i < pickups["create"].length; i++) {
82 | this.world.pickups.push(pickups["create"][i]);
83 | }
84 |
85 | // Delete pickups.
86 | for (i = 0; i < pickups["delete"].length; i++) {
87 | for (j = 0; j < this.world.pickups.length; j++) {
88 | if (this.world.pickups[j]["id"] === players["delete"][i]["id"]) {
89 | this.world.pickups.splice(j, 1);
90 | }
91 | }
92 | }
93 |
94 | this.viewer.reDrawState();
95 | }
96 | });
97 |
98 | // Updates.
99 | function worldUpdate(data) {
100 | CONTROLS.processUpdate(data["players"], data["map_features"]);
101 | }
102 |
103 | // Initialisation.
104 | function worldInit() {
105 | var width = 15;
106 | var height = 15;
107 | var minX = -7;
108 | var minY = -7;
109 | var maxX = 7;
110 | var maxY = 7;
111 |
112 | var layout = {};
113 | for (var x = minX; x <= maxX; x++) {
114 | layout[x] = {};
115 | for (var y = minY; y <= maxY; y++) {
116 | layout[x][y] = 0;
117 | }
118 | }
119 |
120 | CONTROLS.initialiseWorld(width, height, layout, minX, minY, maxX, maxY);
121 | }
122 |
123 | $(document).ready(function() {
124 |
125 | var world = {};
126 | world.players = [];
127 | world.pickups = [];
128 | VIEWER.init(document.getElementById("watch-world-canvas"), world, APPEARANCE);
129 | CONTROLS.init(world, VIEWER);
130 |
131 | if (ACTIVE) {
132 | var socket = io.connect(GAME_URL_BASE, { path: GAME_URL_PATH });
133 | socket.on('world-init', function() {
134 | worldInit();
135 | socket.emit('client-ready', 1);
136 | });
137 |
138 | socket.on('world-update', function(msg) {
139 | worldUpdate(msg);
140 | });
141 | }
142 | });
143 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/action.py:
--------------------------------------------------------------------------------
1 | from logging import getLogger
2 | from simulation.direction import Direction
3 | from simulation.event import FailedAttackEvent, FailedMoveEvent, MovedEvent, PerformedAttackEvent, ReceivedAttackEvent
4 |
5 | LOGGER = getLogger(__name__)
6 |
7 |
8 | class Action(object):
9 | def __init__(self, avatar):
10 | self._avatar = avatar
11 | try:
12 | self._target_location = self._avatar.location + self.direction
13 | except AttributeError:
14 | self._target_location = self._avatar.location
15 |
16 | @property
17 | def avatar(self):
18 | return self._avatar
19 |
20 | @property
21 | def target_location(self):
22 | return self._target_location
23 |
24 | def register(self, world_map):
25 | if world_map.is_on_map(self.target_location):
26 | world_map.get_cell(self.target_location).actions.append(self)
27 |
28 | def process(self, world_map):
29 | if self.is_legal(world_map):
30 | self.apply(world_map)
31 | else:
32 | self.reject()
33 |
34 | def is_legal(self, world_map):
35 | raise NotImplementedError('Abstract method')
36 |
37 | def apply(self, world_map):
38 | raise NotImplementedError('Abstract method')
39 |
40 | def reject(self):
41 | raise NotImplementedError('Abstract method')
42 |
43 |
44 | class WaitAction(Action):
45 | def __init__(self, avatar):
46 | super(WaitAction, self).__init__(avatar)
47 |
48 | def is_legal(self, world_map):
49 | return True
50 |
51 | def apply(self, world_map):
52 | self.avatar.clear_action()
53 |
54 |
55 | class MoveAction(Action):
56 | def __init__(self, avatar, direction):
57 | # Untrusted data!
58 | self.direction = Direction(**direction)
59 | super(MoveAction, self).__init__(avatar)
60 |
61 | def is_legal(self, world_map):
62 | return world_map.can_move_to(self.target_location)
63 |
64 | def process(self, world_map):
65 | self.chain(world_map, {self.avatar.location})
66 |
67 | def apply(self, world_map):
68 | event = MovedEvent(self.avatar.location, self.target_location)
69 | self.avatar.add_event(event)
70 |
71 | world_map.get_cell(self.avatar.location).avatar = None
72 | self.avatar.location = self.target_location
73 | world_map.get_cell(self.target_location).avatar = self.avatar
74 | self.avatar.clear_action()
75 | return True
76 |
77 | def chain(self, world_map, visited):
78 | if not self.is_legal(world_map):
79 | return self.reject()
80 |
81 | # Detect cycles
82 | if self.target_location in visited:
83 | return self.reject()
84 |
85 | next_cell = world_map.get_cell(self.target_location)
86 | if not next_cell.is_occupied:
87 | return self.apply(world_map)
88 |
89 | next_action = next_cell.avatar.action
90 | if next_action.chain(world_map, visited | {self.target_location}):
91 | return self.apply(world_map)
92 |
93 | return self.reject()
94 |
95 | def reject(self):
96 | event = FailedMoveEvent(self.avatar.location, self.target_location)
97 | self.avatar.add_event(event)
98 | self.avatar.clear_action()
99 | return False
100 |
101 |
102 | class AttackAction(Action):
103 | def __init__(self, avatar, direction):
104 | # Untrusted data!
105 | self.direction = Direction(**direction)
106 | super(AttackAction, self).__init__(avatar)
107 |
108 | def is_legal(self, world_map):
109 | return True if world_map.attackable_avatar(self.target_location) else False
110 |
111 | def apply(self, world_map):
112 | attacked_avatar = world_map.attackable_avatar(self.target_location)
113 | damage_dealt = 1
114 | self.avatar.add_event(PerformedAttackEvent(attacked_avatar,
115 | self.target_location,
116 | damage_dealt))
117 | attacked_avatar.add_event(ReceivedAttackEvent(self.avatar,
118 | damage_dealt))
119 | attacked_avatar.damage(damage_dealt)
120 |
121 | LOGGER.debug('{} dealt {} damage to {}'.format(self.avatar,
122 | damage_dealt,
123 | attacked_avatar))
124 | self.avatar.clear_action()
125 |
126 | if attacked_avatar.health <= 0:
127 | # Move responsibility for this to avatar.die() ?
128 | respawn_location = world_map.get_random_spawn_location()
129 | attacked_avatar.die(respawn_location)
130 | world_map.get_cell(self.target_location).avatar = None
131 | world_map.get_cell(respawn_location).avatar = attacked_avatar
132 |
133 | def reject(self):
134 | self.avatar.add_event(FailedAttackEvent(self.target_location))
135 | self.avatar.clear_action()
136 |
137 | ACTIONS = {
138 | 'attack': AttackAction,
139 | 'move': MoveAction,
140 | 'wait': WaitAction,
141 | }
142 |
143 | PRIORITIES = {
144 | WaitAction: 0,
145 | AttackAction: 1,
146 | MoveAction: 2,
147 | }
148 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_simulation/avatar/test_avatar_wrapper.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import json
4 | from unittest import TestCase
5 |
6 | from httmock import HTTMock
7 |
8 | from simulation.avatar import avatar_wrapper
9 |
10 |
11 | class MockEffect(object):
12 | def __init__(self, avatar):
13 | self.turns = 0
14 | self.is_expired = False
15 | self.removed = False
16 | self.expire = False
17 | self.avatar = avatar
18 |
19 | def on_turn(self):
20 | self.is_expired = self.expire
21 | self.turns += 1
22 |
23 | def remove(self):
24 | self.removed = True
25 | self.avatar.effects.remove(self)
26 |
27 |
28 | class MockAction(object):
29 | def __init__(self, avatar, **options):
30 | global actions_created
31 | self.options = options
32 | self.avatar = avatar
33 | actions_created.append(self)
34 |
35 |
36 | class ActionRequest(object):
37 | def __init__(self, options=None):
38 | if options is None:
39 | options = {}
40 | self.options = options
41 |
42 | def __call__(self, url, request):
43 | return json.dumps({'action': {'action_type': 'test', 'options': self.options}})
44 |
45 |
46 | def InvalidJSONRequest(url, request):
47 | return 'EXCEPTION'
48 |
49 |
50 | def NonExistentActionRequest(url, request):
51 | return json.dumps({'action': {'action_type': 'fake', 'option': {}}})
52 |
53 |
54 | avatar_wrapper.ACTIONS = {
55 | 'test': MockAction
56 | }
57 |
58 |
59 | class TestAvatarWrapper(TestCase):
60 | def setUp(self):
61 | global actions_created
62 | actions_created = []
63 | self.avatar = avatar_wrapper.AvatarWrapper(None, None, 'http://test', None)
64 |
65 | def take_turn(self, request_mock=None):
66 | if request_mock is None:
67 | request_mock = ActionRequest()
68 | with HTTMock(request_mock):
69 | self.avatar.decide_action(None)
70 |
71 | def test_action_has_created_correctly(self):
72 | self.take_turn()
73 | self.assertGreater(len(actions_created), 0, 'No action applied')
74 | self.assertEqual(len(actions_created), 1, 'Too many actions applied')
75 | self.assertEqual(actions_created[0].avatar, self.avatar, 'Action applied on wrong avatar')
76 |
77 | def test_bad_action_data_given(self):
78 | request_mock = InvalidJSONRequest
79 | self.take_turn(request_mock)
80 | self.assertEqual(actions_created, [], 'No action should have been applied')
81 |
82 | def test_non_existant_action(self):
83 | request_mock = NonExistentActionRequest
84 | self.take_turn(request_mock)
85 | self.assertEqual(actions_created, [], 'No action should have been applied')
86 |
87 | def add_effects(self, num=2):
88 | effects = []
89 | for _ in xrange(num):
90 | effect = MockEffect(self.avatar)
91 | self.avatar.effects.add(effect)
92 | effects.append(effect)
93 | return effects
94 |
95 | def test_effects_on_turn_are_called(self):
96 | effect1, effect2 = self.add_effects()
97 | self.avatar.update_effects()
98 | self.assertEqual(effect1.turns, 1)
99 | self.assertEqual(effect2.turns, 1)
100 |
101 | def test_effects_not_removed(self):
102 | effect1, effect2 = self.add_effects()
103 | self.avatar.update_effects()
104 | self.assertEqual(set((effect1, effect2)), self.avatar.effects)
105 |
106 | def test_expired_effects_removed(self):
107 | effect1, effect2 = self.add_effects()
108 | effect1.expire = True
109 | self.avatar.update_effects()
110 | self.assertEqual(effect2.turns, 1)
111 | self.assertEqual(self.avatar.effects, set((effect2,)))
112 |
113 | def test_effects_applied_on_invalid_action(self):
114 | self.take_turn(InvalidJSONRequest)
115 | effect = self.add_effects(1)[0]
116 | self.avatar.update_effects()
117 | self.assertEqual(effect.turns, 1)
118 |
119 | def test_avatar_dies_health(self):
120 | self.avatar.die(None)
121 | self.assertEqual(self.avatar.health, 5)
122 |
123 | def test_avatar_dies_score_when_large(self):
124 | self.avatar.score = 10
125 | self.avatar.die(None)
126 | self.assertEqual(self.avatar.score, 8)
127 |
128 | def test_avatar_dies_score_when_small(self):
129 | self.avatar.score = 1
130 | self.avatar.die(None)
131 | self.assertEqual(self.avatar.score, 0)
132 |
133 | def test_avatar_dies_location(self):
134 | self.avatar.die('test')
135 | self.assertEqual(self.avatar.location, 'test')
136 |
137 | def test_damage_applied(self):
138 | self.avatar.health = 10
139 | self.assertEqual(self.avatar.damage(1), 1)
140 | self.assertEqual(self.avatar.health, 9)
141 |
142 | def test_resistance_reduces_damage(self):
143 | self.avatar.health = 10
144 | self.avatar.resistance = 3
145 | self.assertEqual(self.avatar.damage(5), 2)
146 | self.assertEqual(self.avatar.health, 8)
147 |
148 | def test_no_negative_damage(self):
149 | self.avatar.health = 10
150 | self.avatar.resistance = 3
151 | self.assertEqual(self.avatar.damage(1), 0)
152 | self.assertEqual(self.avatar.health, 10)
153 |
--------------------------------------------------------------------------------
/minikube.py:
--------------------------------------------------------------------------------
1 | #!/user/bin/env python
2 | from __future__ import print_function
3 |
4 | import docker
5 | import errno
6 | import kubernetes
7 | import platform
8 | import os
9 | import re
10 | import socket
11 | import stat
12 | import tarfile
13 | import yaml
14 | from run import run_command
15 | from urllib import urlretrieve
16 | from urllib2 import urlopen
17 | from zipfile import ZipFile
18 |
19 | BASE_DIR = os.path.abspath(os.path.dirname(__file__))
20 | TEST_BIN = os.path.join(BASE_DIR, 'test-bin')
21 | MANIFESTS = os.path.join(BASE_DIR, 'manifests/')
22 | RENDER_MANIFESTS = os.path.join(BASE_DIR, 'render-manifests.py')
23 | OS = platform.system().lower()
24 | FILE_SUFFIX = '.exe' if OS == 'windows' else ''
25 | KUBECTL = os.path.join(TEST_BIN, 'kubectl%s' % FILE_SUFFIX)
26 | MINIKUBE = os.path.join(TEST_BIN, 'minikube%s' % FILE_SUFFIX)
27 |
28 |
29 | def create_test_bin():
30 | try:
31 | os.makedirs(TEST_BIN)
32 | except OSError as err:
33 | if err.errno != errno.EEXIST:
34 | raise
35 |
36 |
37 | def get_latest_github_version(repo):
38 | result = urlopen('https://github.com/%s/releases/latest' % repo)
39 | return result.geturl().split('/')[-1]
40 |
41 |
42 | def download_exec(url, dest):
43 | dest = urlretrieve(url, dest)[0]
44 | make_exec(dest)
45 |
46 |
47 | def make_exec(file):
48 | current_stat = os.stat(file)
49 | os.chmod(file, current_stat.st_mode | stat.S_IEXEC)
50 |
51 |
52 | def binary_exists(filename):
53 | # Check if binary is callable on our path
54 | try:
55 | run_command([filename], True)
56 | return True
57 | except OSError:
58 | return False
59 |
60 |
61 | def download_kubectl():
62 | if binary_exists('kubectl'):
63 | return 'kubectl'
64 | if os.path.isfile(KUBECTL):
65 | return KUBECTL
66 | print('Downloading kubectl...')
67 | version = get_latest_github_version('kubernetes/kubernetes')
68 | url = 'http://storage.googleapis.com/kubernetes-release/release/%s/bin/%s/amd64/kubectl%s' % (version, OS, FILE_SUFFIX)
69 | download_exec(url, KUBECTL)
70 | return KUBECTL
71 |
72 |
73 | def download_minikube():
74 | # First check for the user's installation. Don't break it if they have one
75 | if binary_exists('minikube'):
76 | return 'minikube'
77 |
78 | if os.path.isfile(MINIKUBE):
79 | return MINIKUBE
80 | print('Downloading minikube')
81 | version = get_latest_github_version('kubernetes/minikube')
82 | url = 'https://storage.googleapis.com/minikube/releases/%s/minikube-%s-amd64%s' % (version, OS, FILE_SUFFIX)
83 | download_exec(url, MINIKUBE)
84 | return MINIKUBE
85 |
86 |
87 | def get_ip():
88 | # http://stackoverflow.com/a/28950776/671626
89 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
90 | try:
91 | # doesn't even have to be reachable
92 | s.connect(('10.255.255.255', 0))
93 | IP = s.getsockname()[0]
94 | except:
95 | IP = '127.0.0.1'
96 | finally:
97 | s.close()
98 | return IP
99 |
100 |
101 | def render_manifests():
102 | run_command([RENDER_MANIFESTS, BASE_DIR, 'test', 'http://%s:8000/players/api/games/' % get_ip()])
103 |
104 |
105 | def start_cluster(minikube):
106 | status = run_command([minikube, 'status'], True)
107 | if 'Running' in status:
108 | print('Cluster already running')
109 | else:
110 | run_command([minikube, 'start', '--memory=2048', '--cpus=2'])
111 |
112 |
113 | def build_docker_images(minikube):
114 | print('Building docker images')
115 | raw_env_settings = run_command([minikube, 'docker-env', '--shell="bash"'], True)
116 | matches = re.finditer(r'^export (.+)="(.+)"$', raw_env_settings, re.MULTILINE)
117 | env = dict([(m.group(1), m.group(2)) for m in matches])
118 |
119 | client = docker.from_env(
120 | environment=env,
121 | version='auto',
122 | )
123 |
124 | dirs = ('aimmo-game', 'aimmo-game-creator', 'aimmo-game-worker', 'aimmo-reverse-proxy')
125 | for dir in dirs:
126 | path = os.path.join(BASE_DIR, dir)
127 | tag = 'ocadotechnology/%s:test' % dir
128 | print("Building %s..." % tag)
129 | status = client.build(
130 | decode=True,
131 | path=dir,
132 | tag=tag,
133 | )
134 | for line in status:
135 | if 'stream' in line:
136 | print(line['stream'], end='')
137 |
138 |
139 | def apply_manifests(kubectl):
140 | print('Applying manifests...')
141 | run_command([kubectl, '--context=minikube', 'apply', '-f', MANIFESTS])
142 | print('Deleting existing pods...')
143 | kubernetes.config.load_kube_config(context='minikube')
144 | v1_api = kubernetes.client.CoreV1Api()
145 | for pod in v1_api.list_namespaced_pod('default').items:
146 | v1_api.delete_namespaced_pod(body=kubernetes.client.V1DeleteOptions(), name=pod.metadata.name, namespace='default')
147 |
148 |
149 | def start():
150 | if platform.machine().lower() not in ('amd64', 'x86_64'):
151 | raise ValueError('Requires 64-bit')
152 | create_test_bin()
153 | kubectl = download_kubectl()
154 | minikube = download_minikube()
155 | start_cluster(minikube)
156 | build_docker_images(minikube)
157 | render_manifests()
158 | apply_manifests(kubectl)
159 | os.environ['MINIKUBE_PROXY_URL'] = run_command([minikube, 'service', 'aimmo-reverse-proxy', '--url'], True).strip()
160 | print('Cluster ready')
161 |
--------------------------------------------------------------------------------
/players/views.py:
--------------------------------------------------------------------------------
1 | import cPickle as pickle
2 | import logging
3 | import os
4 |
5 | from django.contrib.auth.decorators import login_required
6 | from django.core.exceptions import ValidationError
7 | from django.core.urlresolvers import reverse
8 | from django.http import HttpResponse, Http404
9 | from django.http import JsonResponse
10 | from django.shortcuts import redirect, render, get_object_or_404
11 | from django.views.decorators.csrf import csrf_exempt
12 | from django.views.decorators.http import require_http_methods
13 | from django.views.generic import TemplateView
14 |
15 | from models import Avatar, Game, LevelAttempt
16 | from players import forms
17 | from . import app_settings
18 |
19 | LOGGER = logging.getLogger(__name__)
20 |
21 |
22 | def _post_code_success_response(message):
23 | return _create_response("SUCCESS", message)
24 |
25 |
26 | def _create_response(status, message):
27 | response = {
28 | "status": status,
29 | "message": message
30 | }
31 | return JsonResponse(response)
32 |
33 |
34 | @login_required
35 | def code(request, id):
36 | game = get_object_or_404(Game, id=id)
37 | if not game.can_user_play(request.user):
38 | raise Http404
39 | try:
40 | avatar = game.avatar_set.get(owner=request.user)
41 | except Avatar.DoesNotExist:
42 | initial_code_file_name = os.path.join(
43 | os.path.abspath(os.path.dirname(__file__)),
44 | 'avatar_examples/dumb_avatar.py',
45 | )
46 | with open(initial_code_file_name) as initial_code_file:
47 | initial_code = initial_code_file.read()
48 | avatar = Avatar.objects.create(owner=request.user, code=initial_code,
49 | game_id=id)
50 | if request.method == 'POST':
51 | avatar.code = request.POST['code']
52 | avatar.save()
53 | return _post_code_success_response('Your code was saved!
Watch' % reverse('aimmo/watch', kwargs={'id': game.id}))
54 | else:
55 | return HttpResponse(avatar.code)
56 |
57 |
58 | def list_games(request):
59 | response = {
60 | game.pk:
61 | {
62 | 'name': game.name,
63 | 'settings': pickle.dumps(game.settings_as_dict()),
64 | } for game in Game.objects.exclude_inactive()
65 | }
66 | return JsonResponse(response)
67 |
68 |
69 | def get_game(request, id):
70 | game = get_object_or_404(Game, id=id)
71 | response = {
72 | 'main': {
73 | 'parameters': [],
74 | 'main_avatar': None,
75 | 'users': [],
76 | }
77 | }
78 | for avatar in game.avatar_set.all():
79 | if avatar.owner_id == game.main_user_id:
80 | response['main']['main_avatar'] = avatar.owner_id
81 | response['main']['users'].append({
82 | 'id': avatar.owner_id,
83 | 'code': avatar.code,
84 | })
85 | return JsonResponse(response)
86 |
87 |
88 | @csrf_exempt
89 | @require_http_methods(['POST'])
90 | def mark_game_complete(request, id):
91 | game = get_object_or_404(Game, id=id)
92 | game.completed = True
93 | game.static_data = request.body
94 | game.save()
95 | return HttpResponse('Done!')
96 |
97 |
98 | class ProgramView(TemplateView):
99 | template_name = 'players/program.html'
100 |
101 | def get_context_data(self, **kwargs):
102 | context = super(ProgramView, self).get_context_data(**kwargs)
103 | game = get_object_or_404(Game, id=self.kwargs['id'])
104 | if not game.can_user_play(self.request.user):
105 | raise Http404
106 | context['game_id'] = int(self.kwargs['id'])
107 | return context
108 |
109 |
110 | def program_level(request, num):
111 | try:
112 | game = Game.objects.get(levelattempt__user=request.user, levelattempt__level_number=num)
113 | except Game.DoesNotExist:
114 | LOGGER.debug('Adding level')
115 | game = _add_and_return_level(num, request.user)
116 | LOGGER.debug('Programming game with id %s', game.id)
117 | return render(request, 'players/program.html', {'game_id': game.id})
118 |
119 |
120 | def _render_game(request, game):
121 | context = {
122 | 'current_user_player_key': request.user.pk,
123 | 'active': game.is_active,
124 | 'static_data': game.static_data or '{}',
125 | }
126 | context['game_url_base'], context['game_url_path'] = app_settings.GAME_SERVER_LOCATION_FUNCTION(game.id)
127 | return render(request, 'players/watch.html', context)
128 |
129 |
130 | def watch_game(request, id):
131 | game = get_object_or_404(Game, id=id)
132 | if not game.can_user_play(request.user):
133 | raise Http404
134 | return _render_game(request, game)
135 |
136 |
137 | def watch_level(request, num):
138 | try:
139 | game = Game.objects.get(levelattempt__user=request.user, levelattempt__level_number=num)
140 | except Game.DoesNotExist:
141 | LOGGER.debug('Adding level')
142 | game = _add_and_return_level(num, request.user)
143 | LOGGER.debug('Displaying game with id %s', game.id)
144 | return _render_game(request, game)
145 |
146 |
147 | def _add_and_return_level(num, user):
148 | game = Game(generator='Level'+num, name='Level '+num, public=False, main_user=user)
149 | try:
150 | game.save()
151 | except ValidationError as e:
152 | LOGGER.warn(e)
153 | raise Http404
154 | game.can_play = [user]
155 | game.save()
156 | level_attempt = LevelAttempt(game=game, user=user, level_number=num)
157 | level_attempt.save()
158 | return game
159 |
160 |
161 | @login_required
162 | def add_game(request):
163 | if request.method == 'POST':
164 | form = forms.AddGameForm(request.POST)
165 | if form.is_valid():
166 | game = form.save(commit=False)
167 | game.generator = 'Main'
168 | game.owner = request.user
169 | game.save()
170 | return redirect('aimmo/program', id=game.id)
171 | else:
172 | form = forms.AddGameForm()
173 | return render(request, 'players/add_game.html', {'form': form})
174 |
--------------------------------------------------------------------------------
/players/static/js/watch/world-viewer.js:
--------------------------------------------------------------------------------
1 | // All calls to paper.* should call invertY to get from simulation coordinate system into visualisation coordinate system, then scale up by appearance.cellSize
2 |
3 | const APPEARANCE = Object.create({
4 | cellSize: 50,
5 | worldColours: {
6 | 0: "#efe",
7 | 1: "#777",
8 | 2: "#fbb",
9 | 3: "#ade"
10 | }
11 | });
12 |
13 | const VIEWER = Object.create({
14 | drawnElements: {
15 | players: [],
16 | pickups: []
17 | },
18 |
19 | init: function(canvasDomElement, world, appearance) {
20 | this.world = world;
21 | this.appearance = appearance;
22 | this.paper = Raphael(canvasDomElement);
23 | },
24 |
25 | invertY: function(y) {
26 | return -y;
27 | },
28 |
29 | reDrawWorldLayout: function() {
30 | this.paper.clear();
31 | this.paper.setViewBox(this.world.minX * this.appearance.cellSize, this.world.minY * this.appearance.cellSize, //min possible coordinates
32 | this.world.width * this.appearance.cellSize, this.world.height * this.appearance.cellSize, //size of grid
33 | true);
34 |
35 | for (var x = this.world.minX; x <= this.world.maxX; x++) {
36 | for (var y = this.world.minY; y <= this.world.maxY; y++) {
37 | var currentCellValue = this.world.layout[x][y];
38 |
39 | var square = this.paper.rect(x * this.appearance.cellSize,
40 | this.invertY(y) * this.appearance.cellSize,
41 | this.appearance.cellSize,
42 | this.appearance.cellSize);
43 |
44 | square.attr("fill", this.appearance.worldColours[currentCellValue]);
45 | square.attr("stroke", "#000");
46 |
47 | this.paper.text((x + 0.5) * this.appearance.cellSize, (this.invertY(y) + 0.5) * this.appearance.cellSize, x + ', ' + y)
48 | }
49 | }
50 | },
51 |
52 | constructNewPlayerElement: function(playerData, is_current_user) {
53 | const playerX = (0.5 + playerData["x"]) * this.appearance.cellSize;
54 | const playerY = (0.5 + this.invertY(playerData["y"])) * this.appearance.cellSize;
55 | const playerRadius = this.appearance.cellSize * 0.5 * 0.75;
56 | const currentUserIconSize = playerRadius * 0.4;
57 |
58 | var playerBody = this.paper.circle(playerX, playerY, playerRadius);
59 |
60 | playerBody.attr("fill", "#6495ED");
61 | playerBody.attr("stroke", "#6495ED");
62 |
63 | var currentUserIcon;
64 | if (is_current_user) {
65 | currentUserIcon = this.paper.rect(
66 | playerX - currentUserIconSize / 2,
67 | playerY - currentUserIconSize / 2,
68 | currentUserIconSize,
69 | currentUserIconSize
70 | );
71 | currentUserIcon.attr("fill", "#FF0000");
72 | currentUserIcon.attr("stroke", "#FF0000");
73 | }
74 |
75 | var playerTextAbove = this.paper.text(playerX, playerY - 20, 'Score: ' + playerData["score"]);
76 | var playerTextBelow = this.paper.text(playerX, playerY + 20, playerData["health"] + 'hp, (' + playerData["x"] + ', ' + playerData["y"] + ')');
77 |
78 | var player = this.paper.set();
79 | player.push(
80 | playerBody,
81 | playerTextAbove,
82 | playerTextBelow,
83 | currentUserIcon
84 | );
85 | return player;
86 | },
87 |
88 | clearDrawnElements: function(elements) {
89 | while (elements.length > 0) {
90 | var elementToRemove = elements.pop();
91 | elementToRemove.remove();
92 | }
93 | },
94 |
95 | reDrawPlayers: function() {
96 | this.clearDrawnElements(this.drawnElements.players);
97 |
98 | for (var i = 0; i < this.world.players.length; i++) {
99 | var is_current_user = this.world.players[i]["id"] === CURRENT_USER_PLAYER_KEY;
100 | var playerElement = this.constructNewPlayerElement(this.world.players[i], is_current_user);
101 | this.drawnElements.players.push(playerElement);
102 | }
103 | },
104 |
105 | reDrawPickups: function() {
106 | this.clearDrawnElements(this.drawnElements.pickups);
107 |
108 | for (var i = 0; i < this.world.pickups.length; i++) {
109 | var pickupLocation = this.world.pickups[i].location;
110 | var x = (0.5 + pickupLocation[0]) * this.appearance.cellSize;
111 | var y = (0.5 + this.invertY(pickupLocation[1])) * this.appearance.cellSize;
112 |
113 | // Just for testing.
114 | pickup = this.drawHealth(x, y);
115 | }
116 | },
117 |
118 | drawHealth: function(x, y) {
119 | var radius = this.appearance.cellSize * 0.5 * 0.75;
120 | var circle = this.paper.circle(x, y, radius);
121 | circle.attr("fill", '#FFFFFF');
122 | var crossX = this.paper.rect(x - 10, y - 3, 20, 6).attr({fill: '#FF0000', stroke: '#FF0000'});
123 | var crossY = this.paper.rect(x - 3, y - 10, 6, 20).attr({fill: '#FF0000', stroke: '#FF0000'});
124 | var pickup = this.paper.set();
125 | pickup.push(circle, crossX, crossY);
126 | return pickup;
127 | },
128 |
129 | drawInvulnerability: function(x, y) {
130 | var radius = this.appearance.cellSize * 0.5 * 0.75;
131 | var circle = this.paper.circle(x, y, radius);
132 | circle.attr('fill', '#0066ff');
133 | var pickup = this.paper.set();
134 | pickup.push(circle);
135 | return pickup;
136 | },
137 |
138 | drawDamage: function(x, y) {
139 | var radius = this.appearance.cellSize * 0.5 * 0.75;
140 | var circle = this.paper.circle(x, y, radius);
141 | circle.attr('fill', '#ff0000');
142 | var pickup = this.paper.set();
143 | pickup.push(circle);
144 | return pickup;
145 | },
146 |
147 | reDrawState: function() {
148 | this.reDrawWorldLayout();
149 | this.reDrawPickups();
150 | this.reDrawPlayers();
151 | }
152 | });
153 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_simulation/test_map_generator.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import random
4 | import unittest
5 |
6 | from simulation import map_generator
7 | from simulation.location import Location
8 | from simulation.map_generator import get_random_edge_index
9 | from simulation.world_map import WorldMap
10 | from .dummy_avatar import DummyAvatarManager
11 |
12 |
13 | class ConstantRng(object):
14 | def __init__(self, value):
15 | self.value = value
16 |
17 | def randint(self, minimum, maximum):
18 | if not minimum <= self.value <= maximum:
19 | raise ValueError('Beyond range')
20 | return self.value
21 |
22 |
23 | class TestHelperFunctions(unittest.TestCase):
24 | def test_get_random_edge_index(self):
25 | map = WorldMap.generate_empty_map(3, 4, {})
26 | self.assertEqual(
27 | (0, -1), get_random_edge_index(map, rng=ConstantRng(0)))
28 | self.assertEqual(
29 | (1, -1), get_random_edge_index(map, rng=ConstantRng(1)))
30 | self.assertEqual(
31 | (0, 1), get_random_edge_index(map, rng=ConstantRng(2)))
32 | self.assertEqual(
33 | (1, 1), get_random_edge_index(map, rng=ConstantRng(3)))
34 | self.assertEqual(
35 | (-1, 0), get_random_edge_index(map, rng=ConstantRng(4)))
36 | self.assertEqual(
37 | (2, 0), get_random_edge_index(map, rng=ConstantRng(5)))
38 |
39 | # Verify no out of bounds
40 | with self.assertRaisesRegexp(ValueError, 'Beyond range'):
41 | get_random_edge_index(map, rng=ConstantRng(-1))
42 |
43 | with self.assertRaisesRegexp(ValueError, 'Beyond range'):
44 | get_random_edge_index(map, rng=ConstantRng(6))
45 |
46 | def test_get_random_edge_index_can_give_all_possible(self):
47 | map = WorldMap.generate_empty_map(3, 4, {})
48 | get_random_edge_index(map, rng=ConstantRng(1))
49 | expected = frozenset((
50 | (0, 1), (1, 1),
51 | (-1, 0), (2, 0),
52 | (0, -1), (1, -1),
53 | ))
54 | actual = frozenset(get_random_edge_index(map, rng=ConstantRng(i))
55 | for i in xrange(6))
56 | self.assertEqual(expected, actual)
57 |
58 | def test_out_of_bounds_random_edge(self):
59 | map = WorldMap.generate_empty_map(3, 4, {})
60 | with self.assertRaisesRegexp(ValueError, 'Beyond range'):
61 | get_random_edge_index(map, rng=ConstantRng(-1))
62 |
63 | with self.assertRaisesRegexp(ValueError, 'Beyond range'):
64 | get_random_edge_index(map, rng=ConstantRng(6))
65 |
66 |
67 | class _BaseGeneratorTestCase(unittest.TestCase):
68 | def get_game_state(self, **kwargs):
69 | random.seed(0)
70 | settings = {
71 | 'START_WIDTH': 3,
72 | 'START_HEIGHT': 4,
73 | 'OBSTACLE_RATIO': 1.0
74 | }
75 | settings.update(kwargs)
76 | return self.GENERATOR_CLASS(settings).get_game_state(DummyAvatarManager())
77 |
78 | def get_map(self, **kwargs):
79 | return self.get_game_state(**kwargs).world_map
80 |
81 |
82 | class TestMainGenerator(_BaseGeneratorTestCase):
83 | GENERATOR_CLASS = map_generator.Main
84 |
85 | def test_map_dimensions(self):
86 | m = self.get_map()
87 | grid = list(m.all_cells())
88 | self.assertEqual(len(set(grid)), len(grid), "Repeats in list")
89 | for c in grid:
90 | self.assertLessEqual(c.location.x, 1)
91 | self.assertLessEqual(c.location.y, 2)
92 | self.assertGreaterEqual(c.location.x, -1)
93 | self.assertGreaterEqual(c.location.y, -1)
94 |
95 | def test_obstable_ratio(self):
96 | m = self.get_map(OBSTACLE_RATIO=0)
97 | obstacle_cells = [cell for cell in m.all_cells() if not cell.habitable]
98 | self.assertEqual(len(obstacle_cells), 0)
99 |
100 | def test_map_contains_some_non_habitable_cell(self):
101 | m = self.get_map()
102 | obstacle_cells = [cell for cell in m.all_cells() if not cell.habitable]
103 | self.assertGreaterEqual(len(obstacle_cells), 1)
104 |
105 | def test_map_contains_some_habitable_cell_on_border(self):
106 | m = self.get_map(START_WIDTH=4)
107 | edge_coordinates = [
108 | (-1, 2), (0, 2), (1, 2), (2, 2),
109 | (-1, 1), (2, 1),
110 | (-1, 0), (2, 0),
111 | (-1, -1), (0, -1), (1, -1), (2, -1),
112 | ]
113 | edge_cells = (m.get_cell_by_coords(x, y) for (x, y) in edge_coordinates)
114 | habitable_edge_cells = [cell for cell in edge_cells if cell.habitable]
115 |
116 | self.assertGreaterEqual(len(habitable_edge_cells), 1)
117 |
118 | def test_not_complete(self):
119 | game_state = self.get_game_state()
120 | self.assertFalse(game_state.is_complete())
121 |
122 |
123 | class TestLevel1Generator(_BaseGeneratorTestCase):
124 | GENERATOR_CLASS = map_generator.Level1
125 |
126 | def test_width_5(self):
127 | self.assertEqual(self.get_map().num_cols, 5)
128 |
129 | def test_height_1(self):
130 | self.assertEqual(self.get_map().num_rows, 1)
131 |
132 | def test_incomplete_without_avatars(self):
133 | game_state = self.get_game_state()
134 | self.assertFalse(game_state.is_complete())
135 |
136 | def test_incomplete_at_score_0(self):
137 | game_state = self.get_game_state()
138 | game_state.avatar_manager.add_avatar(1, '', None)
139 | game_state.main_avatar_id = 1
140 | self.assertFalse(game_state.is_complete())
141 |
142 | def test_completes_at_score_1(self):
143 | game_state = self.get_game_state()
144 | game_state.avatar_manager.add_avatar(1, '', None)
145 | game_state.avatar_manager.avatars_by_id[1].score = 1
146 | game_state.main_avatar_id = 1
147 | self.assertTrue(game_state.is_complete())
148 |
149 | def test_static_spawn(self):
150 | game_state = self.get_game_state()
151 | for i in xrange(5):
152 | game_state.add_avatar(i, '')
153 | self.assertEqual(game_state.avatar_manager.avatars_by_id[i].location, Location(-2, 0))
154 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AI:MMO
2 | A **M**assively **M**ulti-player **O**nline game, where players create **A**rtificially **I**ntelligent programs to play on their behalf.
3 |
4 | ## A [Code for Life](https://www.codeforlife.education/) project
5 | * Ocado Technology's [Code for Life initiative](https://www.codeforlife.education/) has been developed to inspire the next generation of computer scientists and to help teachers deliver the computing curriculum.
6 | * This repository hosts the source code of the **AI:MMO game**. AI:MMO is aimed as a follow-on from [Rapid Router](https://www.codeforlife.education/rapidrouter) to teach computing to secondary-school age children.
7 | * The other repos for Code For Life:
8 | * the main portal (as well as registration, dashboards, materials...), [Code For Life Portal](https://github.com/ocadotechnology/codeforlife-portal)
9 | * the first coding game of Code for Life for primary schools, [Rapid Router](https://github.com/ocadotechnology/rapid-router)
10 | * the [deployment code for Google App Engine](https://github.com/ocadotechnology/codeforlife-deploy-appengine)
11 |
12 | ## Objective
13 | People program Avatars. Avatars play the game. A player's aim is to create a better Avatar than other people's Avatars. A "better" Avatar is one that scores points faster than other people's Avatars.
14 |
15 | By getting people to compete to program better Avatars, we can teach them all sorts of algorithms and optimisation techniques. For example, a really good Avatar might incorporate AI techniques such as neural networks in order to make more optimal decisions.
16 |
17 | ## The Game
18 | The world is a 2D grid. Some cells are impassable. Some cells generate score. Some cells contain pick-ups.
19 |
20 | There are other Avatars in the world. The more Avatars there are, the bigger the world gets.
21 |
22 | Time passes in turns. An Avatar may perform a single action every turn. They only have visibility of a small amount of the world around them.
23 |
24 | Avatars can only wait, move or attack.
25 |
26 | Even with these basic mechanics, there is quite a lot of complexity in creating an Avatar that is good at gaining score. For example, you may need to be able to find optimal paths from A to B. You may need to program your Avatar to remember the parts of the world that it has already encountered, so that you can find faster paths between locations. You may need to program your Avatar to machine learn when it is optimal to:
27 | - attack another player
28 | - run away from another player
29 | - try to find a health pick up
30 | - run to the score location
31 | - ...
32 |
33 | ## Architecture
34 | ### Web Interface - `players`
35 | - Django
36 | - In-browser avatar code editor: http://ace.c9.io/#nav=about
37 | - Game view (so players can see their avatars play the game)
38 | - Statistics
39 | - Has a sample deployment in `example_project`
40 |
41 | ### Game creator - `aimmo-game-creator`
42 | - Maintains the set of games
43 |
44 | ### Core Game (Simulation) - `aimmo-game`
45 | - Maintains game state
46 | - Simulates environment events
47 | - Runs player actions
48 |
49 | ### Sandboxed User-Submitted AI Players (Avatars) - `aimmo-game-worker`
50 | - Each avatar will run in their own sandbox so that we can securely deal with user-submitted code
51 | - Each avatar will interact with the core game to get state and perform actions
52 |
53 |
54 |
55 | ## Running Locally
56 | * Clone the repo
57 | * Make and activate a virtualenv (We recommend [virtualenvwrapper](http://virtualenvwrapper.readthedocs.org/en/latest/index.html)) - if you have a Mac see the following section.
58 | * e.g. the first time, `mkvirtualenv -a path/to/aimmo aimmo`
59 | * and thereafter `workon aimmo`
60 | * `./run.py` in your aimmo dir - This will:
61 | * if necessary, create a superuser 'admin' with password 'admin'
62 | * install all of the dependencies using pip
63 | * sync the database
64 | * collect the static files
65 | * run the server
66 | * You can quickly create players as desired using the following command:
67 |
68 | `python example_project/manage.py generate_players 5 dumb_avatar.py`
69 |
70 | This creates 5 users with password `123`, and creates for each user an avatar that runs the code in `dumb_avatar.py`
71 | * To delete the generated players use the following command:
72 |
73 | `python example_project/manage.py delete_generated_players`
74 |
75 | ### Under Kubernetes
76 | * By default, the local environment runs each worker in a Python thread. However, for some testing, it is useful to run as a Kubernetes cluster. Note that this is not for most testing, the default is more convenient as the Kubernetes cluster is slow and runs into resource limits with ~10 avatars.
77 | * Linux, Windows (minikube is experimental though), and OSX (untested) are supported.
78 | * Prerequisites:
79 | * All platforms: VT-x/AMD-v virtualization.
80 | * Linux: [Virtualbox](https://www.virtualbox.org/wiki/Downloads).
81 | * OSX: either [Virtualbox](https://www.virtualbox.org/wiki/Downloads) or [VMWare Fusion](http://www.vmware.com/products/fusion.html).
82 | * Usage: `python run.py -k`. This will:
83 | * Download Docker, Minikube, and Kubectl into a `test-bin` folder in the project's root directory.
84 | * Run `minikube start` (if the cluster is not already running).
85 | * Build each image.
86 | * Start aimmo-game-creator.
87 | * Perform the same setup that run.py normally performs.
88 | * Start the Django project (which is not kubernetes controlled) on localhost:8000.
89 | * Run the same command to update all the images.
90 |
91 | #### Interacting with the cluster
92 | * `kubectl` and `minikube` (both in the `test-bin` folder, note that this is not on your PATH) can be used to interact with the cluster.
93 | * Running either command without any options will give the most useful commands.
94 | * `./test-bin/minikube dashboard` to open the kubernetes dashboard in your def
95 |
96 | ## Testing Locally
97 | *`./all_tests.py` will run all tests (note that this is several pages of output).
98 | * `--coverage` option generates coverage data using coverage.py
99 |
100 | ## Useful commands
101 | * To create an admin account:
102 | `python example_project/manage.py createsuperuser`
103 |
104 | ### Installing virtualenvwrapper on Mac
105 | * Run `pip install virtualenvwrapper`
106 | * Add the following to ~/.bashrc:
107 | ```
108 | export WORKON_HOME=$HOME/.virtualenvs
109 | source /usr/local/bin/virtualenvwrapper.sh
110 | ```
111 | * [This blog post](http://mkelsey.com/2013/04/30/how-i-setup-virtualenv-and-virtualenvwrapper-on-my-mac/) may also be
112 | useful.
113 |
114 | ## How to contribute!
115 | __Want to help?__ You can contact us using this [contact form][c4l-contact-form] and we'll get in touch as soon as possible! Thanks a lot.
116 |
117 | [c4l-contact-form]: https://www.codeforlife.education/contact/
118 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/map_generator.py:
--------------------------------------------------------------------------------
1 | import abc
2 | import heapq
3 | import logging
4 | import random
5 | from itertools import tee
6 |
7 | from six.moves import zip, range
8 |
9 | from simulation.direction import ALL_DIRECTIONS
10 | from simulation.game_state import GameState
11 | from simulation.location import Location
12 | from simulation.world_map import WorldMap
13 | from simulation.world_map import WorldMapStaticSpawnDecorator
14 | from simulation.world_map import DEFAULT_LEVEL_SETTINGS
15 |
16 | LOGGER = logging.getLogger(__name__)
17 |
18 |
19 | class _BaseGenerator(object):
20 | __metaclass__ = abc.ABCMeta
21 |
22 | def __init__(self, settings):
23 | self.settings = settings
24 |
25 | def get_game_state(self, avatar_manager):
26 | return GameState(self.get_map(), avatar_manager, self.check_complete)
27 |
28 | def check_complete(self, game_state):
29 | return False
30 |
31 | @abc.abstractmethod
32 | def get_map(self):
33 | pass
34 |
35 |
36 | class Main(_BaseGenerator):
37 | def get_map(self):
38 | height = self.settings['START_HEIGHT']
39 | width = self.settings['START_WIDTH']
40 | world_map = WorldMap.generate_empty_map(height, width, self.settings)
41 |
42 | # We designate one non-corner edge cell as empty, to ensure that the map can be expanded
43 | always_empty_edge_x, always_empty_edge_y = get_random_edge_index(world_map)
44 | always_empty_location = Location(always_empty_edge_x, always_empty_edge_y)
45 |
46 | for cell in shuffled(world_map.all_cells()):
47 | if cell.location != always_empty_location and random.random() < self.settings['OBSTACLE_RATIO']:
48 | cell.habitable = False
49 | # So long as all habitable neighbours can still reach each other,
50 | # then the map cannot get bisected
51 | if not _all_habitable_neighbours_can_reach_each_other(cell, world_map):
52 | cell.habitable = True
53 |
54 | return world_map
55 |
56 |
57 | def _get_edge_coordinates(height, width):
58 | for x in range(width):
59 | for y in range(height):
60 | yield x, y
61 |
62 |
63 | def shuffled(iterable):
64 | values = list(iterable)
65 | random.shuffle(values)
66 | return iter(values)
67 |
68 |
69 | def pairwise(iterable):
70 | """s -> (s0,s1), (s1,s2), (s2, s3), ..."""
71 | a, b = tee(iterable)
72 | next(b, None)
73 | return zip(a, b)
74 |
75 |
76 | def _all_habitable_neighbours_can_reach_each_other(cell, world_map):
77 | neighbours = get_adjacent_habitable_cells(cell, world_map)
78 |
79 | assert len(neighbours) >= 1
80 | neighbour_pairs = ((n1, n2) for n1, n2 in pairwise(neighbours))
81 | shortest_path_exists = (get_shortest_path_between(n1, n2, world_map) is not None
82 | for n1, n2 in neighbour_pairs)
83 | return all(shortest_path_exists)
84 |
85 |
86 | def get_shortest_path_between(source_cell, destination_cell, world_map):
87 |
88 | def manhattan_distance_to_destination_cell(this_branch):
89 | branch_tip_location = this_branch[-1].location
90 | x_distance = abs(branch_tip_location.x - destination_cell.location.x)
91 | y_distance = abs(branch_tip_location.y - destination_cell.location.y)
92 | return x_distance + y_distance + len(this_branch)
93 |
94 | branches = PriorityQueue(key=manhattan_distance_to_destination_cell, init_items=[[source_cell]])
95 | visited_cells = set()
96 |
97 | while branches:
98 | branch = branches.pop()
99 |
100 | for cell in get_adjacent_habitable_cells(branch[-1], world_map):
101 | if cell in visited_cells:
102 | continue
103 |
104 | visited_cells.add(cell)
105 |
106 | new_branch = branch + [cell]
107 |
108 | if cell == destination_cell:
109 | return new_branch
110 |
111 | branches.push(new_branch)
112 |
113 | return None
114 |
115 |
116 | def get_random_edge_index(world_map, rng=random):
117 | num_row_cells = world_map.num_rows - 2
118 | num_col_cells = world_map.num_cols - 2
119 | num_edge_cells = 2*num_row_cells + 2*num_col_cells
120 | random_cell = rng.randint(0, num_edge_cells-1)
121 |
122 | if 0 <= random_cell < num_col_cells:
123 | # random non-corner cell on the first row
124 | return random_cell + 1 + world_map.min_x(), world_map.min_y()
125 | random_cell -= num_col_cells
126 |
127 | if 0 <= random_cell < num_col_cells:
128 | # random non-corner cell on the last row
129 | return random_cell + 1 + world_map.min_x(), world_map.max_y()
130 | random_cell -= num_col_cells
131 |
132 | if 0 <= random_cell < num_row_cells:
133 | # random non-corner cell on the first column
134 | return world_map.min_x(), world_map.min_y() + random_cell + 1
135 | random_cell -= num_row_cells
136 |
137 | assert 0 <= random_cell < num_row_cells
138 | # random non-corner cell on the last column
139 | return world_map.max_x(), world_map.min_y() + random_cell + 1
140 |
141 |
142 | def get_adjacent_habitable_cells(cell, world_map):
143 | adjacent_locations = [cell.location + d for d in ALL_DIRECTIONS]
144 | adjacent_locations = [location for location in adjacent_locations
145 | if world_map.is_on_map(location)]
146 |
147 | adjacent_cells = [world_map.get_cell(location) for location in adjacent_locations]
148 | return [c for c in adjacent_cells if c.habitable]
149 |
150 |
151 | class PriorityQueue(object):
152 | def __init__(self, key, init_items=tuple()):
153 | self.key = key
154 | self.heap = [self._build_tuple(i) for i in init_items]
155 | heapq.heapify(self.heap)
156 |
157 | def _build_tuple(self, item):
158 | return self.key(item), item
159 |
160 | def push(self, item):
161 | to_push = self._build_tuple(item)
162 | heapq.heappush(self.heap, to_push)
163 |
164 | def pop(self):
165 | _, item = heapq.heappop(self.heap)
166 | return item
167 |
168 | def __len__(self):
169 | return len(self.heap)
170 |
171 |
172 | class _BaseLevelGenerator(_BaseGenerator):
173 | __metaclass__ = abc.ABCMeta
174 |
175 | def __init__(self, *args, **kwargs):
176 | super(_BaseLevelGenerator, self).__init__(*args, **kwargs)
177 | self.settings.update(DEFAULT_LEVEL_SETTINGS)
178 |
179 |
180 | class Level1(_BaseLevelGenerator):
181 | def get_map(self):
182 | world_map = WorldMap.generate_empty_map(1, 5, self.settings)
183 | world_map = WorldMapStaticSpawnDecorator(world_map, Location(-2, 0))
184 | world_map.get_cell(Location(2, 0)).generates_score = True
185 | return world_map
186 |
187 | def check_complete(self, game_state):
188 | try:
189 | main_avatar = game_state.get_main_avatar()
190 | except KeyError:
191 | return False
192 | return main_avatar.score > 0
193 |
--------------------------------------------------------------------------------
/aimmo-game/tests/test_simulation/test_turn_manager.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import unittest
4 |
5 | from simulation.avatar.avatar_appearance import AvatarAppearance
6 | from simulation.game_state import GameState
7 | from simulation.location import Location
8 | from simulation.turn_manager import ConcurrentTurnManager
9 | from .dummy_avatar import DummyAvatarManager
10 | from .dummy_avatar import MoveEastDummy
11 | from .dummy_avatar import MoveNorthDummy
12 | from .dummy_avatar import MoveSouthDummy
13 | from .dummy_avatar import MoveWestDummy
14 | from .dummy_avatar import WaitDummy
15 | from .maps import InfiniteMap
16 |
17 | ORIGIN = Location(0, 0)
18 |
19 | RIGHT_OF_ORIGIN = Location(1, 0)
20 | FIVE_RIGHT_OF_ORIGIN = Location(5, 0)
21 |
22 | ABOVE_ORIGIN = Location(0, 1)
23 | FIVE_RIGHT_OF_ORIGIN_AND_ONE_ABOVE = Location(5, 1)
24 |
25 |
26 | class MockGameState(GameState):
27 | def get_state_for(self, avatar):
28 | return self
29 |
30 |
31 | class TestTurnManager(unittest.TestCase):
32 | def construct_default_avatar_appearance(self):
33 | return AvatarAppearance("#000", "#ddd", "#777", "#fff")
34 |
35 | def construct_turn_manager(self, avatars, locations):
36 | self.avatar_manager = DummyAvatarManager(avatars)
37 | self.game_state = MockGameState(InfiniteMap(), self.avatar_manager)
38 | self.turn_manager = ConcurrentTurnManager(game_state=self.game_state,
39 | end_turn_callback=lambda: None,
40 | completion_url='')
41 | for index, location in enumerate(locations):
42 | self.game_state.add_avatar(index, "", location)
43 | return self.turn_manager
44 |
45 | def assert_at(self, avatar, location):
46 | self.assertEqual(avatar.location, location)
47 | cell = self.game_state.world_map.get_cell(location)
48 | self.assertEqual(cell.avatar, avatar)
49 |
50 | def get_avatar(self, player_id):
51 | return self.avatar_manager.get_avatar(player_id)
52 |
53 | def run_turn(self):
54 | self.turn_manager.run_turn()
55 |
56 | def test_run_turn(self):
57 | '''
58 | Given: > _
59 | (1)
60 | Expect: _ o
61 | '''
62 | self.construct_turn_manager([MoveEastDummy], [ORIGIN])
63 | avatar = self.get_avatar(0)
64 |
65 | self.assert_at(avatar, ORIGIN)
66 | self.run_turn()
67 | self.assert_at(avatar, RIGHT_OF_ORIGIN)
68 |
69 | def test_run_several_turns(self):
70 | '''
71 | Given: > _ _ _ _ _
72 | (5)
73 | Expect: _ _ _ _ _ o
74 | '''
75 | self.construct_turn_manager([MoveEastDummy], [ORIGIN])
76 | avatar = self.get_avatar(0)
77 |
78 | self.assertEqual(avatar.location, ORIGIN)
79 | [self.run_turn() for _ in range(5)]
80 | self.assertEqual(avatar.location, FIVE_RIGHT_OF_ORIGIN)
81 |
82 | def test_run_several_turns_and_avatars(self):
83 | '''
84 | Given: > _ _ _ _ _
85 | > _ _ _ _ _
86 | (5)
87 | Expect: _ _ _ _ _ o
88 | _ _ _ _ _ o
89 | '''
90 | self.construct_turn_manager([MoveEastDummy, MoveEastDummy],
91 | [ORIGIN, ABOVE_ORIGIN])
92 | avatar0 = self.get_avatar(0)
93 | avatar1 = self.get_avatar(1)
94 |
95 | self.assert_at(avatar0, ORIGIN)
96 | self.assert_at(avatar1, ABOVE_ORIGIN)
97 | [self.run_turn() for _ in range(5)]
98 | self.assert_at(avatar0, FIVE_RIGHT_OF_ORIGIN)
99 | self.assert_at(avatar1, FIVE_RIGHT_OF_ORIGIN_AND_ONE_ABOVE)
100 |
101 | def test_move_chain_succeeds(self):
102 | '''
103 | Given: > > > > > _
104 |
105 | Expect: _ o o o o o
106 | '''
107 | self.construct_turn_manager([MoveEastDummy for _ in range(5)],
108 | [Location(x, 0) for x in range(5)])
109 | avatars = [self.get_avatar(i) for i in range(5)]
110 |
111 | [self.assert_at(avatars[x], Location(x, 0)) for x in range(5)]
112 | self.run_turn()
113 | [self.assert_at(avatars[x], Location(x + 1, 0)) for x in range(5)]
114 |
115 | def test_move_chain_fails_occupied(self):
116 | '''
117 | Given: > > x _
118 |
119 | Expect: x x x _
120 | '''
121 | self.construct_turn_manager([MoveEastDummy, MoveEastDummy, WaitDummy],
122 | [Location(x, 0) for x in range(3)])
123 | avatars = [self.get_avatar(i) for i in range(3)]
124 |
125 | [self.assert_at(avatars[x], Location(x, 0)) for x in range(3)]
126 | self.run_turn()
127 | [self.assert_at(avatars[x], Location(x, 0)) for x in range(3)]
128 |
129 | def test_move_chain_fails_collision(self):
130 | '''
131 | Given: > > > _ <
132 | (1)
133 | Expect: x x x _ x
134 | '''
135 | locations = [Location(0, 0), Location(1, 0), Location(2, 0), Location(4, 0)]
136 | self.construct_turn_manager(
137 | [MoveEastDummy, MoveEastDummy, MoveEastDummy, MoveWestDummy],
138 | locations)
139 | avatars = [self.get_avatar(i) for i in range(4)]
140 |
141 | [self.assert_at(avatars[i], locations[i]) for i in range(4)]
142 | self.run_turn()
143 | [self.assert_at(avatars[i], locations[i]) for i in range(4)]
144 |
145 | def test_move_chain_fails_cycle(self):
146 | '''
147 | Given: > v
148 | ^ <
149 | (1)
150 | Expect: x x
151 | x x
152 | '''
153 | locations = [Location(0, 1), Location(1, 1), Location(1, 0), Location(0, 0)]
154 | self.construct_turn_manager(
155 | [MoveEastDummy, MoveSouthDummy, MoveWestDummy, MoveNorthDummy],
156 | locations)
157 | avatars = [self.get_avatar(i) for i in range(4)]
158 |
159 | [self.assert_at(avatars[i], locations[i]) for i in range(4)]
160 | self.run_turn()
161 | [self.assert_at(avatars[i], locations[i]) for i in range(4)]
162 |
163 | def test_move_chain_fails_spiral(self):
164 | '''
165 | Given: > > v
166 | ^ <
167 | (1)
168 | Expect: x x x
169 | x x
170 | '''
171 | locations = [Location(0, 1),
172 | Location(1, 1),
173 | Location(2, 1),
174 | Location(2, 0),
175 | Location(1, 0)]
176 | self.construct_turn_manager(
177 | [MoveEastDummy, MoveEastDummy, MoveSouthDummy, MoveWestDummy, MoveNorthDummy],
178 | locations)
179 | avatars = [self.get_avatar(i) for i in range(5)]
180 |
181 | [self.assert_at(avatars[i], locations[i]) for i in range(5)]
182 | self.run_turn()
183 | [self.assert_at(avatars[i], locations[i]) for i in range(5)]
184 |
185 | if __name__ == '__main__':
186 | unittest.main()
187 |
--------------------------------------------------------------------------------
/aimmo-game/simulation/worker_manager.py:
--------------------------------------------------------------------------------
1 | import atexit
2 | import itertools
3 | import json
4 | import logging
5 | import os
6 | import subprocess
7 | import tempfile
8 | import threading
9 | import time
10 |
11 | import requests
12 | from eventlet.greenpool import GreenPool
13 | from eventlet.semaphore import Semaphore
14 | from pykube import HTTPClient
15 | from pykube import KubeConfig
16 | from pykube import Pod
17 |
18 | LOGGER = logging.getLogger(__name__)
19 |
20 |
21 | class _WorkerManagerData(object):
22 | """
23 | This class is thread safe
24 | """
25 |
26 | def __init__(self, game_state, user_codes):
27 | self._game_state = game_state
28 | self._user_codes = user_codes
29 | self._lock = Semaphore()
30 |
31 | def _remove_avatar(self, user_id):
32 | assert self._lock.locked
33 | self._game_state.remove_avatar(user_id)
34 | del self._user_codes[user_id]
35 |
36 | def remove_user_if_code_is_different(self, user):
37 | with self._lock:
38 | existing_code = self._user_codes.get(user['id'], None)
39 | if existing_code != user['code']:
40 | # Remove avatar from the game, so it stops being called for turns
41 | if existing_code is not None:
42 | self._remove_avatar(user['id'])
43 | return True
44 | else:
45 | return False
46 |
47 | def add_avatar(self, user, worker_url):
48 | with self._lock:
49 | # Add avatar back into game
50 | self._game_state.add_avatar(
51 | user_id=user['id'], worker_url="%s/turn/" % worker_url)
52 |
53 | def set_code(self, user):
54 | with self._lock:
55 | self._user_codes[user['id']] = user['code']
56 |
57 | def get_code(self, player_id):
58 | with self._lock:
59 | return self._user_codes[player_id]
60 |
61 | def remove_unknown_avatars(self, known_user_ids):
62 | with self._lock:
63 | unknown_user_ids = set(self._user_codes) - frozenset(known_user_ids)
64 | for u in unknown_user_ids:
65 | self._remove_avatar(u)
66 | return unknown_user_ids
67 |
68 | def set_main_avatar(self, avatar_id):
69 | with self._lock:
70 | self._game_state.main_avatar_id = avatar_id
71 |
72 |
73 | class WorkerManager(threading.Thread):
74 | """
75 | Methods of this class must be thread safe unless explicitly stated.
76 | """
77 | daemon = True
78 |
79 | def __init__(self, game_state, users_url, port=5000):
80 | """
81 |
82 | :param thread_pool:
83 | """
84 | self._data = _WorkerManagerData(game_state, {})
85 | self.users_url = users_url
86 | self._pool = GreenPool(size=3)
87 | self.port = port
88 | super(WorkerManager, self).__init__()
89 |
90 | def get_code(self, player_id):
91 | return self._data.get_code(player_id)
92 |
93 | def get_persistent_state(self, player_id):
94 | """Get the persistent state for a worker."""
95 |
96 | return None
97 |
98 | def create_worker(self, player_id):
99 | """Create a worker."""
100 |
101 | raise NotImplemented
102 |
103 | def remove_worker(self, player_id):
104 | """Remove a worker for the given player."""
105 |
106 | raise NotImplemented
107 |
108 | # TODO handle failure
109 | def spawn(self, user):
110 | # Get persistent state from worker
111 | persistent_state = self.get_persistent_state(user['id']) # noqa: F841
112 |
113 | # Kill worker
114 | LOGGER.info("Removing worker for user %s" % user['id'])
115 | self.remove_worker(user['id'])
116 |
117 | self._data.set_code(user)
118 |
119 | # Spawn worker
120 | LOGGER.info("Spawning worker for user %s" % user['id'])
121 | worker_url = self.create_worker(user['id'])
122 |
123 | # Add avatar back into game
124 | self._data.add_avatar(user, worker_url)
125 | LOGGER.info('Added user %s', user['id'])
126 |
127 | def _parallel_map(self, func, iterable_args):
128 | list(self._pool.imap(func, iterable_args))
129 |
130 | def update(self):
131 | try:
132 | LOGGER.info("Waking up")
133 | game_data = requests.get(self.users_url).json()
134 | except (requests.RequestException, ValueError) as err:
135 | LOGGER.error("Failed to obtain game data : %s", err)
136 | else:
137 | game = game_data['main']
138 |
139 | # Remove users with different code
140 | users_to_add = []
141 | for user in game['users']:
142 | if self._data.remove_user_if_code_is_different(user):
143 | users_to_add.append(user)
144 | LOGGER.debug("Need to add users: %s" % [x['id'] for x in users_to_add])
145 |
146 | # Add missing users
147 | self._parallel_map(self.spawn, users_to_add)
148 |
149 | # Delete extra users
150 | known_avatars = set(user['id'] for user in game['users'])
151 | removed_user_ids = self._data.remove_unknown_avatars(known_avatars)
152 | LOGGER.debug("Removing users: %s" % removed_user_ids)
153 | self._parallel_map(self.remove_worker, removed_user_ids)
154 |
155 | # Update main avatar
156 | self._data.set_main_avatar(game_data['main']['main_avatar'])
157 |
158 | def run(self):
159 | while True:
160 | self.update()
161 | LOGGER.info("Sleeping")
162 | time.sleep(10)
163 |
164 |
165 | class LocalWorkerManager(WorkerManager):
166 | """Relies on them already being created already."""
167 |
168 | host = '127.0.0.1'
169 | worker_directory = os.path.join(
170 | os.path.dirname(__file__),
171 | '../../aimmo-game-worker/',
172 | )
173 |
174 | def __init__(self, *args, **kwargs):
175 | super(LocalWorkerManager, self).__init__(*args, **kwargs)
176 | self.workers = {}
177 | self.port_counter = itertools.count(self.port + 10)
178 |
179 | def create_worker(self, player_id):
180 | assert(player_id not in self.workers)
181 | port = self.port_counter.next()
182 | env = os.environ.copy()
183 | data_dir = tempfile.mkdtemp()
184 |
185 | LOGGER.debug('Data dir is %s', data_dir)
186 | data = requests.get("http://127.0.0.1:{}/player/{}".format(self.port, player_id)).json()
187 |
188 | options = data['options']
189 | with open('{}/options.json'.format(data_dir), 'w') as options_file:
190 | json.dump(options, options_file)
191 |
192 | code = data['code']
193 | with open('{}/avatar.py'.format(data_dir), 'w') as avatar_file:
194 | avatar_file.write(code)
195 |
196 | env['PYTHONPATH'] = data_dir
197 |
198 | process = subprocess.Popen(['python', 'service.py', self.host, str(port), str(data_dir)], cwd=self.worker_directory, env=env)
199 | atexit.register(process.kill)
200 | self.workers[player_id] = process
201 | worker_url = 'http://%s:%d' % (
202 | self.host,
203 | port,
204 | )
205 | LOGGER.info("Worker started for %s, listening at %s", player_id, worker_url)
206 | return worker_url
207 |
208 | def remove_worker(self, player_id):
209 | if player_id in self.workers:
210 | self.workers[player_id].kill()
211 | del self.workers[player_id]
212 |
213 |
214 | class KubernetesWorkerManager(WorkerManager):
215 | """Kubernetes worker manager."""
216 |
217 | def __init__(self, *args, **kwargs):
218 | self.api = HTTPClient(KubeConfig.from_service_account())
219 | self.game_id = os.environ['GAME_ID']
220 | self.game_url = os.environ['GAME_URL']
221 | super(KubernetesWorkerManager, self).__init__(*args, **kwargs)
222 |
223 | def create_worker(self, player_id):
224 | pod = Pod(
225 | self.api,
226 | {
227 | 'kind': 'Pod',
228 | 'apiVersion': 'v1',
229 | 'metadata': {
230 | 'generateName': "aimmo-%s-worker-%s-" % (self.game_id, player_id),
231 | 'labels': {
232 | 'app': 'aimmo-game-worker',
233 | 'game': self.game_id,
234 | 'player': str(player_id),
235 | },
236 | },
237 | 'spec': {
238 | 'containers': [
239 | {
240 | 'env': [
241 | {
242 | 'name': 'DATA_URL',
243 | 'value': "%s/player/%d" % (self.game_url, player_id),
244 | },
245 | ],
246 | 'name': 'aimmo-game-worker',
247 | 'image': 'ocadotechnology/aimmo-game-worker:%s' % os.environ.get('IMAGE_SUFFIX', 'latest'),
248 | 'ports': [
249 | {
250 | 'containerPort': 5000,
251 | 'protocol': 'TCP'
252 | }
253 | ],
254 | 'resources': {
255 | 'limits': {
256 | 'cpu': '10m',
257 | 'memory': '64Mi',
258 | },
259 | },
260 | },
261 | ],
262 | },
263 | }
264 | )
265 | pod.create()
266 | iterations = 0
267 | while pod.obj['status']['phase'] == 'Pending':
268 | if iterations > 30:
269 | raise EnvironmentError('Could not start worker %s, details %s' % (player_id, pod.obj))
270 | LOGGER.debug('Waiting for worker %s', player_id)
271 | time.sleep(5)
272 | pod.reload()
273 | iterations += 1
274 | worker_url = "http://%s:5000" % pod.obj['status']['podIP']
275 | LOGGER.info("Worker started for %s, listening at %s", player_id, worker_url)
276 | return worker_url
277 |
278 | def remove_worker(self, player_id):
279 | for pod in Pod.objects(self.api).filter(selector={
280 | 'app': 'aimmo-game-worker',
281 | 'game': self.game_id,
282 | 'player': str(player_id),
283 | }):
284 | LOGGER.debug('Removing pod %s', pod.obj['spec'])
285 | pod.delete()
286 |
287 | WORKER_MANAGERS = {
288 | 'local': LocalWorkerManager,
289 | 'kubernetes': KubernetesWorkerManager,
290 | }
291 |
--------------------------------------------------------------------------------