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:
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)
|
||||
Reference in New Issue
Block a user