Basic game creating API (#2)
* Adding the base of our API * Little file and lint adjustments * Adding the lint command to Makefile * Adding the Minesweeper logic for game creation * Adding some tests for the Minesweeper algorithm * Adding some tools command to Makefile like pre-commit and pip-tools * Adding test help text to Makefile * all new user is_staff=True, for development for now * Now we can get the data from specific game Adding game status Adding game status Fixing game models
This commit is contained in:
19
.pre-commit-config.yaml
Normal file
19
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- repo: https://github.com/ambv/black
|
||||
rev: stable
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.8
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.7.9
|
||||
hooks:
|
||||
- id: flake8
|
||||
20
Makefile
20
Makefile
@@ -7,6 +7,11 @@ help:
|
||||
@echo "⭐️ help : Show this message"
|
||||
@echo "⭐️ clean : Removes all python cache and temporary files"
|
||||
@echo "⭐️ run : Runs the application using docker-compose"
|
||||
@echo "⭐️ lint : Lint the source using flake8 codestyle"
|
||||
@echo "⭐️ test : Runs the tests using Docker"
|
||||
@echo "⭐️ pre-commit-install : Install the pre-commit hook"
|
||||
@echo "⭐️ pre-commit-run : Runs the standalone pre-commit routine for checking files"
|
||||
@echo "⭐️ update-requirements : Using pip-compile(from pip-tools), update the requirements.txt with fixed version of used libraries"
|
||||
@echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――"
|
||||
|
||||
clean:
|
||||
@@ -18,3 +23,18 @@ clean:
|
||||
|
||||
run:
|
||||
@docker-compose up
|
||||
|
||||
lint:
|
||||
@flake8 .
|
||||
|
||||
test: clean
|
||||
@docker-compose run --rm app-test python manage.py test
|
||||
|
||||
pre-commit-install:
|
||||
@pre-commit install
|
||||
|
||||
pre-commit-run:
|
||||
@pre-commit run --all-files
|
||||
|
||||
update-requirements:
|
||||
@pip-compile requirements.txt
|
||||
|
||||
0
api/__init__.py
Normal file
0
api/__init__.py
Normal file
5
api/apps.py
Normal file
5
api/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
name = "api"
|
||||
0
api/resources/__init__.py
Normal file
0
api/resources/__init__.py
Normal file
30
api/resources/game.py
Normal file
30
api/resources/game.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from game.models import Game
|
||||
from ..serializers import GameSerializer
|
||||
|
||||
|
||||
class GameResource(APIView):
|
||||
def post(self, request):
|
||||
""" Creates a new game """
|
||||
|
||||
serializer = GameSerializer(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)
|
||||
|
||||
|
||||
class GameSingleResource(APIView):
|
||||
def get(self, request, game_id):
|
||||
""" Returns a game serialized or not found """
|
||||
|
||||
try:
|
||||
game = Game.objects.get(pk=game_id)
|
||||
except Game.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
serializer = GameSerializer(game)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
8
api/resources/main.py
Normal file
8
api/resources/main.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
||||
class MainResource(APIView):
|
||||
def get(self, request):
|
||||
return Response({"message": "Welcome to the game!"}, status=status.HTTP_200_OK)
|
||||
4
api/serializers/__init__.py
Normal file
4
api/serializers/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .game import GameSerializer
|
||||
|
||||
|
||||
__all__ = ["GameSerializer"]
|
||||
9
api/serializers/game.py
Normal file
9
api/serializers/game.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from game.models import Game
|
||||
|
||||
|
||||
class GameSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Game
|
||||
fields = "__all__"
|
||||
0
api/tests.py
Normal file
0
api/tests.py
Normal file
13
api/urls.py
Normal file
13
api/urls.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.urls import path
|
||||
|
||||
from .resources.main import MainResource
|
||||
from .resources.game import GameResource, GameSingleResource
|
||||
|
||||
|
||||
app_name = "api"
|
||||
|
||||
urlpatterns = [
|
||||
path("games/<game_id>", GameSingleResource.as_view(), name="games_single"),
|
||||
path("games", GameResource.as_view(), name="games"),
|
||||
path("", MainResource.as_view(), name="main"),
|
||||
]
|
||||
@@ -16,6 +16,7 @@ ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "127.0.0.1,localhost").split(",")
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
"rest_framework",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
@@ -23,6 +24,8 @@ INSTALLED_APPS = [
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"core",
|
||||
"api",
|
||||
"game",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -68,9 +71,7 @@ DATABASES = {
|
||||
"PASSWORD": os.getenv("DB_PASS"),
|
||||
"HOST": os.getenv("DB_HOST"),
|
||||
"PORT": os.getenv("DB_PORT"),
|
||||
"OPTIONS": {
|
||||
"charset": "utf8mb4",
|
||||
},
|
||||
"OPTIONS": {"charset": "utf8mb4"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,17 +81,11 @@ DATABASES = {
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
|
||||
},
|
||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||
]
|
||||
|
||||
|
||||
@@ -112,3 +107,12 @@ USE_TZ = True
|
||||
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
||||
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
# Some Django Rest Framework parameters
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.AllowAny"],
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||
"rest_framework.authentication.TokenAuthentication"
|
||||
],
|
||||
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
||||
}
|
||||
|
||||
27
app/urls.py
27
app/urls.py
@@ -1,21 +1,14 @@
|
||||
"""app URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.1/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("", include("api.urls")),
|
||||
]
|
||||
|
||||
# We need this only for development purpose
|
||||
if settings.DEBUG is True:
|
||||
urlpatterns += [
|
||||
path("admin/", admin.site.urls),
|
||||
]
|
||||
|
||||
@@ -2,4 +2,4 @@ from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
name = 'core'
|
||||
name = "core"
|
||||
|
||||
@@ -9,34 +9,90 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
name="User",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('email', models.EmailField(max_length=254, unique=True, verbose_name='E-mail')),
|
||||
('first_name', models.CharField(blank=True, max_length=30, verbose_name='First name')),
|
||||
('last_name', models.CharField(blank=True, max_length=30, verbose_name='Last name')),
|
||||
('date_joined', models.DateTimeField(auto_now_add=True, verbose_name='date joined')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='User active?')),
|
||||
('is_staff', models.BooleanField(default=False, verbose_name='Staff?')),
|
||||
('is_superuser', models.BooleanField(default=False, verbose_name='Superuser?')),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
max_length=254, unique=True, verbose_name="E-mail"
|
||||
),
|
||||
),
|
||||
(
|
||||
"first_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=30, verbose_name="First name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=30, verbose_name="Last name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"date_joined",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="date joined"),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(default=True, verbose_name="User active?"),
|
||||
),
|
||||
("is_staff", models.BooleanField(default=False, verbose_name="Staff?")),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(default=False, verbose_name="Superuser?"),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A"
|
||||
"user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'User',
|
||||
'verbose_name_plural': 'Users',
|
||||
'db_table': 'users',
|
||||
'ordering': ['first_name', 'last_name'],
|
||||
"verbose_name": "User",
|
||||
"verbose_name_plural": "Users",
|
||||
"db_table": "users",
|
||||
"ordering": ["first_name", "last_name"],
|
||||
},
|
||||
managers=[
|
||||
('objects', core.managers.UserManager()),
|
||||
],
|
||||
managers=[("objects", core.managers.UserManager())],
|
||||
),
|
||||
]
|
||||
|
||||
27
core/migrations/0002_auto_20201105_0303.py
Normal file
27
core/migrations/0002_auto_20201105_0303.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-05 03:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
("core", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="groups",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. "
|
||||
"A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0003_auto_20201106_0152.py
Normal file
18
core/migrations/0003_auto_20201106_0152.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-06 01:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0002_auto_20201105_0303"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="is_staff",
|
||||
field=models.BooleanField(default=True, verbose_name="Staff?"),
|
||||
),
|
||||
]
|
||||
@@ -14,7 +14,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
date_joined = models.DateTimeField("date joined", auto_now_add=True)
|
||||
|
||||
is_active = models.BooleanField("User active?", default=True)
|
||||
is_staff = models.BooleanField("Staff?", default=False)
|
||||
is_staff = models.BooleanField("Staff?", default=True)
|
||||
is_superuser = models.BooleanField("Superuser?", default=False)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -18,6 +18,28 @@ services:
|
||||
- 8000:8000
|
||||
links:
|
||||
- db
|
||||
app-test:
|
||||
build: .
|
||||
container_name: mines-app-test
|
||||
volumes:
|
||||
- ./commands:/commands
|
||||
- .:/app
|
||||
environment:
|
||||
- DB_USER=root
|
||||
- DB_PASS=minesweeper
|
||||
- DB_HOST=db
|
||||
- DB_NAME=minesweeper
|
||||
- DB_PORT=3306
|
||||
- DEBUG=1
|
||||
links:
|
||||
- db-test
|
||||
db-test:
|
||||
image: mysql:8
|
||||
container_name: mines-db-test
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=minesweeper
|
||||
- MYSQL_DATABASE=minesweeper
|
||||
db:
|
||||
image: mysql:8
|
||||
container_name: mines-db
|
||||
|
||||
0
game/__init__.py
Normal file
0
game/__init__.py
Normal file
0
game/admin.py
Normal file
0
game/admin.py
Normal file
7
game/apps.py
Normal file
7
game/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GameConfig(AppConfig):
|
||||
name = "game"
|
||||
verbose_name = "Game"
|
||||
verbose_name_plural = "Games"
|
||||
89
game/game.py
Normal file
89
game/game.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import random
|
||||
|
||||
|
||||
class Minesweeper:
|
||||
board = []
|
||||
|
||||
def __init__(self, rows=10, cols=10, mines=5):
|
||||
self.rows = rows
|
||||
self.cols = cols
|
||||
self.mines = mines
|
||||
|
||||
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)]
|
||||
|
||||
def put_mine(self):
|
||||
"""Put a single mine on the board.
|
||||
The mine have a -1 value just for reference
|
||||
"""
|
||||
mine_position_row = random.randrange(0, self.rows)
|
||||
mine_position_col = random.randrange(0, self.cols)
|
||||
|
||||
if self.is_mine(mine_position_row, mine_position_col):
|
||||
self.put_mine()
|
||||
|
||||
self.board[mine_position_row][mine_position_col] = -1
|
||||
return mine_position_row, mine_position_col
|
||||
|
||||
def put_mines(self):
|
||||
""" Put the desired amount of mines on the board """
|
||||
for mine in range(1, self.mines + 1):
|
||||
mine_position_row, mine_position_col = self.put_mine()
|
||||
|
||||
self.create_mine_points(mine_position_row, mine_position_col)
|
||||
|
||||
def create_mine_points(self, mine_position_row, mine_position_col):
|
||||
"""Populate the board with points that sorrounds the mine.
|
||||
The reference used is the mine that was already placed"""
|
||||
|
||||
# North
|
||||
self.increment_safe_point(mine_position_row - 1, mine_position_col)
|
||||
|
||||
# North-east
|
||||
self.increment_safe_point(mine_position_row - 1, mine_position_col + 1)
|
||||
|
||||
# East
|
||||
self.increment_safe_point(mine_position_row, mine_position_col + 1)
|
||||
|
||||
# South-east
|
||||
self.increment_safe_point(mine_position_row + 1, mine_position_col + 1)
|
||||
|
||||
# South
|
||||
self.increment_safe_point(mine_position_row + 1, mine_position_col)
|
||||
|
||||
# South-west
|
||||
self.increment_safe_point(mine_position_row + 1, mine_position_col - 1)
|
||||
|
||||
# West
|
||||
self.increment_safe_point(mine_position_row, mine_position_col - 1)
|
||||
|
||||
# North-west
|
||||
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 """
|
||||
try:
|
||||
return self.board[row][col] == -1
|
||||
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):
|
||||
return True
|
||||
return False
|
||||
|
||||
def increment_safe_point(self, row, col):
|
||||
""" Creates the mine's pontuation frame """
|
||||
|
||||
# Ignores if the point whether not in the board
|
||||
if not self.is_point_in_board(row, col):
|
||||
return
|
||||
|
||||
# Verify if the position have a mine on it
|
||||
if self.is_mine(row, col):
|
||||
return
|
||||
|
||||
# Increment the value of the position becaus is close to some mine
|
||||
self.board[row][col] += 1
|
||||
65
game/migrations/0001_initial.py
Normal file
65
game/migrations/0001_initial.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-05 03:03
|
||||
|
||||
from django.db import migrations, models
|
||||
import django_mysql.models
|
||||
import internal.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Game",
|
||||
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"
|
||||
),
|
||||
),
|
||||
(
|
||||
"modified_at",
|
||||
models.DateTimeField(auto_now=True, verbose_name="Last update"),
|
||||
),
|
||||
(
|
||||
"rows",
|
||||
models.PositiveIntegerField(default=10, verbose_name="Board rows"),
|
||||
),
|
||||
(
|
||||
"cols",
|
||||
models.PositiveIntegerField(default=10, verbose_name="Board cols"),
|
||||
),
|
||||
(
|
||||
"mines",
|
||||
models.PositiveIntegerField(
|
||||
default=5, verbose_name="Mines on board"
|
||||
),
|
||||
),
|
||||
(
|
||||
"board",
|
||||
django_mysql.models.JSONField(
|
||||
default=internal.utils.empty_list,
|
||||
verbose_name="Generated board",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Game",
|
||||
"verbose_name_plural": "Games",
|
||||
"db_table": "games",
|
||||
},
|
||||
),
|
||||
]
|
||||
68
game/migrations/0002_auto_20201106_0225.py
Normal file
68
game/migrations/0002_auto_20201106_0225.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-06 02:25
|
||||
|
||||
from django.db import migrations, models
|
||||
import django_mysql.models
|
||||
import game.models
|
||||
import internal.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("game", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="game",
|
||||
name="status",
|
||||
field=models.IntegerField(
|
||||
choices=[(0, "NOT_PLAYED"), (1, "PLAYING"), (2, "FINISHED")],
|
||||
default=game.models.GameStatuses["NOT_PLAYED"],
|
||||
help_text="Actual game status",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="game",
|
||||
name="win",
|
||||
field=models.BooleanField(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Did the user win the game?",
|
||||
null=True,
|
||||
verbose_name="Win?",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="game",
|
||||
name="board",
|
||||
field=django_mysql.models.JSONField(
|
||||
default=internal.utils.empty_list,
|
||||
help_text="Whe generated board game",
|
||||
verbose_name="Generated board",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="game",
|
||||
name="cols",
|
||||
field=models.PositiveIntegerField(
|
||||
default=10, help_text="Board's total columns", verbose_name="Board cols"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="game",
|
||||
name="mines",
|
||||
field=models.PositiveIntegerField(
|
||||
default=5,
|
||||
help_text="Board's total placed mines",
|
||||
verbose_name="Mines on board",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="game",
|
||||
name="rows",
|
||||
field=models.PositiveIntegerField(
|
||||
default=10, help_text="Board's total rows", verbose_name="Board rows"
|
||||
),
|
||||
),
|
||||
]
|
||||
0
game/migrations/__init__.py
Normal file
0
game/migrations/__init__.py
Normal file
65
game/models.py
Normal file
65
game/models.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from enum import IntEnum
|
||||
from django.db import models
|
||||
from django_mysql.models import JSONField
|
||||
|
||||
from internal.utils import empty_list
|
||||
from .game import Minesweeper
|
||||
|
||||
|
||||
class GameStatuses(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 Game(models.Model):
|
||||
created_at = models.DateTimeField("Creation date", auto_now_add=True)
|
||||
modified_at = models.DateTimeField("Last update", auto_now=True)
|
||||
|
||||
rows = models.PositiveIntegerField(
|
||||
"Board rows", default=10, help_text="Board's total rows"
|
||||
)
|
||||
cols = models.PositiveIntegerField(
|
||||
"Board cols", default=10, help_text="Board's total columns"
|
||||
)
|
||||
mines = models.PositiveIntegerField(
|
||||
"Mines on board", default=5, help_text="Board's total placed mines"
|
||||
)
|
||||
|
||||
board = JSONField(
|
||||
"Generated board", default=empty_list, help_text="Whe generated board game"
|
||||
)
|
||||
win = models.BooleanField(
|
||||
"Win?",
|
||||
default=None,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Did the user win the game?",
|
||||
)
|
||||
status = models.IntegerField(
|
||||
choices=GameStatuses.choices(),
|
||||
default=GameStatuses.NOT_PLAYED,
|
||||
help_text="Actual game status",
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" If the board was not defined, we create a new as default """
|
||||
|
||||
if not self.board:
|
||||
ms = Minesweeper(self.rows, self.cols, self.mines)
|
||||
ms.create_board()
|
||||
ms.put_mines()
|
||||
self.board = ms.board
|
||||
|
||||
super(Game, self).save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Game"
|
||||
verbose_name_plural = "Games"
|
||||
db_table = "games"
|
||||
0
game/tests/__init__.py
Normal file
0
game/tests/__init__.py
Normal file
86
game/tests/test_game.py
Normal file
86
game/tests/test_game.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from ..game import Minesweeper
|
||||
|
||||
|
||||
class MinesweeperTestCase(TestCase):
|
||||
def test_board_2x3x1(self):
|
||||
ms = Minesweeper(2, 3)
|
||||
ms.create_board()
|
||||
|
||||
expected_board = [[0, 0, 0], [0, 0, 0]]
|
||||
self.assertEqual(ms.board, expected_board)
|
||||
|
||||
def test_the_board_should_have_8_mines(self):
|
||||
expected_mines = 8
|
||||
ms = Minesweeper(10, 10, expected_mines)
|
||||
ms.create_board()
|
||||
ms.put_mines()
|
||||
|
||||
located_mines = 0
|
||||
|
||||
for row in ms.board:
|
||||
located_mines += row.count(-1)
|
||||
|
||||
self.assertEqual(located_mines, expected_mines)
|
||||
|
||||
def test_is_mine_on_0x1(self):
|
||||
ms = Minesweeper(2, 2, 1)
|
||||
ms.board = [[0, -1], [0, 0]]
|
||||
|
||||
self.assertEqual(ms.is_mine(0, 1), True)
|
||||
|
||||
def test_is_not_mine_on_1x1(self):
|
||||
ms = Minesweeper(2, 2, 1)
|
||||
ms.board = [[1, -1], [-1, 1]]
|
||||
|
||||
self.assertEqual(ms.is_mine(1, 1), False)
|
||||
|
||||
def test_is_point_is_not_on_the_board(self):
|
||||
ms = Minesweeper(2, 2, 1)
|
||||
ms.board = [[0, 0], [0, 0]]
|
||||
|
||||
self.assertEqual(ms.is_point_in_board(2, 1), False)
|
||||
|
||||
def test_is_point_is_on_the_board(self):
|
||||
ms = Minesweeper(2, 2, 1)
|
||||
ms.board = [[0, 0], [0, 0]]
|
||||
|
||||
self.assertEqual(ms.is_point_in_board(1, 1), True)
|
||||
|
||||
def test_pontuation_creation_around_one_mine(self):
|
||||
ms = Minesweeper(3, 3, 1)
|
||||
ms.board = [
|
||||
[0, 0, 0],
|
||||
[0, -1, 0],
|
||||
[0, 0, 0],
|
||||
]
|
||||
|
||||
expected_board = [
|
||||
[1, 1, 1],
|
||||
[1, -1, 1],
|
||||
[1, 1, 1],
|
||||
]
|
||||
|
||||
ms.create_mine_points(1, 1)
|
||||
|
||||
self.assertEqual(ms.board, expected_board)
|
||||
|
||||
def test_pontuation_creation_around_two_mines(self):
|
||||
ms = Minesweeper(3, 3, 1)
|
||||
ms.board = [
|
||||
[0, 0, 0],
|
||||
[0, -1, -1],
|
||||
[0, 0, 0],
|
||||
]
|
||||
|
||||
expected_board = [
|
||||
[1, 2, 2],
|
||||
[1, -1, -1],
|
||||
[1, 2, 2],
|
||||
]
|
||||
|
||||
ms.create_mine_points(1, 1)
|
||||
ms.create_mine_points(1, 2)
|
||||
|
||||
self.assertEqual(ms.board, expected_board)
|
||||
3
internal/__init__.py
Normal file
3
internal/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .utils import empty_list, empty_object
|
||||
|
||||
__all__ = ["empty_list", "empty_object"]
|
||||
6
internal/utils.py
Normal file
6
internal/utils.py
Normal file
@@ -0,0 +1,6 @@
|
||||
def empty_object():
|
||||
return {}
|
||||
|
||||
|
||||
def empty_list():
|
||||
return []
|
||||
@@ -4,33 +4,46 @@
|
||||
#
|
||||
# pip-compile requirements.txt
|
||||
#
|
||||
appdirs==1.4.4 # via -r requirements.txt, black
|
||||
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
|
||||
click==7.1.2 # via -r requirements.txt, black
|
||||
distlib==0.3.1 # via virtualenv
|
||||
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
|
||||
djangorestframework==3.12.1 # via -r requirements.txt
|
||||
filelock==3.0.12 # via 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
|
||||
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
|
||||
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
|
||||
pycodestyle==2.6.0 # via -r requirements.txt, flake8
|
||||
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
|
||||
regex==2020.10.28 # via -r requirements.txt, black
|
||||
six==1.15.0 # via -r requirements.txt, python-dateutil
|
||||
six==1.15.0 # via -r requirements.txt, python-dateutil, virtualenv
|
||||
sqlparse==0.4.1 # via -r requirements.txt, django
|
||||
toml==0.10.2 # via -r requirements.txt, black
|
||||
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
|
||||
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
|
||||
flake8
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
||||
|
||||
Reference in New Issue
Block a user