From 2f56832defb40964361ee17906916873a222897f Mon Sep 17 00:00:00 2001 From: Michel Wilhelm Date: Fri, 6 Nov 2020 00:54:34 -0300 Subject: [PATCH 01/13] Adding the GameVent model and migration --- game/migrations/0003_gameevent.py | 73 +++++++++++++++++++++++++++++++ game/models.py | 49 ++++++++++++++++++--- 2 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 game/migrations/0003_gameevent.py diff --git a/game/migrations/0003_gameevent.py b/game/migrations/0003_gameevent.py new file mode 100644 index 0000000..e0fd74b --- /dev/null +++ b/game/migrations/0003_gameevent.py @@ -0,0 +1,73 @@ +# Generated by Django 3.1.3 on 2020-11-06 03:54 + +from django.db import migrations, models +import django.db.models.deletion +import django_mysql.models +import game.models +import internal.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ("game", "0002_auto_20201106_0225"), + ] + + operations = [ + migrations.CreateModel( + name="GameEvent", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Creation date" + ), + ), + ( + "type", + models.IntegerField( + choices=[ + (0, "CREATE_GAME"), + (1, "PAUSE"), + (2, "RESUME"), + (3, "CLICK_MINE"), + (4, "CLICK_POINT"), + (5, "CLICK_EMPTY"), + (6, "CLICK_FLAG"), + (7, "GAME_OVER"), + ], + default=game.models.EventTypes["CREATE_GAME"], + help_text="The game event", + ), + ), + ( + "metadata", + django_mysql.models.JSONField( + default=internal.utils.empty_object, + help_text="Some usefull event metadata", + verbose_name="Event metadata", + ), + ), + ( + "game", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="game.game" + ), + ), + ], + options={ + "verbose_name": "Game event", + "verbose_name_plural": "Game events", + "db_table": "game_events", + }, + ), + ] diff --git a/game/models.py b/game/models.py index 81c30d3..ecc765c 100644 --- a/game/models.py +++ b/game/models.py @@ -2,22 +2,26 @@ from enum import IntEnum from django.db import models from django_mysql.models import JSONField -from internal.utils import empty_list +from internal.utils import empty_list, empty_object from .game import Minesweeper -class GameStatuses(IntEnum): +class EnumChoicesBase(IntEnum): """ Enum was used as choices of Game.status because explicit is better than implicit """ - NOT_PLAYED = 0 - PLAYING = 1 - FINISHED = 2 - @classmethod def choices(cls): return [(key.value, key.name) for key in cls] +class GameStatuses(EnumChoicesBase): + """ Statuses used by the player and system on game """ + + NOT_PLAYED = 0 + PLAYING = 1 + FINISHED = 2 + + class Game(models.Model): created_at = models.DateTimeField("Creation date", auto_now_add=True) modified_at = models.DateTimeField("Last update", auto_now=True) @@ -63,3 +67,36 @@ class Game(models.Model): verbose_name = "Game" verbose_name_plural = "Games" db_table = "games" + + +class EventTypes(EnumChoicesBase): + """ Event types to generate a game timeline """ + + CREATE_GAME = 0 + PAUSE = 1 + RESUME = 2 + CLICK_MINE = 3 + CLICK_POINT = 4 + CLICK_EMPTY = 5 + CLICK_FLAG = 6 + GAME_OVER = 7 + + +class GameEvent(models.Model): + created_at = models.DateTimeField("Creation date", auto_now_add=True) + game = models.ForeignKey("game.Game", on_delete=models.CASCADE) + + type = models.IntegerField( + choices=EventTypes.choices(), + default=EventTypes.CREATE_GAME, + help_text="The game event", + ) + + metadata = JSONField( + "Event metadata", default=empty_object, help_text="Some usefull event metadata" + ) + + class Meta: + verbose_name = "Game event" + verbose_name_plural = "Game events" + db_table = "game_events" From 01c6b44427bdc6242ab895c2ddea0e9677704e75 Mon Sep 17 00:00:00 2001 From: Michel Wilhelm Date: Fri, 6 Nov 2020 01:16:59 -0300 Subject: [PATCH 02/13] Initializing the GameEvent endpoint --- api/resources/game.py | 18 +++++++++++++++++- api/serializers/__init__.py | 4 ++-- api/serializers/game.py | 8 +++++++- api/urls.py | 3 ++- game/models.py | 4 ++-- game/signals.py | 0 6 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 game/signals.py diff --git a/api/resources/game.py b/api/resources/game.py index c51ac25..0555f09 100644 --- a/api/resources/game.py +++ b/api/resources/game.py @@ -3,7 +3,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from game.models import Game -from ..serializers import GameSerializer +from ..serializers import GameSerializer, GameEventSerializer class GameResource(APIView): @@ -28,3 +28,19 @@ class GameSingleResource(APIView): serializer = GameSerializer(game) return Response(serializer.data, status=status.HTTP_200_OK) + + +class GameEventResource(APIView): + def post(self, request, game_id): + """ Creates a new event """ + + try: + Game.objects.get(pk=game_id) + except Game.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + serializer = GameEventSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/api/serializers/__init__.py b/api/serializers/__init__.py index 964a753..8872be8 100644 --- a/api/serializers/__init__.py +++ b/api/serializers/__init__.py @@ -1,4 +1,4 @@ -from .game import GameSerializer +from .game import GameSerializer, GameEventSerializer -__all__ = ["GameSerializer"] +__all__ = ["GameSerializer", "GameEventSerializer"] diff --git a/api/serializers/game.py b/api/serializers/game.py index 6b61620..c57a5ab 100644 --- a/api/serializers/game.py +++ b/api/serializers/game.py @@ -1,9 +1,15 @@ from rest_framework import serializers -from game.models import Game +from game.models import Game, GameEvent class GameSerializer(serializers.ModelSerializer): class Meta: model = Game fields = "__all__" + + +class GameEventSerializer(serializers.ModelSerializer): + class Meta: + model = GameEvent + fields = "__all__" diff --git a/api/urls.py b/api/urls.py index a1ff5ac..922fcaf 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,12 +1,13 @@ from django.urls import path from .resources.main import MainResource -from .resources.game import GameResource, GameSingleResource +from .resources.game import GameResource, GameSingleResource, GameEventResource app_name = "api" urlpatterns = [ + path("games//events", GameEventResource.as_view(), name="games_events"), path("games/", GameSingleResource.as_view(), name="games_single"), path("games", GameResource.as_view(), name="games"), path("", MainResource.as_view(), name="main"), diff --git a/game/models.py b/game/models.py index ecc765c..2c05314 100644 --- a/game/models.py +++ b/game/models.py @@ -72,7 +72,7 @@ class Game(models.Model): class EventTypes(EnumChoicesBase): """ Event types to generate a game timeline """ - CREATE_GAME = 0 + START_GAME = 0 PAUSE = 1 RESUME = 2 CLICK_MINE = 3 @@ -88,7 +88,7 @@ class GameEvent(models.Model): type = models.IntegerField( choices=EventTypes.choices(), - default=EventTypes.CREATE_GAME, + default=EventTypes.START_GAME, help_text="The game event", ) diff --git a/game/signals.py b/game/signals.py new file mode 100644 index 0000000..e69de29 From c80dc93447ffa6c868d0f96157e58802f3c16da4 Mon Sep 17 00:00:00 2001 From: Michel Wilhelm Date: Fri, 6 Nov 2020 01:44:34 -0300 Subject: [PATCH 03/13] GameEvent listing endpoint --- api/resources/game.py | 8 +++++++- game/__init__.py | 1 + game/apps.py | 3 +++ game/migrations/0003_gameevent.py | 4 ++-- game/models.py | 1 + game/signals.py | 11 +++++++++++ 6 files changed, 25 insertions(+), 3 deletions(-) diff --git a/api/resources/game.py b/api/resources/game.py index 0555f09..3cc1b64 100644 --- a/api/resources/game.py +++ b/api/resources/game.py @@ -2,7 +2,7 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView -from game.models import Game +from game.models import Game, GameEvent from ..serializers import GameSerializer, GameEventSerializer @@ -31,6 +31,12 @@ class GameSingleResource(APIView): class GameEventResource(APIView): + def get(self, request, game_id): + """ Returns a list of all game events """ + events = GameEvent.objects.filter(game_id=game_id) + serializer = GameEventSerializer(events, many=True) + return Response(serializer.data) + def post(self, request, game_id): """ Creates a new event """ diff --git a/game/__init__.py b/game/__init__.py index e69de29..6d8f478 100644 --- a/game/__init__.py +++ b/game/__init__.py @@ -0,0 +1 @@ +default_app_config = "game.apps.GameConfig" diff --git a/game/apps.py b/game/apps.py index 6198c24..d056f90 100644 --- a/game/apps.py +++ b/game/apps.py @@ -5,3 +5,6 @@ class GameConfig(AppConfig): name = "game" verbose_name = "Game" verbose_name_plural = "Games" + + def ready(self): + import game.signals # noqa diff --git a/game/migrations/0003_gameevent.py b/game/migrations/0003_gameevent.py index e0fd74b..14cfeae 100644 --- a/game/migrations/0003_gameevent.py +++ b/game/migrations/0003_gameevent.py @@ -36,7 +36,7 @@ class Migration(migrations.Migration): "type", models.IntegerField( choices=[ - (0, "CREATE_GAME"), + (0, "START_GAME"), (1, "PAUSE"), (2, "RESUME"), (3, "CLICK_MINE"), @@ -45,7 +45,7 @@ class Migration(migrations.Migration): (6, "CLICK_FLAG"), (7, "GAME_OVER"), ], - default=game.models.EventTypes["CREATE_GAME"], + default=game.models.EventTypes["START_GAME"], help_text="The game event", ), ), diff --git a/game/models.py b/game/models.py index 2c05314..41b55a1 100644 --- a/game/models.py +++ b/game/models.py @@ -97,6 +97,7 @@ class GameEvent(models.Model): ) class Meta: + ordering = ["created_at"] verbose_name = "Game event" verbose_name_plural = "Game events" db_table = "game_events" diff --git a/game/signals.py b/game/signals.py index e69de29..b846395 100644 --- a/game/signals.py +++ b/game/signals.py @@ -0,0 +1,11 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver + +from .models import Game, GameEvent, EventTypes + + +@receiver(post_save, sender=Game) +def game_start(sender, signal, instance, **kwargs): + """ If the game was just created, insert the first event START_GAME """ + + GameEvent.objects.get_or_create(game=instance, type=EventTypes.START_GAME) From e29a1a88e04b975462f32fc5399aa8db9328774b Mon Sep 17 00:00:00 2001 From: Michel Wilhelm Date: Fri, 6 Nov 2020 02:01:11 -0300 Subject: [PATCH 04/13] Adding CLICK_NAIVE event --- game/game.py | 5 ++- game/migrations/0004_auto_20201106_0453.py | 41 ++++++++++++++++++++++ game/models.py | 1 + 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 game/migrations/0004_auto_20201106_0453.py diff --git a/game/game.py b/game/game.py index ed5f5c2..4f65528 100644 --- a/game/game.py +++ b/game/game.py @@ -4,11 +4,14 @@ import random class Minesweeper: board = [] - def __init__(self, rows=10, cols=10, mines=5): + def __init__(self, rows=10, cols=10, mines=5, board=None): self.rows = rows self.cols = cols self.mines = mines + if board is not None: + self.board = board + def create_board(self): """ Creating the board cells with 0 as default value """ self.board = [[0 for col in range(self.cols)] for row in range(self.rows)] diff --git a/game/migrations/0004_auto_20201106_0453.py b/game/migrations/0004_auto_20201106_0453.py new file mode 100644 index 0000000..e391011 --- /dev/null +++ b/game/migrations/0004_auto_20201106_0453.py @@ -0,0 +1,41 @@ +# Generated by Django 3.1.3 on 2020-11-06 04:53 + +from django.db import migrations, models +import game.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game", "0003_gameevent"), + ] + + operations = [ + migrations.AlterModelOptions( + name="gameevent", + options={ + "ordering": ["created_at"], + "verbose_name": "Game event", + "verbose_name_plural": "Game events", + }, + ), + migrations.AlterField( + model_name="gameevent", + name="type", + field=models.IntegerField( + choices=[ + (0, "START_GAME"), + (1, "PAUSE"), + (2, "RESUME"), + (3, "CLICK_MINE"), + (4, "CLICK_POINT"), + (5, "CLICK_EMPTY"), + (6, "CLICK_FLAG"), + (7, "GAME_OVER"), + (8, "CLICK_NAIVE"), + ], + default=game.models.EventTypes["START_GAME"], + help_text="The game event", + ), + ), + ] diff --git a/game/models.py b/game/models.py index 41b55a1..779ab12 100644 --- a/game/models.py +++ b/game/models.py @@ -80,6 +80,7 @@ class EventTypes(EnumChoicesBase): CLICK_EMPTY = 5 CLICK_FLAG = 6 GAME_OVER = 7 + CLICK_NAIVE = 8 class GameEvent(models.Model): From adf12efd7504744b0f658eb08fc0708ad429f939 Mon Sep 17 00:00:00 2001 From: Michel Wilhelm Date: Fri, 6 Nov 2020 13:22:28 -0300 Subject: [PATCH 05/13] Added signal for identify what is on the click location --- game/game.py | 16 +++++++++++++++- game/signals.py | 26 +++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/game/game.py b/game/game.py index 4f65528..adfee36 100644 --- a/game/game.py +++ b/game/game.py @@ -65,12 +65,26 @@ class Minesweeper: self.increment_safe_point(mine_position_row - 1, mine_position_col - 1) def is_mine(self, row, col): - """ Checks whether the given location is a mine or not """ + """ Checks whether the given location have a mine """ try: return self.board[row][col] == -1 except IndexError: return False + def is_empty(self, row, col): + """ Checks whether the given location is empty """ + try: + return self.board[row][col] == 0 + except IndexError: + return False + + def is_point(self, row, col): + """ Checks whether the given location have pontuation """ + try: + return self.board[row][col] > 0 + except IndexError: + return False + def is_point_in_board(self, row, col): """ Checks whether the location is inside board """ if row in range(0, self.rows) and col in range(0, self.cols): diff --git a/game/signals.py b/game/signals.py index b846395..5f83578 100644 --- a/game/signals.py +++ b/game/signals.py @@ -1,7 +1,8 @@ -from django.db.models.signals import post_save +from django.db.models.signals import post_save, pre_save from django.dispatch import receiver from .models import Game, GameEvent, EventTypes +from .game import Minesweeper @receiver(post_save, sender=Game) @@ -9,3 +10,26 @@ def game_start(sender, signal, instance, **kwargs): """ If the game was just created, insert the first event START_GAME """ GameEvent.objects.get_or_create(game=instance, type=EventTypes.START_GAME) + + +@receiver(pre_save, sender=GameEvent) +def identify_click_event(sender, signal, instance, **kwargs): + """ Verify what is on the naive click: mine, point or empty """ + if not instance.type == EventTypes.CLICK_NAIVE: + return + + ms = Minesweeper( + instance.game.rows, + instance.game.cols, + instance.game.mines, + instance.game.board, + ) + + if ms.is_mine(instance.metadata["row"], instance.metadata["col"]): + instance.type = EventTypes.CLICK_MINE + + elif ms.is_empty(instance.metadata["row"], instance.metadata["col"]): + instance.type = EventTypes.CLICK_EMPTY + + elif ms.is_point(instance.metadata["row"], instance.metadata["col"]): + instance.type = EventTypes.CLICK_POINT From 197624d7c83ab75ece848742370cfadf145d2072 Mon Sep 17 00:00:00 2001 From: Michel Wilhelm Date: Fri, 6 Nov 2020 20:58:47 -0300 Subject: [PATCH 06/13] Using row and col integer is better than metadata with jsonfield --- api/resources/game.py | 11 ++++- game/admin.py | 29 ++++++++++++ game/migrations/0005_auto_20201106_2357.py | 51 ++++++++++++++++++++++ game/models.py | 20 +++++++-- game/signals.py | 32 +++++++++++++- 5 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 game/migrations/0005_auto_20201106_2357.py diff --git a/api/resources/game.py b/api/resources/game.py index 3cc1b64..90a1792 100644 --- a/api/resources/game.py +++ b/api/resources/game.py @@ -2,7 +2,7 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView -from game.models import Game, GameEvent +from game.models import Game, GameEvent, GameStatuses from ..serializers import GameSerializer, GameEventSerializer @@ -41,9 +41,16 @@ class GameEventResource(APIView): """ Creates a new event """ try: - Game.objects.get(pk=game_id) + game = Game.objects.get(pk=game_id) except Game.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) + print(game.status, game.status == GameStatuses.FINISHED) + if game.status == GameStatuses.FINISHED: + print("WTF, DEVERIA PASSAR AQUI") + return Response( + {"message": "Game is already finished"}, + status=status.HTTP_412_PRECONDITION_FAILED, + ) serializer = GameEventSerializer(data=request.data) if serializer.is_valid(): diff --git a/game/admin.py b/game/admin.py index e69de29..60e6a79 100644 --- a/game/admin.py +++ b/game/admin.py @@ -0,0 +1,29 @@ +from django.contrib import admin + +from .models import Game, GameEvent + + +@admin.register(Game) +class GameAdmin(admin.ModelAdmin): + list_display = ( + "id", + "created_at", + "modified_at", + "rows", + "cols", + "mines", + "win", + "status", + ) + + list_filter = ( + "win", + "status", + ) + + +@admin.register(GameEvent) +class GameEventAdmin(admin.ModelAdmin): + list_display = ("id", "created_at", "game", "type", "event_row", "event_col") + + list_filter = ("type",) diff --git a/game/migrations/0005_auto_20201106_2357.py b/game/migrations/0005_auto_20201106_2357.py new file mode 100644 index 0000000..83e1568 --- /dev/null +++ b/game/migrations/0005_auto_20201106_2357.py @@ -0,0 +1,51 @@ +# Generated by Django 3.1.3 on 2020-11-06 23:57 + +from django.db import migrations, models +import game.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game", "0004_auto_20201106_0453"), + ] + + operations = [ + migrations.RemoveField(model_name="gameevent", name="metadata",), + migrations.AddField( + model_name="gameevent", + name="event_col", + field=models.PositiveIntegerField( + blank=True, + default=None, + help_text="Column on the board where the event occurred, if applicable", + null=True, + verbose_name="The column clicked", + ), + ), + migrations.AddField( + model_name="gameevent", + name="event_row", + field=models.PositiveIntegerField( + blank=True, + default=None, + help_text="Row on the board where the event occurred, if applicable", + null=True, + verbose_name="The row clicked", + ), + ), + migrations.AlterField( + model_name="game", + name="status", + field=models.IntegerField( + choices=[ + (0, "NOT_PLAYED"), + (1, "PLAYING"), + (2, "PAUSED"), + (3, "FINISHED"), + ], + default=game.models.GameStatuses["NOT_PLAYED"], + help_text="Actual game status", + ), + ), + ] diff --git a/game/models.py b/game/models.py index 779ab12..3a51d09 100644 --- a/game/models.py +++ b/game/models.py @@ -2,7 +2,7 @@ from enum import IntEnum from django.db import models from django_mysql.models import JSONField -from internal.utils import empty_list, empty_object +from internal.utils import empty_list from .game import Minesweeper @@ -19,7 +19,8 @@ class GameStatuses(EnumChoicesBase): NOT_PLAYED = 0 PLAYING = 1 - FINISHED = 2 + PAUSED = 2 + FINISHED = 3 class Game(models.Model): @@ -93,8 +94,19 @@ class GameEvent(models.Model): help_text="The game event", ) - metadata = JSONField( - "Event metadata", default=empty_object, help_text="Some usefull event metadata" + event_row = models.PositiveIntegerField( + "The row clicked", + default=None, + null=True, + blank=True, + help_text="Row on the board where the event occurred, if applicable", + ) + event_col = models.PositiveIntegerField( + "The column clicked", + default=None, + null=True, + blank=True, + help_text="Column on the board where the event occurred, if applicable", ) class Meta: diff --git a/game/signals.py b/game/signals.py index 5f83578..8ca2152 100644 --- a/game/signals.py +++ b/game/signals.py @@ -1,17 +1,45 @@ from django.db.models.signals import post_save, pre_save from django.dispatch import receiver -from .models import Game, GameEvent, EventTypes +from .models import Game, GameEvent, EventTypes, GameStatuses from .game import Minesweeper @receiver(post_save, sender=Game) def game_start(sender, signal, instance, **kwargs): """ If the game was just created, insert the first event START_GAME """ - + if not instance.status == GameStatuses.NOT_PLAYED: + return GameEvent.objects.get_or_create(game=instance, type=EventTypes.START_GAME) +@receiver(pre_save, sender=GameEvent) +def mark_game_as_playing(sender, signal, instance, **kwargs): + """ When event match, mark the game instance as playing """ + + playing_events = [ + EventTypes.START_GAME, + EventTypes.RESUME, + EventTypes.CLICK_MINE, + EventTypes.CLICK_POINT, + EventTypes.CLICK_EMPTY, + EventTypes.CLICK_FLAG, + ] + + if instance.type in playing_events: + instance.game.status = GameStatuses.PLAYING + instance.game.save() + + elif instance.type == EventTypes.PAUSE: + instance.game.status = GameStatuses.PAUSED + instance.game.save() + + if instance.type == EventTypes.CLICK_MINE: + instance.game.status = GameStatuses.FINISHED + instance.game.win = False + instance.game.save() + + @receiver(pre_save, sender=GameEvent) def identify_click_event(sender, signal, instance, **kwargs): """ Verify what is on the naive click: mine, point or empty """ From 2e40d7032c497c181fc11fced3c8a15d8c6f4e60 Mon Sep 17 00:00:00 2001 From: Michel Wilhelm Date: Fri, 6 Nov 2020 21:12:57 -0300 Subject: [PATCH 07/13] Create event for the same position is not allowed --- api/resources/game.py | 12 ++++++++++-- game/admin.py | 2 +- game/migrations/0006_auto_20201107_0010.py | 19 +++++++++++++++++++ game/models.py | 4 ++-- game/signals.py | 9 ++++++--- 5 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 game/migrations/0006_auto_20201107_0010.py diff --git a/api/resources/game.py b/api/resources/game.py index 90a1792..ac703e6 100644 --- a/api/resources/game.py +++ b/api/resources/game.py @@ -44,14 +44,22 @@ class GameEventResource(APIView): game = Game.objects.get(pk=game_id) except Game.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) - print(game.status, game.status == GameStatuses.FINISHED) + if game.status == GameStatuses.FINISHED: - print("WTF, DEVERIA PASSAR AQUI") return Response( {"message": "Game is already finished"}, status=status.HTTP_412_PRECONDITION_FAILED, ) + row = request.data.get("row") + col = request.data.get("col") + game_event = GameEvent.objects.filter(game=game, row=row, col=col).first() + if game_event: + return Response( + {"message": "This event was already registered"}, + status=status.HTTP_409_CONFLICT, + ) + serializer = GameEventSerializer(data=request.data) if serializer.is_valid(): serializer.save() diff --git a/game/admin.py b/game/admin.py index 60e6a79..f83268e 100644 --- a/game/admin.py +++ b/game/admin.py @@ -24,6 +24,6 @@ class GameAdmin(admin.ModelAdmin): @admin.register(GameEvent) class GameEventAdmin(admin.ModelAdmin): - list_display = ("id", "created_at", "game", "type", "event_row", "event_col") + list_display = ("id", "created_at", "game", "type", "row", "col") list_filter = ("type",) diff --git a/game/migrations/0006_auto_20201107_0010.py b/game/migrations/0006_auto_20201107_0010.py new file mode 100644 index 0000000..7b2dc25 --- /dev/null +++ b/game/migrations/0006_auto_20201107_0010.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.3 on 2020-11-07 00:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("game", "0005_auto_20201106_2357"), + ] + + operations = [ + migrations.RenameField( + model_name="gameevent", old_name="event_col", new_name="col", + ), + migrations.RenameField( + model_name="gameevent", old_name="event_row", new_name="row", + ), + ] diff --git a/game/models.py b/game/models.py index 3a51d09..7529f71 100644 --- a/game/models.py +++ b/game/models.py @@ -94,14 +94,14 @@ class GameEvent(models.Model): help_text="The game event", ) - event_row = models.PositiveIntegerField( + row = models.PositiveIntegerField( "The row clicked", default=None, null=True, blank=True, help_text="Row on the board where the event occurred, if applicable", ) - event_col = models.PositiveIntegerField( + col = models.PositiveIntegerField( "The column clicked", default=None, null=True, diff --git a/game/signals.py b/game/signals.py index 8ca2152..1c4d972 100644 --- a/game/signals.py +++ b/game/signals.py @@ -46,6 +46,9 @@ def identify_click_event(sender, signal, instance, **kwargs): if not instance.type == EventTypes.CLICK_NAIVE: return + if instance.row is None and instance.col is None: + return + ms = Minesweeper( instance.game.rows, instance.game.cols, @@ -53,11 +56,11 @@ def identify_click_event(sender, signal, instance, **kwargs): instance.game.board, ) - if ms.is_mine(instance.metadata["row"], instance.metadata["col"]): + if ms.is_mine(instance.row, instance.col): instance.type = EventTypes.CLICK_MINE - elif ms.is_empty(instance.metadata["row"], instance.metadata["col"]): + elif ms.is_empty(instance.row, instance.col): instance.type = EventTypes.CLICK_EMPTY - elif ms.is_point(instance.metadata["row"], instance.metadata["col"]): + elif ms.is_point(instance.row, instance.col): instance.type = EventTypes.CLICK_POINT From 1682d7f4a658fc1fecc2d749025a04b13dce4d02 Mon Sep 17 00:00:00 2001 From: Michel Wilhelm Date: Fri, 6 Nov 2020 22:13:38 -0300 Subject: [PATCH 08/13] Better signals control --- game/models.py | 2 +- game/signals.py | 53 ++++++++++++++++++++++++------------------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/game/models.py b/game/models.py index 7529f71..d3754bc 100644 --- a/game/models.py +++ b/game/models.py @@ -38,7 +38,7 @@ class Game(models.Model): ) board = JSONField( - "Generated board", default=empty_list, help_text="Whe generated board game" + "Generated board", default=empty_list, help_text="The generated board game" ) win = models.BooleanField( "Win?", diff --git a/game/signals.py b/game/signals.py index 1c4d972..f999f41 100644 --- a/game/signals.py +++ b/game/signals.py @@ -13,33 +13,6 @@ def game_start(sender, signal, instance, **kwargs): GameEvent.objects.get_or_create(game=instance, type=EventTypes.START_GAME) -@receiver(pre_save, sender=GameEvent) -def mark_game_as_playing(sender, signal, instance, **kwargs): - """ When event match, mark the game instance as playing """ - - playing_events = [ - EventTypes.START_GAME, - EventTypes.RESUME, - EventTypes.CLICK_MINE, - EventTypes.CLICK_POINT, - EventTypes.CLICK_EMPTY, - EventTypes.CLICK_FLAG, - ] - - if instance.type in playing_events: - instance.game.status = GameStatuses.PLAYING - instance.game.save() - - elif instance.type == EventTypes.PAUSE: - instance.game.status = GameStatuses.PAUSED - instance.game.save() - - if instance.type == EventTypes.CLICK_MINE: - instance.game.status = GameStatuses.FINISHED - instance.game.win = False - instance.game.save() - - @receiver(pre_save, sender=GameEvent) def identify_click_event(sender, signal, instance, **kwargs): """ Verify what is on the naive click: mine, point or empty """ @@ -64,3 +37,29 @@ def identify_click_event(sender, signal, instance, **kwargs): elif ms.is_point(instance.row, instance.col): instance.type = EventTypes.CLICK_POINT + + +@receiver(post_save, sender=GameEvent) +def create_post_save_game_event(sender, signal, instance, **kwargs): + playing_events = [ + EventTypes.START_GAME, + EventTypes.RESUME, + EventTypes.CLICK_POINT, + EventTypes.CLICK_EMPTY, + EventTypes.CLICK_FLAG, + ] + + if instance.type in playing_events: + instance.game.status = GameStatuses.PLAYING + instance.game.save() + + if instance.type == EventTypes.PAUSE: + instance.game.status = GameStatuses.PAUSED + instance.game.save() + + if instance.type == EventTypes.CLICK_MINE: + instance.game.status = GameStatuses.FINISHED + instance.game.win = False + instance.game.save() + + GameEvent(game=instance.game, type=EventTypes.GAME_OVER).save() From 4b191ca06de68c15b0dfb4e8aaadec49a7b88320 Mon Sep 17 00:00:00 2001 From: Michel Wilhelm Date: Fri, 6 Nov 2020 22:31:34 -0300 Subject: [PATCH 09/13] Adding a board progress to Game model --- game/migrations/0007_auto_20201107_0130.py | 33 ++++++++++++++++++++++ game/models.py | 6 ++++ 2 files changed, 39 insertions(+) create mode 100644 game/migrations/0007_auto_20201107_0130.py diff --git a/game/migrations/0007_auto_20201107_0130.py b/game/migrations/0007_auto_20201107_0130.py new file mode 100644 index 0000000..056e56f --- /dev/null +++ b/game/migrations/0007_auto_20201107_0130.py @@ -0,0 +1,33 @@ +# Generated by Django 3.1.3 on 2020-11-07 01:30 + +from django.db import migrations +import django_mysql.models +import internal.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ("game", "0006_auto_20201107_0010"), + ] + + operations = [ + migrations.AddField( + model_name="game", + name="board_progress", + field=django_mysql.models.JSONField( + default=internal.utils.empty_list, + help_text="This board is updated at each GameEvent recorded", + verbose_name="Progress board", + ), + ), + migrations.AlterField( + model_name="game", + name="board", + field=django_mysql.models.JSONField( + default=internal.utils.empty_list, + help_text="The generated board game", + verbose_name="Generated board", + ), + ), + ] diff --git a/game/models.py b/game/models.py index d3754bc..f9726b0 100644 --- a/game/models.py +++ b/game/models.py @@ -40,6 +40,12 @@ class Game(models.Model): board = JSONField( "Generated board", default=empty_list, help_text="The generated board game" ) + board_progress = JSONField( + "Progress board", + default=empty_list, + help_text="This board is updated at each GameEvent recorded", + ) + win = models.BooleanField( "Win?", default=None, From 7e8f65a5b73065134e0b76dcd25d68c7a68d3170 Mon Sep 17 00:00:00 2001 From: Michel Wilhelm Date: Sat, 7 Nov 2020 00:11:05 -0300 Subject: [PATCH 10/13] Identifying the win status --- game/game.py | 21 ++++++++++++++++++++- game/models.py | 1 + game/signals.py | 37 ++++++++++++++++++++++++++++++++----- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/game/game.py b/game/game.py index adfee36..02b98a5 100644 --- a/game/game.py +++ b/game/game.py @@ -4,7 +4,7 @@ import random class Minesweeper: board = [] - def __init__(self, rows=10, cols=10, mines=5, board=None): + def __init__(self, rows=10, cols=10, mines=5, board=None, board_progress=None): self.rows = rows self.cols = cols self.mines = mines @@ -12,9 +12,15 @@ class Minesweeper: if board is not None: self.board = board + if board_progress is not None: + self.board_progress = board_progress + def create_board(self): """ Creating the board cells with 0 as default value """ self.board = [[0 for col in range(self.cols)] for row in range(self.rows)] + self.board_progress = [ + ["-" for col in range(self.cols)] for row in range(self.rows) + ] def put_mine(self): """Put a single mine on the board. @@ -104,3 +110,16 @@ class Minesweeper: # Increment the value of the position becaus is close to some mine self.board[row][col] += 1 + + def reveal(self, row, col): + self.board_progress[row][col] = self.board[row][col] + + def win(self): + """ Identify if the player won the game """ + unrevealed = 0 + for row in self.board_progress: + for cell in row: + if cell == "-": + unrevealed += 1 + if (unrevealed - self.mines) == 0: + return True diff --git a/game/models.py b/game/models.py index f9726b0..f8f56ce 100644 --- a/game/models.py +++ b/game/models.py @@ -67,6 +67,7 @@ class Game(models.Model): ms.create_board() ms.put_mines() self.board = ms.board + self.progress_board = ms.progress_board super(Game, self).save(*args, **kwargs) diff --git a/game/signals.py b/game/signals.py index f999f41..f1c1d7a 100644 --- a/game/signals.py +++ b/game/signals.py @@ -41,6 +41,25 @@ def identify_click_event(sender, signal, instance, **kwargs): @receiver(post_save, sender=GameEvent) def create_post_save_game_event(sender, signal, instance, **kwargs): + ms = Minesweeper( + instance.game.rows, + instance.game.cols, + instance.game.mines, + instance.game.board, + instance.game.board_progress, + ) + game_changed = False + + reveal_events = [ + EventTypes.CLICK_POINT, + EventTypes.CLICK_EMPTY, + EventTypes.CLICK_MINE, + ] + if instance.type in reveal_events: + ms.reveal(instance.row, instance.col) + instance.game.board_progress = ms.board_progress + game_changed = True + playing_events = [ EventTypes.START_GAME, EventTypes.RESUME, @@ -51,15 +70,23 @@ def create_post_save_game_event(sender, signal, instance, **kwargs): if instance.type in playing_events: instance.game.status = GameStatuses.PLAYING - instance.game.save() + game_changed = True - if instance.type == EventTypes.PAUSE: + elif instance.type == EventTypes.PAUSE: instance.game.status = GameStatuses.PAUSED - instance.game.save() + game_changed = True - if instance.type == EventTypes.CLICK_MINE: + elif instance.type == EventTypes.CLICK_MINE: instance.game.status = GameStatuses.FINISHED instance.game.win = False - instance.game.save() + game_changed = True GameEvent(game=instance.game, type=EventTypes.GAME_OVER).save() + + if ms.win() is True: + instance.game.status = GameStatuses.FINISHED + instance.game.win = True + game_changed = True + + if game_changed is True: + instance.game.save() From 0c9f847865d6642becca2433f538e0fc36bcd2fe Mon Sep 17 00:00:00 2001 From: Michel Wilhelm Date: Sat, 7 Nov 2020 00:29:00 -0300 Subject: [PATCH 11/13] Hide generated board from client --- api/serializers/game.py | 2 +- game/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/serializers/game.py b/api/serializers/game.py index c57a5ab..5d5a717 100644 --- a/api/serializers/game.py +++ b/api/serializers/game.py @@ -6,7 +6,7 @@ from game.models import Game, GameEvent class GameSerializer(serializers.ModelSerializer): class Meta: model = Game - fields = "__all__" + exclude = ["board"] class GameEventSerializer(serializers.ModelSerializer): diff --git a/game/models.py b/game/models.py index f8f56ce..7def6f9 100644 --- a/game/models.py +++ b/game/models.py @@ -67,7 +67,7 @@ class Game(models.Model): ms.create_board() ms.put_mines() self.board = ms.board - self.progress_board = ms.progress_board + self.board_progress = ms.board_progress super(Game, self).save(*args, **kwargs) From 928cb8ffcae243e97ba03da169f896ec84c6ca57 Mon Sep 17 00:00:00 2001 From: Michel Wilhelm Date: Sat, 7 Nov 2020 15:34:59 -0300 Subject: [PATCH 12/13] Adding django-cors-headers to Django for development --- requirements.txt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7a42880..616cc9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,23 +7,24 @@ appdirs==1.4.4 # via -r requirements.txt, black, virtualenv asgiref==3.3.0 # via -r requirements.txt, django black==20.8b1 # via -r requirements.txt -cfgv==3.2.0 # via pre-commit +cfgv==3.2.0 # via -r requirements.txt, pre-commit click==7.1.2 # via -r requirements.txt, black -distlib==0.3.1 # via virtualenv +distlib==0.3.1 # via -r requirements.txt, virtualenv +django-cors-headers==3.5.0 # via -r requirements.txt django-dbml==0.3.5 # via -r requirements.txt django-mysql==3.9.0 # via -r requirements.txt -django==3.1.3 # via -r requirements.txt, django-mysql, djangorestframework +django==3.1.3 # via -r requirements.txt, django-cors-headers, django-mysql, djangorestframework djangorestframework==3.12.1 # via -r requirements.txt -filelock==3.0.12 # via virtualenv +filelock==3.0.12 # via -r requirements.txt, virtualenv flake8==3.8.4 # via -r requirements.txt gevent==20.9.0 # via -r requirements.txt greenlet==0.4.17 # via -r requirements.txt, gevent -identify==1.5.9 # via pre-commit +identify==1.5.9 # via -r requirements.txt, pre-commit importlib-metadata==2.0.0 # via -r requirements.txt, flake8, pre-commit, virtualenv mccabe==0.6.1 # via -r requirements.txt, flake8 mypy-extensions==0.4.3 # via -r requirements.txt, black mysqlclient==2.0.1 # via -r requirements.txt -nodeenv==1.5.0 # via pre-commit +nodeenv==1.5.0 # via -r requirements.txt, pre-commit pathspec==0.8.0 # via -r requirements.txt, black pendulum==2.1.2 # via -r requirements.txt pre-commit==2.8.2 # via -r requirements.txt @@ -32,7 +33,7 @@ pyflakes==2.2.0 # via -r requirements.txt, flake8 python-dateutil==2.8.1 # via -r requirements.txt, pendulum pytz==2020.4 # via -r requirements.txt, django pytzdata==2020.1 # via -r requirements.txt, pendulum -pyyaml==5.3.1 # via pre-commit +pyyaml==5.3.1 # via -r requirements.txt, pre-commit regex==2020.10.28 # via -r requirements.txt, black six==1.15.0 # via -r requirements.txt, python-dateutil, virtualenv sqlparse==0.4.1 # via -r requirements.txt, django @@ -40,7 +41,7 @@ toml==0.10.2 # via -r requirements.txt, black, pre-commit typed-ast==1.4.1 # via -r requirements.txt, black typing-extensions==3.7.4.3 # via -r requirements.txt, black uwsgi==2.0.19.1 # via -r requirements.txt -virtualenv==20.1.0 # via pre-commit +virtualenv==20.1.0 # via -r requirements.txt, pre-commit zipp==3.4.0 # via -r requirements.txt, importlib-metadata zope.event==4.5.0 # via -r requirements.txt, gevent zope.interface==5.1.2 # via -r requirements.txt, gevent From 4cdb149a947f9b9aaf4f81ecc04e5fc6f6da6475 Mon Sep 17 00:00:00 2001 From: Michel Wilhelm Date: Sat, 7 Nov 2020 15:36:59 -0300 Subject: [PATCH 13/13] Enabling CORS for the frontend development --- app/settings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/settings.py b/app/settings.py index 88aa958..28b5642 100644 --- a/app/settings.py +++ b/app/settings.py @@ -23,6 +23,7 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "corsheaders", "core", "api", "game", @@ -31,6 +32,7 @@ INSTALLED_APPS = [ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", @@ -116,3 +118,6 @@ REST_FRAMEWORK = { ], "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), } + +if DEBUG is True: + CORS_ALLOW_ALL_ORIGINS = True