diff --git a/README.md b/README.md index cd3e73a..3fa970b 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ A web browser version of the classic strategy board game Diplomacy. -We are massive fans of the game and have been happily using some of the +We are fans of the game and enjoy playing on the existing web versions like [Play Diplomacy][play diplomacy], -[Backstabbr][backstabbr], and others for years. We decided to build a new +[Backstabbr][backstabbr], and others. We decided to build a new version of the game to try to bring together and improve on some of the features of the existing versions. We also wanted to make the project open source so that it could be maintained by an enthusiastic community of diplomacy diff --git a/adjudicator/named_coast.py b/adjudicator/named_coast.py index e81232b..fbb29e7 100644 --- a/adjudicator/named_coast.py +++ b/adjudicator/named_coast.py @@ -5,10 +5,15 @@ class NamedCoast: @register def __init__(self, state, id, name, parent, neighbours): + self.state = state self.id = id self.name = name self.parent = parent - self.neighbours = neighbours + self.neighbour_ids = neighbours def __str__(self): return f"{self.parent.name} ({self.name})" + + @property + def neighbours(self): + return [t for t in self.state.territories if t.id in self.neighbour_ids] diff --git a/adjudicator/processor.py b/adjudicator/processor.py index 31b8d79..6c815d0 100644 --- a/adjudicator/processor.py +++ b/adjudicator/processor.py @@ -109,6 +109,9 @@ def process(state): # Find all pieces that are not dislodged non_dislodged_pieces = [p for p in state.pieces if not p.dislodged] for piece in non_dislodged_pieces: + # Ignore pieces that move successfully + if piece.order.is_move and piece.order.outcome == Outcomes.SUCCEEDS: + continue if piece.nation != getattr(piece.territory, 'controlled_by', False): if not (piece.territory.is_sea): piece.territory.captured_by = piece.nation diff --git a/adjudicator/tests/data.py b/adjudicator/tests/data.py index f582690..75af407 100644 --- a/adjudicator/tests/data.py +++ b/adjudicator/tests/data.py @@ -186,29 +186,29 @@ def __init__(self, state): class NamedCoasts: def __init__(self, state, territories): self.SPAIN_SC = NamedCoast(state, 1, 'spain sc', territories.SPAIN, [ - territories.MARSEILLES, territories.PORTUGAL, - territories.MID_ATLANTIC, - territories.WESTERN_MEDITERRANEAN, territories.GULF_OF_LYON + territories.MARSEILLES.id, territories.PORTUGAL.id, + territories.MID_ATLANTIC.id, + territories.WESTERN_MEDITERRANEAN.id, territories.GULF_OF_LYON.id ]) self.SPAIN_NC = NamedCoast(state, 2, 'spain nc', territories.SPAIN, [ - territories.PORTUGAL, territories.MID_ATLANTIC, territories.GASCONY + territories.PORTUGAL.id, territories.MID_ATLANTIC.id, territories.GASCONY.id ]) self.BULGARIA_EC = NamedCoast(state, 3, 'bulgaria ec', territories.BULGARIA, [ - territories.BLACK_SEA, territories.RUMANIA, - territories.CONSTANTINOPLE, + territories.BLACK_SEA.id, territories.RUMANIA.id, + territories.CONSTANTINOPLE.id, ]) self.BULGARIA_SC = NamedCoast(state, 4, 'bulgaria sc', territories.BULGARIA, [ - territories.CONSTANTINOPLE, territories.AEGEAN_SEA, - territories.GREECE + territories.CONSTANTINOPLE.id, territories.AEGEAN_SEA.id, + territories.GREECE.id ]) self.ST_PETERSBURG_NC = NamedCoast(state, 5, 'st petersburg nc', territories.ST_PETERSBURG, [ - territories.BARRENTS_SEA, - territories.NORWAY + territories.BARRENTS_SEA.id, + territories.NORWAY.id ]) self.ST_PETERSBURG_SC = NamedCoast(state, 6, 'st petersburg nc', territories.ST_PETERSBURG, [ - territories.FINLAND, - territories.LIVONIA, - territories.GULF_OF_BOTHNIA + territories.FINLAND.id, + territories.LIVONIA.id, + territories.GULF_OF_BOTHNIA.id ]) diff --git a/adjudicator/tests/test_piece.py b/adjudicator/tests/test_piece.py index 981c985..4e422cf 100644 --- a/adjudicator/tests/test_piece.py +++ b/adjudicator/tests/test_piece.py @@ -84,8 +84,8 @@ def setUp(self): self.marseilles = CoastalTerritory(self.state, 12, 'Marseilles', 'France', [10], [10]) self.mid_atlantic = SeaTerritory(self.state, 13, 'Mid Atlantic', [10]) self.gulf_of_lyon = SeaTerritory(self.state, 14, 'Gulf of Lyon', [10]) - self.spain_north_coast = NamedCoast(self.state, 1, 'North Coast', self.spain, [self.gascony, self.mid_atlantic]) - self.spain_south_coast = NamedCoast(self.state, 2, 'South Coast', self.spain, [self.marseilles, self.gulf_of_lyon, self.marseilles]) + self.spain_north_coast = NamedCoast(self.state, 1, 'North Coast', self.spain, [self.gascony.id, self.mid_atlantic.id]) + self.spain_south_coast = NamedCoast(self.state, 2, 'South Coast', self.spain, [self.marseilles.id, self.gulf_of_lyon.id, self.marseilles.id]) to_register = [self.paris, self.london, self.wales, self.english_channel, self.brest, self.rome, diff --git a/core/game.py b/core/game.py index f1f37c4..2aa1a47 100644 --- a/core/game.py +++ b/core/game.py @@ -20,6 +20,12 @@ def process_turn(turn, dry_run=False): the turn the adjudicator. Update the turn based on the adjudicator response. """ + # Remove pending turnend + try: + turn.turnend.delete() + except models.TurnEnd.DoesNotExist: + pass + logger.info('Processing turn: {}'.format(turn)) turn_data = TurnSerializer(turn).data outcome = process_game_state(turn_data) @@ -40,6 +46,7 @@ def process_turn(turn, dry_run=False): winning_nation = new_turn.check_for_winning_nation() if winning_nation: turn.game.set_winner(winning_nation) + return new_turn diff --git a/core/migrations/0005_auto_20210423_0935.py b/core/migrations/0005_auto_20210423_0935.py new file mode 100644 index 0000000..5cdb972 --- /dev/null +++ b/core/migrations/0005_auto_20210423_0935.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.7 on 2021-04-23 08:35 + +from django.db import migrations + + +def add_st_petersburg_to_norway_shared_coasts(apps, schema_editor): + Territory = apps.get_model('core', 'Territory') + st_petersburg = Territory.objects.get(id='standard-st-petersburg') + norway = Territory.objects.get(id='standard-norway') + norway.shared_coasts.add(st_petersburg) + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_auto_20210422_1309'), + ] + + operations = [ + migrations.RunPython( + add_st_petersburg_to_norway_shared_coasts, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/core/migrations/0006_auto_20210423_1426.py b/core/migrations/0006_auto_20210423_1426.py new file mode 100644 index 0000000..7090e5d --- /dev/null +++ b/core/migrations/0006_auto_20210423_1426.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.7 on 2021-04-23 13:26 + +from django.db import migrations + + +def title_case_named_coast_names(apps, schema_editor): + NamedCoast = apps.get_model('core', 'NamedCoast') + for named_coast in NamedCoast.objects.all(): + named_coast.name = named_coast.name.title() + named_coast.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_auto_20210423_0935'), + ] + + operations = [ + migrations.RunPython( + title_case_named_coast_names, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/core/migrations/0007_auto_20210425_1732.py b/core/migrations/0007_auto_20210425_1732.py new file mode 100644 index 0000000..e5d0f58 --- /dev/null +++ b/core/migrations/0007_auto_20210425_1732.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.7 on 2021-04-25 16:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_auto_20210423_1426'), + ] + + operations = [ + migrations.AlterField( + model_name='game', + name='build_deadline', + field=models.CharField(choices=[(None, 'None'), ('twelve_hours', '12 hours'), ('twenty_four_hours', '24 hours'), ('two_days', '2 days'), ('three_days', '3 days'), ('five_days', '5 days'), ('seven_days', '7 days')], default='twelve_hours', max_length=100), + ), + migrations.AlterField( + model_name='game', + name='order_deadline', + field=models.CharField(choices=[(None, 'None'), ('twelve_hours', '12 hours'), ('twenty_four_hours', '24 hours'), ('two_days', '2 days'), ('three_days', '3 days'), ('five_days', '5 days'), ('seven_days', '7 days')], default='twenty_four_hours', max_length=100, null=True), + ), + migrations.AlterField( + model_name='game', + name='retreat_deadline', + field=models.CharField(choices=[(None, 'None'), ('twelve_hours', '12 hours'), ('twenty_four_hours', '24 hours'), ('two_days', '2 days'), ('three_days', '3 days'), ('five_days', '5 days'), ('seven_days', '7 days')], default='twenty_four_hours', max_length=100), + ), + ] diff --git a/core/migrations/0008_auto_20210426_1518.py b/core/migrations/0008_auto_20210426_1518.py new file mode 100644 index 0000000..98da29f --- /dev/null +++ b/core/migrations/0008_auto_20210426_1518.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.7 on 2021-04-26 14:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_auto_20210425_1732'), + ] + + operations = [ + migrations.AlterField( + model_name='game', + name='build_deadline', + field=models.CharField(choices=[(None, 'None'), ('twelve_hours', '12 hours'), ('twenty_four_hours', '24 hours'), ('two_days', '2 days'), ('three_days', '3 days'), ('five_days', '5 days'), ('seven_days', '7 days')], max_length=100, null=True), + ), + migrations.AlterField( + model_name='game', + name='order_deadline', + field=models.CharField(choices=[(None, 'None'), ('twelve_hours', '12 hours'), ('twenty_four_hours', '24 hours'), ('two_days', '2 days'), ('three_days', '3 days'), ('five_days', '5 days'), ('seven_days', '7 days')], max_length=100, null=True), + ), + migrations.AlterField( + model_name='game', + name='retreat_deadline', + field=models.CharField(choices=[(None, 'None'), ('twelve_hours', '12 hours'), ('twenty_four_hours', '24 hours'), ('two_days', '2 days'), ('three_days', '3 days'), ('five_days', '5 days'), ('seven_days', '7 days')], max_length=100, null=True), + ), + ] diff --git a/core/migrations/fixtures/territory.json b/core/migrations/fixtures/territory.json index 5f9fa07..9a6aca1 100644 --- a/core/migrations/fixtures/territory.json +++ b/core/migrations/fixtures/territory.json @@ -112,7 +112,6 @@ 31, 37, 51, - 75, 75 ], "shared_coasts":[ @@ -762,7 +761,6 @@ ], "controlled_by_initial":3, "nationality":3, - "nationality":3, "type":"coastal" } }, diff --git a/core/models/base.py b/core/models/base.py index 6d5eef3..9203f7c 100644 --- a/core/models/base.py +++ b/core/models/base.py @@ -16,6 +16,7 @@ class NationChoiceMode: class DeadlineFrequency: + NONE = None TWELVE_HOURS = timespans.TWELVE_HOURS.db_string TWENTY_FOUR_HOURS = timespans.TWENTY_FOUR_HOURS.db_string TWO_DAYS = timespans.TWO_DAYS.db_string @@ -23,6 +24,7 @@ class DeadlineFrequency: FIVE_DAYS = timespans.FIVE_DAYS.db_string SEVEN_DAYS = timespans.SEVEN_DAYS.db_string CHOICES = ( + (NONE, 'None'), timespans.TWELVE_HOURS.as_choice, timespans.TWENTY_FOUR_HOURS.as_choice, timespans.TWO_DAYS.as_choice, diff --git a/core/models/game.py b/core/models/game.py index 867294f..53a70ca 100644 --- a/core/models/game.py +++ b/core/models/game.py @@ -83,21 +83,18 @@ class Game(models.Model, AutoSlug): max_length=100, ) order_deadline = models.CharField( - null=False, + null=True, choices=DeadlineFrequency.CHOICES, - default=DeadlineFrequency.TWENTY_FOUR_HOURS, max_length=100, ) retreat_deadline = models.CharField( - null=False, + null=True, choices=DeadlineFrequency.CHOICES, - default=DeadlineFrequency.TWENTY_FOUR_HOURS, max_length=100, ) build_deadline = models.CharField( - null=False, + null=True, choices=DeadlineFrequency.CHOICES, - default=DeadlineFrequency.TWELVE_HOURS, max_length=100, ) process_on_finalized_orders = models.BooleanField( diff --git a/core/models/turn.py b/core/models/turn.py index b1e5a7d..3823791 100644 --- a/core/models/turn.py +++ b/core/models/turn.py @@ -38,9 +38,10 @@ def new(self, **kwargs): instance. """ turn = self.create(**kwargs) - td = timespan.get_timespan(turn.deadline).timedelta - turn_end_dt = timezone.now() + td - TurnEnd.objects.new(turn, turn_end_dt) + if turn.deadline: + td = timespan.get_timespan(turn.deadline).timedelta + turn_end_dt = timezone.now() + td + TurnEnd.objects.new(turn, turn_end_dt) return turn diff --git a/core/signals.py b/core/signals.py index fcf43af..ece2b6c 100644 --- a/core/signals.py +++ b/core/signals.py @@ -12,6 +12,7 @@ from core.models.base import DrawStatus, GameStatus from core.models.mixins import AutoSlug from core.utils.models import super_receiver +from project.celery import app def set_id(instance): @@ -72,6 +73,11 @@ def set_game_state_if_draw_accepted(sender, instance, **kwargs): game.set_winners(*nations) +@receiver(signals.pre_delete, sender=models.TurnEnd) +def revoke_task_on_delete(sender, instance, **kwargs): + app.control.revoke(instance.task_id) + + @receiver(reset_password_token_created) def password_reset_token_created(sender, instance, reset_password_token, *args, **kwargs): """ diff --git a/core/tests/__init__.py b/core/tests/__init__.py index 3550aa0..94495d9 100644 --- a/core/tests/__init__.py +++ b/core/tests/__init__.py @@ -14,6 +14,7 @@ apply_async_path = 'core.tasks.process_turn.apply_async' +revoke_task_on_delete_path = 'celery.app.control.Control.revoke' set_status_path = 'core.models.Draw.set_status' @@ -55,6 +56,11 @@ def patch_process_turn_apply_async(self): self.apply_async = apply_async.start() self.apply_async.return_value = AsyncResult(id=self.dummy_task_id) + def patch_revoke_task_on_delete(self): + self.revoke_task_on_delete_patcher = patch(revoke_task_on_delete_path) + self.revoke_task_on_delete_patch = self.revoke_task_on_delete_patcher.start() + self.addCleanup(self.revoke_task_on_delete_patcher.stop) + def patch_set_status(self): self.set_status_patcher = patch(set_status_path) self.set_status_patch = self.set_status_patcher.start() diff --git a/core/tests/test_adjudicator.py b/core/tests/test_adjudicator.py new file mode 100644 index 0000000..a475b36 --- /dev/null +++ b/core/tests/test_adjudicator.py @@ -0,0 +1,187 @@ +from django.test import TestCase + +from adjudicator.check import ArmyMovesToAdjacentTerritoryNotConvoy +from core import models +from core.game import process_turn +from core.models.base import GameStatus, OrderType, PieceType, Phase, Season +from . import DiplomacyTestCaseMixin + + +class TestAdjudicator(TestCase, DiplomacyTestCaseMixin): + + def setUp(self): + self.variant = models.Variant.objects.get(id='standard') + self.game = models.Game.objects.create( + num_players=7, + status=GameStatus.ACTIVE, + variant=self.variant, + ) + self.turn = models.Turn.objects.create( + current_turn=True, + game=self.game, + phase=Phase.ORDER, + season=Season.SPRING, + year=1900, + ) + self.england = models.Nation.objects.get(id='standard-england') + self.russia = models.Nation.objects.get(id='standard-russia') + self.turkey = models.Nation.objects.get(id='standard-turkey') + self.patch_process_turn_apply_async() + + def test_move_st_petersburg_south_coast_to_gulf_of_bothnia(self): + piece = models.Piece.objects.create( + game=self.game, + type=PieceType.FLEET, + nation=self.russia, + ) + st_petersburg = models.Territory.objects.get(id='standard-st-petersburg') + st_petersburg_south_coast = models.NamedCoast.objects.get(id='standard-st-petersburg-south-coast') + gulf_of_bothnia = models.Territory.objects.get(id='standard-gulf-of-bothnia') + for nation in models.Nation.objects.all(): + models.NationState.objects.create( + nation=nation, + turn=self.turn, + ) + for territory in models.Territory.objects.all(): + models.TerritoryState.objects.create( + territory=territory, + turn=self.turn, + ) + models.PieceState.objects.create( + named_coast=st_petersburg_south_coast, + piece=piece, + territory=st_petersburg, + turn=self.turn, + ) + order = models.Order.objects.create( + nation=self.russia, + source=st_petersburg, + target=gulf_of_bothnia, + turn=self.turn, + type=OrderType.MOVE, + ) + process_turn(self.turn) + order.refresh_from_db() + self.assertFalse(order.illegal) + + def test_st_petersburg_and_norway_shared_coast(self): + st_petersburg = models.Territory.objects.get(id='standard-st-petersburg') + st_petersburg_north_coast = models.NamedCoast.objects.get(id='standard-st-petersburg-north-coast') + norway = models.Territory.objects.get(id='standard-norway') + self.assertTrue(st_petersburg in norway.shared_coasts.all()) + self.assertTrue(st_petersburg in norway.neighbours.all()) + self.assertTrue(norway in st_petersburg.shared_coasts.all()) + self.assertTrue(norway in st_petersburg.neighbours.all()) + self.assertTrue(norway in st_petersburg_north_coast.neighbours.all()) + + def test_move_st_petersburg_north_coast_to_norway(self): + piece = models.Piece.objects.create( + game=self.game, + type=PieceType.FLEET, + nation=self.russia, + ) + st_petersburg = models.Territory.objects.get(id='standard-st-petersburg') + st_petersburg_north_coast = models.NamedCoast.objects.get(id='standard-st-petersburg-north-coast') + norway = models.Territory.objects.get(id='standard-norway') + for nation in models.Nation.objects.all(): + models.NationState.objects.create( + nation=nation, + turn=self.turn, + ) + for territory in models.Territory.objects.all(): + models.TerritoryState.objects.create( + territory=territory, + turn=self.turn, + ) + models.PieceState.objects.create( + named_coast=st_petersburg_north_coast, + piece=piece, + territory=st_petersburg, + turn=self.turn, + ) + order = models.Order.objects.create( + nation=self.russia, + source=st_petersburg, + target=norway, + turn=self.turn, + type=OrderType.MOVE, + ) + process_turn(self.turn) + order.refresh_from_db() + self.assertFalse(order.illegal) + + def test_move_from_liverpool_to_london(self): + piece = models.Piece.objects.create( + game=self.game, + type=PieceType.ARMY, + nation=self.england, + ) + liverpool = models.Territory.objects.get(id='standard-liverpool') + london = models.Territory.objects.get(id='standard-london') + for nation in models.Nation.objects.all(): + models.NationState.objects.create( + nation=nation, + turn=self.turn, + ) + for territory in models.Territory.objects.all(): + models.TerritoryState.objects.create( + territory=territory, + turn=self.turn, + ) + models.PieceState.objects.create( + piece=piece, + territory=liverpool, + turn=self.turn, + ) + order = models.Order.objects.create( + nation=self.england, + source=liverpool, + target=london, + turn=self.turn, + type=OrderType.MOVE, + ) + process_turn(self.turn) + order.refresh_from_db() + self.assertTrue(order.illegal) + self.assertEqual(order.illegal_code, ArmyMovesToAdjacentTerritoryNotConvoy.code) + self.assertEqual(order.illegal_verbose, ArmyMovesToAdjacentTerritoryNotConvoy.message) + + def test_turkey_does_not_take_bulgaria(self): + self.turn.season = Season.FALL + self.turn.save() + piece = models.Piece.objects.create( + game=self.game, + type=PieceType.ARMY, + nation=self.turkey, + ) + bulgaria = models.Territory.objects.get(id='standard-bulgaria') + greece = models.Territory.objects.get(id='standard-greece') + for nation in models.Nation.objects.all(): + models.NationState.objects.create( + nation=nation, + turn=self.turn, + ) + for territory in models.Territory.objects.all(): + models.TerritoryState.objects.create( + territory=territory, + turn=self.turn, + ) + models.PieceState.objects.create( + piece=piece, + territory=bulgaria, + turn=self.turn, + ) + order = models.Order.objects.create( + nation=self.turkey, + source=bulgaria, + target=greece, + turn=self.turn, + type=OrderType.MOVE, + ) + new_turn = process_turn(self.turn) + order.refresh_from_db() + self.assertFalse(order.illegal) + old_bulgaria_state = self.turn.territorystates.get(territory=bulgaria) + new_bulgaria_state = new_turn.territorystates.get(territory=bulgaria) + self.assertIsNone(old_bulgaria_state.captured_by) + self.assertIsNone(new_bulgaria_state.controlled_by) diff --git a/core/tests/test_game.py b/core/tests/test_game.py index 94026a0..f14fb22 100644 --- a/core/tests/test_game.py +++ b/core/tests/test_game.py @@ -2,7 +2,7 @@ from django.utils import timezone from core import factories, models -from core.models.base import GameStatus, Phase, Season +from core.models.base import DeadlineFrequency, GameStatus, Phase, Season from core.tests import DiplomacyTestCaseMixin @@ -18,6 +18,9 @@ def setUp(self): variant=self.variant, num_players=7, created_by=self.users[0], + order_deadline=DeadlineFrequency.TWENTY_FOUR_HOURS, + retreat_deadline=DeadlineFrequency.TWELVE_HOURS, + build_deadline=DeadlineFrequency.TWELVE_HOURS, ) self.game.participants.add(*self.users) self.patch_process_turn_apply_async() diff --git a/core/tests/test_set_turn_end.py b/core/tests/test_set_turn_end.py index 8d68aea..03fba08 100644 --- a/core/tests/test_set_turn_end.py +++ b/core/tests/test_set_turn_end.py @@ -35,6 +35,7 @@ def setUp(self): self.yesterday_string = self.yesterday.strftime(self.date_format) self.patch_process_turn_apply_async() + self.patch_revoke_task_on_delete() def call_command(self, slug, date_string): call_command(self.command, slug, date_string, stdout=self.out) diff --git a/core/tests/test_turn.py b/core/tests/test_turn.py index 7bb4dbe..d0d8689 100644 --- a/core/tests/test_turn.py +++ b/core/tests/test_turn.py @@ -135,11 +135,6 @@ def test_turn_end_none(self): turn = self.create_test_turn(game=self.game) self.assertIsNone(turn.turn_end) - def test_turn_end(self): - turn = self.create_test_turn(game=self.game, turn_end=True) - turn_end = models.TurnEnd.objects.get() - self.assertEqual(turn.turn_end, turn_end) - def test_deadline_order(self): self.game.order_deadline = DeadlineFrequency.SEVEN_DAYS self.game.save() @@ -317,9 +312,12 @@ def setUp(self): self.user = factories.UserFactory() self.game = self.create_test_game() self.game.participants.add(self.user) + self.patch_process_turn_apply_async() def test_new(self): self.assertEqual(models.TurnEnd.objects.count(), 0) + self.game.retreat_deadline = DeadlineFrequency.FIVE_DAYS + self.game.save() turn = models.Turn.objects.new( game=self.game, phase=Phase.RETREAT, @@ -329,3 +327,16 @@ def test_new(self): turn_end = models.TurnEnd.objects.get() self.assertEqual(turn_end.turn, turn) self.assertSimilarTimestamp(turn_end.datetime, self.tomorrow) + + def test_new_no_deadline(self): + self.assertEqual(models.TurnEnd.objects.count(), 0) + self.game.order_deadline = None + self.game.save() + turn = models.Turn.objects.new( + game=self.game, + phase=Phase.ORDER, + season=Season.SPRING, + year=1901, + ) + with self.assertRaises(models.TurnEnd.DoesNotExist): + turn.turnend diff --git a/core/tests/test_turn_end.py b/core/tests/test_turn_end.py index 3fda24b..97b7ef3 100644 --- a/core/tests/test_turn_end.py +++ b/core/tests/test_turn_end.py @@ -7,7 +7,7 @@ from core import factories, models from core.tasks import process_turn from core.tests import DiplomacyTestCaseMixin -from core.models.base import Phase, Season +from core.models.base import DeadlineFrequency, Phase, Season process_path = 'core.tasks._process_turn' @@ -23,8 +23,12 @@ def setUp(self): variant=self.variant, num_players=7, created_by=self.user, + order_deadline=DeadlineFrequency.TWENTY_FOUR_HOURS, + retreat_deadline=DeadlineFrequency.TWELVE_HOURS, + build_deadline=DeadlineFrequency.TWELVE_HOURS, ) self.patch_process_turn_apply_async() + self.patch_revoke_task_on_delete() def create_turn(self): return models.Turn.objects.create( diff --git a/fixtures/fixtures/game_1/named_coast.json b/fixtures/fixtures/game_1/named_coast.json index 95a143f..21d278c 100644 --- a/fixtures/fixtures/game_1/named_coast.json +++ b/fixtures/fixtures/game_1/named_coast.json @@ -3,7 +3,7 @@ "model": "core.namedcoast", "pk": 1, "fields": { - "name": "bulgaria east coast", + "name": "Bulgaria East Coast", "map_abbreviation": "ec", "parent": 73, "piece_starts_here": false, @@ -18,7 +18,7 @@ "model": "core.namedcoast", "pk": 2, "fields": { - "name": "bulgaria south coast", + "name": "Bulgaria South Coast", "map_abbreviation": "sc", "parent": 73, "piece_starts_here": false, @@ -33,7 +33,7 @@ "model": "core.namedcoast", "pk": 3, "fields": { - "name": "spain north coast", + "name": "Spain North Coast", "map_abbreviation": "nc", "parent": 74, "piece_starts_here": false, @@ -48,7 +48,7 @@ "model": "core.namedcoast", "pk": 4, "fields": { - "name": "spain south coast", + "name": "Spain South Coast", "map_abbreviation": "sc", "parent": 74, "piece_starts_here": false, @@ -65,7 +65,7 @@ "model": "core.namedcoast", "pk": 5, "fields": { - "name": "st. petersburg north coast", + "name": "St. Petersburg North Coast", "map_abbreviation": "nc", "parent": 75, "piece_starts_here": false, @@ -79,7 +79,7 @@ "model": "core.namedcoast", "pk": 6, "fields": { - "name": "st. petersburg south coast", + "name": "St. Petersburg South Coast", "map_abbreviation": "sc", "parent": 75, "piece_starts_here": true, diff --git a/fixtures/fixtures/game_1/territory.json b/fixtures/fixtures/game_1/territory.json index 5653f85..06bddc8 100644 --- a/fixtures/fixtures/game_1/territory.json +++ b/fixtures/fixtures/game_1/territory.json @@ -3,7 +3,7 @@ "model": "core.territory", "pk": 1, "fields": { - "name": "adriatic sea", + "name": "Adriatic Sea", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -24,7 +24,7 @@ "model": "core.territory", "pk": 2, "fields": { - "name": "aegean sea", + "name": "Aegean Sea", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -46,7 +46,7 @@ "model": "core.territory", "pk": 3, "fields": { - "name": "baltic sea", + "name": "Baltic Sea", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -69,7 +69,7 @@ "model": "core.territory", "pk": 4, "fields": { - "name": "barents sea", + "name": "Barents Sea", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -88,7 +88,7 @@ "model": "core.territory", "pk": 5, "fields": { - "name": "black sea", + "name": "Black Sea", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -109,7 +109,7 @@ "model": "core.territory", "pk": 6, "fields": { - "name": "gulf of bothnia", + "name": "Gulf Of Bothnia", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -130,7 +130,7 @@ "model": "core.territory", "pk": 7, "fields": { - "name": "eastern mediterranean", + "name": "Eastern Mediterranean", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -150,7 +150,7 @@ "model": "core.territory", "pk": 8, "fields": { - "name": "english channel", + "name": "English Channel", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -174,7 +174,7 @@ "model": "core.territory", "pk": 9, "fields": { - "name": "gulf of lyon", + "name": "Gulf Of Lyon", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -198,7 +198,7 @@ "model": "core.territory", "pk": 10, "fields": { - "name": "helgoland bight", + "name": "Helgoland Bight", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -218,7 +218,7 @@ "model": "core.territory", "pk": 11, "fields": { - "name": "ionian sea", + "name": "Ionian Sea", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -243,7 +243,7 @@ "model": "core.territory", "pk": 12, "fields": { - "name": "irish sea", + "name": "Irish Sea", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -265,7 +265,7 @@ "model": "core.territory", "pk": 13, "fields": { - "name": "mid-atlantic ocean", + "name": "Mid-Atlantic Ocean", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -292,7 +292,7 @@ "model": "core.territory", "pk": 14, "fields": { - "name": "north atlantic ocean", + "name": "North Atlantic Ocean", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -313,7 +313,7 @@ "model": "core.territory", "pk": 15, "fields": { - "name": "norwegian sea", + "name": "Norwegian Sea", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -335,7 +335,7 @@ "model": "core.territory", "pk": 16, "fields": { - "name": "north sea", + "name": "North Sea", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -362,7 +362,7 @@ "model": "core.territory", "pk": 17, "fields": { - "name": "skagerrak", + "name": "Skagerrak", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -382,7 +382,7 @@ "model": "core.territory", "pk": 18, "fields": { - "name": "tyrrhenian sea", + "name": "Tyrrhenian Sea", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -406,7 +406,7 @@ "model": "core.territory", "pk": 19, "fields": { - "name": "western mediterranean", + "name": "Western Mediterranean", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -428,7 +428,7 @@ "model": "core.territory", "pk": 20, "fields": { - "name": "albania", + "name": "Albania", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -452,7 +452,7 @@ "model": "core.territory", "pk": 21, "fields": { - "name": "ankara", + "name": "Ankara", "controlled_by_initial": 7, "variant": 1, "nationality": 7, @@ -475,7 +475,7 @@ "model": "core.territory", "pk": 22, "fields": { - "name": "apulia", + "name": "Apulia", "controlled_by_initial": 5, "variant": 1, "nationality": 5, @@ -499,7 +499,7 @@ "model": "core.territory", "pk": 23, "fields": { - "name": "armenia", + "name": "Armenia", "controlled_by_initial": 7, "variant": 1, "nationality": 7, @@ -522,7 +522,7 @@ "model": "core.territory", "pk": 24, "fields": { - "name": "berlin", + "name": "Berlin", "controlled_by_initial": 3, "variant": 1, "nationality": 3, @@ -546,7 +546,7 @@ "model": "core.territory", "pk": 25, "fields": { - "name": "belgium", + "name": "Belgium", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -571,7 +571,7 @@ "model": "core.territory", "pk": 26, "fields": { - "name": "brest", + "name": "Brest", "controlled_by_initial": 2, "variant": 1, "nationality": 2, @@ -595,7 +595,7 @@ "model": "core.territory", "pk": 27, "fields": { - "name": "clyde", + "name": "Clyde", "controlled_by_initial": 1, "variant": 1, "nationality": 1, @@ -619,7 +619,7 @@ "model": "core.territory", "pk": 28, "fields": { - "name": "constantinople", + "name": "Constantinople", "controlled_by_initial": 7, "variant": 1, "nationality": 7, @@ -644,7 +644,7 @@ "model": "core.territory", "pk": 29, "fields": { - "name": "denmark", + "name": "Denmark", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -669,7 +669,7 @@ "model": "core.territory", "pk": 30, "fields": { - "name": "edinburgh", + "name": "Edinburgh", "controlled_by_initial": 1, "variant": 1, "nationality": 1, @@ -693,7 +693,7 @@ "model": "core.territory", "pk": 31, "fields": { - "name": "finland", + "name": "Finland", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -716,7 +716,7 @@ "model": "core.territory", "pk": 32, "fields": { - "name": "gascony", + "name": "Gascony", "controlled_by_initial": 2, "variant": 1, "nationality": 2, @@ -741,7 +741,7 @@ "model": "core.territory", "pk": 33, "fields": { - "name": "greece", + "name": "Greece", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -765,7 +765,7 @@ "model": "core.territory", "pk": 34, "fields": { - "name": "holland", + "name": "Holland", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -789,7 +789,7 @@ "model": "core.territory", "pk": 35, "fields": { - "name": "kiel", + "name": "Kiel", "controlled_by_initial": 3, "variant": 1, "nationality": 3, @@ -816,7 +816,7 @@ "model": "core.territory", "pk": 36, "fields": { - "name": "london", + "name": "London", "controlled_by_initial": 1, "variant": 1, "nationality": 1, @@ -839,7 +839,7 @@ "model": "core.territory", "pk": 37, "fields": { - "name": "livonia", + "name": "Livonia", "controlled_by_initial": 6, "variant": 1, "nationality": 6, @@ -864,7 +864,7 @@ "model": "core.territory", "pk": 38, "fields": { - "name": "liverpool", + "name": "Liverpool", "controlled_by_initial": 1, "variant": 1, "nationality": 1, @@ -889,7 +889,7 @@ "model": "core.territory", "pk": 39, "fields": { - "name": "marseilles", + "name": "Marseilles", "controlled_by_initial": 2, "variant": 1, "nationality": 2, @@ -913,7 +913,7 @@ "model": "core.territory", "pk": 40, "fields": { - "name": "north africa", + "name": "North Africa", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -934,7 +934,7 @@ "model": "core.territory", "pk": 41, "fields": { - "name": "naples", + "name": "Naples", "controlled_by_initial": 5, "variant": 1, "nationality": 5, @@ -957,7 +957,7 @@ "model": "core.territory", "pk": 42, "fields": { - "name": "norway", + "name": "Norway", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -983,7 +983,7 @@ "model": "core.territory", "pk": 43, "fields": { - "name": "picardy", + "name": "Picardy", "controlled_by_initial": 2, "variant": 1, "nationality": 2, @@ -1007,7 +1007,7 @@ "model": "core.territory", "pk": 44, "fields": { - "name": "piedmont", + "name": "Piedmont", "controlled_by_initial": 5, "variant": 1, "nationality": 5, @@ -1031,7 +1031,7 @@ "model": "core.territory", "pk": 45, "fields": { - "name": "portugal", + "name": "Portugal", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -1051,7 +1051,7 @@ "model": "core.territory", "pk": 46, "fields": { - "name": "rome", + "name": "Rome", "controlled_by_initial": 5, "variant": 1, "nationality": 5, @@ -1075,7 +1075,7 @@ "model": "core.territory", "pk": 47, "fields": { - "name": "rumania", + "name": "Rumania", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -1102,7 +1102,7 @@ "model": "core.territory", "pk": 48, "fields": { - "name": "prussia", + "name": "Prussia", "controlled_by_initial": 3, "variant": 1, "nationality": 3, @@ -1126,7 +1126,7 @@ "model": "core.territory", "pk": 49, "fields": { - "name": "sevastopol", + "name": "Sevastopol", "controlled_by_initial": 6, "variant": 1, "nationality": 6, @@ -1150,7 +1150,7 @@ "model": "core.territory", "pk": 50, "fields": { - "name": "smyrna", + "name": "Smyrna", "controlled_by_initial": 7, "variant": 1, "nationality": 7, @@ -1175,7 +1175,7 @@ "model": "core.territory", "pk": 51, "fields": { - "name": "sweden", + "name": "Sweden", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -1201,7 +1201,7 @@ "model": "core.territory", "pk": 52, "fields": { - "name": "syria", + "name": "Syria", "controlled_by_initial": 7, "variant": 1, "nationality": 7, @@ -1223,7 +1223,7 @@ "model": "core.territory", "pk": 53, "fields": { - "name": "trieste", + "name": "Trieste", "controlled_by_initial": 4, "variant": 1, "nationality": 4, @@ -1250,7 +1250,7 @@ "model": "core.territory", "pk": 54, "fields": { - "name": "tunis", + "name": "Tunis", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -1272,7 +1272,7 @@ "model": "core.territory", "pk": 55, "fields": { - "name": "tuscany", + "name": "Tuscany", "controlled_by_initial": 5, "variant": 1, "nationality": 5, @@ -1296,7 +1296,7 @@ "model": "core.territory", "pk": 56, "fields": { - "name": "venice", + "name": "Venice", "controlled_by_initial": 5, "variant": 1, "nationality": 5, @@ -1323,7 +1323,7 @@ "model": "core.territory", "pk": 57, "fields": { - "name": "wales", + "name": "Wales", "controlled_by_initial": 1, "variant": 1, "nationality": 1, @@ -1347,7 +1347,7 @@ "model": "core.territory", "pk": 58, "fields": { - "name": "yorkshire", + "name": "Yorkshire", "controlled_by_initial": 1, "variant": 1, "nationality": 1, @@ -1371,7 +1371,7 @@ "model": "core.territory", "pk": 59, "fields": { - "name": "bohemia", + "name": "Bohemia", "controlled_by_initial": 4, "variant": 1, "nationality": 4, @@ -1392,7 +1392,7 @@ "model": "core.territory", "pk": 60, "fields": { - "name": "budapest", + "name": "Budapest", "controlled_by_initial": 4, "variant": 1, "nationality": 4, @@ -1413,7 +1413,7 @@ "model": "core.territory", "pk": 61, "fields": { - "name": "burgundy", + "name": "Burgundy", "controlled_by_initial": 2, "variant": 1, "nationality": 2, @@ -1436,7 +1436,7 @@ "model": "core.territory", "pk": 62, "fields": { - "name": "galicia", + "name": "Galicia", "controlled_by_initial": 4, "variant": 1, "nationality": 4, @@ -1459,7 +1459,7 @@ "model": "core.territory", "pk": 63, "fields": { - "name": "moscow", + "name": "Moscow", "controlled_by_initial": 6, "variant": 1, "nationality": 6, @@ -1481,7 +1481,7 @@ "model": "core.territory", "pk": 64, "fields": { - "name": "munich", + "name": "Munich", "controlled_by_initial": 3, "variant": 1, "nationality": 3, @@ -1504,7 +1504,7 @@ "model": "core.territory", "pk": 65, "fields": { - "name": "paris", + "name": "Paris", "controlled_by_initial": 2, "variant": 1, "nationality": 2, @@ -1524,7 +1524,7 @@ "model": "core.territory", "pk": 66, "fields": { - "name": "ruhr", + "name": "Ruhr", "controlled_by_initial": 3, "variant": 1, "nationality": 3, @@ -1545,7 +1545,7 @@ "model": "core.territory", "pk": 67, "fields": { - "name": "serbia", + "name": "Serbia", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -1567,7 +1567,7 @@ "model": "core.territory", "pk": 68, "fields": { - "name": "silesia", + "name": "Silesia", "controlled_by_initial": 3, "variant": 1, "nationality": 3, @@ -1589,7 +1589,7 @@ "model": "core.territory", "pk": 69, "fields": { - "name": "tyrolia", + "name": "Tyrolia", "controlled_by_initial": 4, "variant": 1, "nationality": 4, @@ -1611,7 +1611,7 @@ "model": "core.territory", "pk": 70, "fields": { - "name": "ukraine", + "name": "Ukraine", "controlled_by_initial": 6, "variant": 1, "nationality": 6, @@ -1632,7 +1632,7 @@ "model": "core.territory", "pk": 71, "fields": { - "name": "vienna", + "name": "Vienna", "controlled_by_initial": 4, "variant": 1, "nationality": 4, @@ -1653,7 +1653,7 @@ "model": "core.territory", "pk": 72, "fields": { - "name": "warsaw", + "name": "Warsaw", "controlled_by_initial": 6, "variant": 1, "nationality": 6, @@ -1675,7 +1675,7 @@ "model": "core.territory", "pk": 73, "fields": { - "name": "bulgaria", + "name": "Bulgaria", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -1701,7 +1701,7 @@ "model": "core.territory", "pk": 74, "fields": { - "name": "spain", + "name": "Spain", "controlled_by_initial": null, "variant": 1, "nationality": null, @@ -1729,7 +1729,7 @@ "model": "core.territory", "pk": 75, "fields": { - "name": "st. petersburg", + "name": "St. Petersburg", "controlled_by_initial": 6, "variant": 1, "nationality": 6, diff --git a/service/serializers.py b/service/serializers.py index bb2aa36..287e866 100644 --- a/service/serializers.py +++ b/service/serializers.py @@ -190,6 +190,7 @@ class Meta: fields = ( 'id', 'name', + 'variant', ) @@ -384,6 +385,10 @@ class Meta: 'piece_type', 'via_convoy', 'turn', + 'outcome', + 'illegal', + 'illegal_code', + 'illegal_verbose', ) read_only_fields = ( 'nation', diff --git a/service/tests/test_views.py b/service/tests/test_views.py index a801289..c3ee70f 100644 --- a/service/tests/test_views.py +++ b/service/tests/test_views.py @@ -9,8 +9,8 @@ from core import factories, models from core.tests import DiplomacyTestCaseMixin from core.models.base import ( - DrawStatus, DrawResponse, GameStatus, OrderType, Phase, PieceType, Season, - SurrenderStatus + DeadlineFrequency, DrawStatus, DrawResponse, GameStatus, OrderType, Phase, PieceType, + Season, SurrenderStatus ) from service import validators @@ -64,12 +64,9 @@ def test_list_games_converts_camel_to_snake_query_params(self): self.assertEqual(len(response.data), 1) -class TestGetCreateGame(BaseTestCase): +class TestCreateGame(BaseTestCase): def test_get_create_game(self): - """ - Get create game returns a form. - """ user = factories.UserFactory() self.client.force_authenticate(user=user) @@ -81,16 +78,10 @@ def test_get_create_game(self): ) def test_get_create_game_unauthenticated(self): - """ - Cannot get create game when unauthenticated. - """ url = reverse('create-game') response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - -class TestCreateGame(BaseTestCase): - def test_post_invalid_game(self): """ Posting invalid game data causes a 400 error. @@ -104,17 +95,15 @@ def test_post_invalid_game(self): self.assertEqual(response.status_code, 400) def test_post_valid_game(self): - """ - Posting valid game data creates a game instance and redirects to - `user-games` view. The user is automatically added as a participant of - the game. - """ user = factories.UserFactory() self.client.force_authenticate(user=user) variant = models.Variant.objects.get(id='standard') data = { 'name': 'Test Game', + 'order_deadline': DeadlineFrequency.TWENTY_FOUR_HOURS, + 'retreat_deadline': DeadlineFrequency.TWELVE_HOURS, + 'build_deadline': DeadlineFrequency.TWELVE_HOURS, } url = reverse('create-game') response = self.client.post(url, data, format='json') @@ -126,6 +115,9 @@ def test_post_valid_game(self): created_by=user, variant=variant, participants=user, + order_deadline=DeadlineFrequency.TWENTY_FOUR_HOURS, + retreat_deadline=DeadlineFrequency.TWELVE_HOURS, + build_deadline=DeadlineFrequency.TWELVE_HOURS, ) ) @@ -203,6 +195,36 @@ def test_join_game_initialise_game(self, mock_initialize): mock_initialize.assert_called() +class TestLeaveGame(BaseTestCase): + + def setUp(self): + self.data = {} + self.user = factories.UserFactory() + self.variant = models.Variant.objects.get(id='standard') + self.game = models.Game.objects.create( + status=GameStatus.PENDING, + variant=self.variant, + name='Test Game', + created_by=self.user, + num_players=7 + ) + self.url = reverse('toggle-join-game', args=[self.game.slug]) + self.game.participants.add(self.user) + self.client.force_authenticate(user=self.user) + + def test_leave_game_success(self): + response = self.client.patch(self.url, self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertFalse(self.user in self.game.participants.all()) + + def test_leave_game_already_started(self): + self.game.status = GameStatus.ACTIVE + self.game.save() + response = self.client.patch(self.url, self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertTrue(self.user in self.game.participants.all()) + + class TestGetGameState(BaseTestCase): def setUp(self): diff --git a/service/views.py b/service/views.py index 78b3178..c280a53 100644 --- a/service/views.py +++ b/service/views.py @@ -19,7 +19,7 @@ def get_game_filter_choices(): 'gameStatuses': models.base.GameStatus.CHOICES, 'nationChoiceModes': models.base.NationChoiceMode.CHOICES, 'deadlines': models.base.DeadlineFrequency.CHOICES, - 'variants': [(v.uid, str(v)) for v in models.Variant.objects.all()], + 'variants': [(v.id, str(v)) for v in models.Variant.objects.all()], } @@ -154,6 +154,11 @@ def check_object_permissions(self, request, obj): raise exceptions.PermissionDenied( detail='Game is not pending.' ) + else: + if obj.status != GameStatus.PENDING: + raise exceptions.PermissionDenied( + detail='Cannot leave game.' + ) class CreateOrderView(CamelCase, BaseMixin, generics.CreateAPIView, generics.DestroyAPIView):