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:
2020-11-05 23:29:35 -03:00
committed by GitHub
parent 55ae104806
commit 733f3e5992
35 changed files with 690 additions and 67 deletions

0
game/__init__.py Normal file
View File

0
game/admin.py Normal file
View File

7
game/apps.py Normal file
View 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
View 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

View 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",
},
),
]

View 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"
),
),
]

View File

65
game/models.py Normal file
View 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
View File

86
game/tests/test_game.py Normal file
View 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)