Skip to content
Snippets Groups Projects
Commit 03f1dd39 authored by Benoît Harrault's avatar Benoît Harrault
Browse files

Improve grids generator, update levels

parent 9281d9b7
No related branches found
No related tags found
1 merge request!58Resolve "Add/improve levels"
Pipeline #2514 passed
This commit is part of merge request !58. Comments created here will be created in the context of that merge request.
.editorconfig 0 → 100644
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
app.versionName=0.1.3
app.versionCode=52
app.versionName=0.1.4
app.versionCode=53
This diff is collapsed.
Improve grids generator, update levels
Amélioration du générateur de grilles, mise à jour des niveaux
#!/usr/bin/env bash
# This script should be run and redirected to a file like "batch.sh > grids.log"
# Then this grids file should be imported in "parse_grids.sh" to generate json
CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
ALLOWED_BLOCK_SIZE_VALUES="2x2 3x2 3x3 4x4"
ALLOWED_DIFFICULTY_VALUES="easy medium hard nightmare"
GRIDS_COUNT=10
GRIDS_COUNT=40
for BLOCK_SIZE in ${ALLOWED_BLOCK_SIZE_VALUES}; do
CELLS_COUNT=$(echo "(${BLOCK_SIZE})^2" | sed 's/x/\*/g' | bc)
for DIFFICULTY in ${ALLOWED_DIFFICULTY_VALUES}; do
echo "Block size: ${BLOCK_SIZE} / Difficulty: ${DIFFICULTY}"
for i in $(seq ${GRIDS_COUNT}); do
# Generate grid candidate
GRID="$(python ${CURRENT_DIR}/generate.py ${BLOCK_SIZE} ${DIFFICULTY} | tail -n 1)"
# Ensure grid can be resolve without any assumption
# Ensure grid can be resolved without any assumption
CAN_BE_SOLVED="$(python ${CURRENT_DIR}/solve.py ${BLOCK_SIZE} ${GRID} | tail -n 1)"
if [ "${CAN_BE_SOLVED}" == "Ok" ]; then
echo "${BLOCK_SIZE} ${DIFFICULTY} ${GRID}"
# Count "0" in grid, compute "emptiness" ratio
STRING="${GRID//[^0]}"
ZEROS_COUNT=$(echo "${#STRING}")
RATIO=$(echo "100*${ZEROS_COUNT}/${CELLS_COUNT}" | bc)
RATIO_STRING="$(printf "%02d" ${RATIO})"
echo "${BLOCK_SIZE} (${RATIO_STRING}%) ${DIFFICULTY} ${GRID}"
else
echo "FAILED: ${BLOCK_SIZE} ${DIFFICULTY} ${GRID}"
echo "FAILED / ${BLOCK_SIZE} ${DIFFICULTY}"
fi
done
done
......
......@@ -2,7 +2,6 @@
# -*- coding: iso-8859-15 -*-
import sys
import math
from random import randint, shuffle
if (len(sys.argv) != 3):
......@@ -13,7 +12,7 @@ if (len(sys.argv) != 3):
blocksize, difficulty = sys.argv[1], sys.argv[2]
if not blocksize in ['2x2', '3x2', '3x3', '4x4']:
if blocksize not in ['2x2', '3x2', '3x3', '4x4']:
print('wrong size given')
exit()
......@@ -23,7 +22,7 @@ size_vertical = int(splitted_blocksize[1])
boardSize = size_horizontal * size_vertical
if not difficulty in ['easy', 'medium', 'hard', 'nightmare']:
if difficulty not in ['easy', 'medium', 'hard', 'nightmare']:
print('wrong difficulty given')
exit()
......@@ -33,20 +32,35 @@ debugComputeGameGrid = True
############################################################################
difficultyLevel = 1;
#
# Difficulty grid:
# (number of "force/retry" during grid generation)
#
# | Size | H+V | Easy | Medium | Hard | Nightmare |
# | :--: | --: | ---: | ---------- | ----------- | ----------- |
# | 2x2 | 4 | 1 | 12 - 4 = 8 | 15 - 4 = 11 | 18 - 4 = 14 |
# | 2x3 | 5 | 1 | 12 - 5 = 7 | 15 - 5 = 10 | 18 - 5 = 13 |
# | 3x2 | 5 | 1 | 12 - 5 = 7 | 15 - 5 = 10 | 18 - 5 = 13 |
# | 3x3 | 6 | 1 | 12 - 6 = 6 | 15 - 6 = 9 | 18 - 6 = 12 |
# | 4x4 | 8 | 1 | 12 - 8 = 4 | 15 - 8 = 7 | 18 - 8 = 10 |
#
difficultyLevel = 1
if difficulty == 'easy':
difficultyLevel = 1
if difficulty == 'medium':
difficultyLevel = 11 - (size_horizontal + size_vertical)
if difficulty == 'hard':
difficultyLevel = 12 - (size_horizontal + size_vertical)
if difficulty == 'hard':
difficultyLevel = 15 - (size_horizontal + size_vertical)
if difficulty == 'nightmare':
difficultyLevel = 13 - (size_horizontal + size_vertical)
difficultyLevel = 18 - (size_horizontal + size_vertical)
sys.stdout.write('Will generate grid: ['+str(size_horizontal)+'x'+str(size_vertical)+'], difficulty: '+difficulty+' (level '+str(difficultyLevel)+')\n')
sys.stdout.write('Will generate grid: [' + str(size_horizontal) + 'x' + str(
size_vertical) + '], difficulty: ' + difficulty + ' (level ' + str(difficultyLevel) + ')\n')
stringValues = '0123456789ABCDEFG'
# draw grid (array style)
def drawGrid(grid):
gridVerticalSize = len(grid)
......@@ -67,6 +81,7 @@ def drawGrid(grid):
sys.stdout.write(('' * horizontalLineLength) + '\n')
sys.stdout.write('\n')
# draw grid (inline style)
def drawGridInline(grid):
for row in range(len(grid)):
......@@ -74,6 +89,7 @@ def drawGridInline(grid):
sys.stdout.write(stringValues[grid[row][col]])
sys.stdout.write('\n')
# initialise empty grid
def generateEmptyGrid(boardSize):
emptyGrid = []
......@@ -83,6 +99,7 @@ def generateEmptyGrid(boardSize):
emptyGrid[row].append(0)
return emptyGrid
# A check if the grid is full
def checkFullyCompletedGrid(grid):
for row in range(len(grid)):
......@@ -91,6 +108,7 @@ def checkFullyCompletedGrid(grid):
return False
return True
# (deep) copy of grid
def copyGrid(grid):
copiedGrid = []
......@@ -100,7 +118,9 @@ def copyGrid(grid):
copiedGrid[row].append(grid[row][col])
return copiedGrid
#A backtracking/recursive function to check all possible combinations of numbers until a solution is found
# A backtracking/recursive function to check all
# possible combinations of numbers until a solution is found
def solveGrid(grid, iterationSolveCount):
if debugSolveGrid:
sys.stdout.write('solveGrid / ' + str(iterationSolveCount) + '\n')
......@@ -118,7 +138,12 @@ def solveGrid(grid, iterationSolveCount):
shuffle(numberList)
for value in numberList:
if debugSolveGrid:
sys.stdout.write('solveGrid: ['+str(row)+','+str(col)+'] try with value '+str(value)+'\n')
sys.stdout.write(
'solveGrid: '
+ '[' + str(row) + ',' + str(col) + ']'
+ ' try with value ' + str(value)
+ '\n'
)
# Check that this value has not already be used on this row
if not(value in grid[row]):
# Check that this value has not already be used on this column
......@@ -129,29 +154,35 @@ def solveGrid(grid, iterationSolveCount):
if not foundInColumn:
# Get sub-square
blockColFrom = size_horizontal * int(col / size_horizontal)
blockColFrom = size_horizontal * \
int(col / size_horizontal)
blockRowFrom = size_vertical * int(row / size_vertical)
square = [grid[i][blockColFrom:blockColFrom + size_horizontal] for i in range(blockRowFrom, blockRowFrom + size_vertical)]
square = [grid[i][blockColFrom:blockColFrom + size_horizontal]
for i in range(blockRowFrom, blockRowFrom + size_vertical)]
# Check that this value has not already be used on this sub square
if not any(value in squareLine for squareLine in square):
grid[row][col] = value
if checkFullyCompletedGrid(grid):
if debugSolveGrid:
sys.stdout.write('solveGrid: grid complete, found solution\n')
sys.stdout.write(
'solveGrid: grid complete, found solution\n')
iterationSolveCount += 1
solutionsCount += 1
break
else:
if debugSolveGrid:
sys.stdout.write('solveGrid: recursive call (solutionsCount='+str(solutionsCount)+', iterationSolveCount='+str(iterationSolveCount)+')\n')
sys.stdout.write('solveGrid: recursive call (solutionsCount=' + str(
solutionsCount) + ', iterationSolveCount=' + str(iterationSolveCount) + ')\n')
if solveGrid(grid, iterationSolveCount + 1):
if debugSolveGrid:
sys.stdout.write('solveGrid: still searching for solution\n')
sys.stdout.write(
'solveGrid: still searching for solution\n')
return True
break
grid[row][col] = 0
# A backtracking/recursive function to check all possible combinations of numbers until a solution is found
def fillGrid(grid, boardSize, iterationFillCount):
if debugFillGrid:
......@@ -175,7 +206,8 @@ def fillGrid(grid, boardSize, iterationFillCount):
shuffle(numberList)
for value in numberList:
if debugFillGrid:
sys.stdout.write('fillGrid: ['+str(row)+','+str(col)+'] -> try with value '+str(value)+'\n')
sys.stdout.write(
'fillGrid: [' + str(row) + ',' + str(col) + '] -> try with value ' + str(value) + '\n')
# Check that this value has not already be used on this row
if not(value in grid[row]):
# Check that this value has not already be used on this column
......@@ -186,31 +218,40 @@ def fillGrid(grid, boardSize, iterationFillCount):
if not foundInColumn:
# Get sub-square
blockColFrom = size_horizontal * int(col / size_horizontal)
blockColFrom = size_horizontal * \
int(col / size_horizontal)
blockRowFrom = size_vertical * int(row / size_vertical)
square = [grid[i][blockColFrom:blockColFrom + size_horizontal] for i in range(blockRowFrom, blockRowFrom + size_vertical)]
square = [grid[i][blockColFrom:blockColFrom + size_horizontal]
for i in range(blockRowFrom, blockRowFrom + size_vertical)]
# Check that this value has not already be used on this sub square
if not any(value in squareLine for squareLine in square):
if debugFillGrid:
sys.stdout.write('fillGrid: ['+str(row)+','+str(col)+'] <- '+str(value)+' / ok, no conflict\n')
sys.stdout.write(
'fillGrid: [' + str(row) + ',' + str(col) + '] <- ' + str(value) + ' / ok, no conflict\n')
grid[row][col] = value
if checkFullyCompletedGrid(grid):
if debugFillGrid:
sys.stdout.write('fillGrid: found final solution\n')
sys.stdout.write(
'fillGrid: found final solution\n')
return True
else:
if debugFillGrid:
sys.stdout.write('fillGrid: recursive call (iterationFillCount='+str(iterationFillCount)+')\n')
sys.stdout.write(
'fillGrid: recursive call (iterationFillCount=' + str(iterationFillCount) + ')\n')
iterationFillCount += 1
if fillGrid(grid, boardSize, iterationFillCount):
return True
break
if debugFillGrid:
sys.stdout.write('fillGrid: no solution found ['+str(row)+','+str(col)+'] <- 0\n')
sys.stdout.write(
'fillGrid: no solution found [' + str(row) + ',' + str(col) + '] <- 0\n')
grid[row][col] = 0
solutionsCount = 1
def computeResolvableGrid(grid, maxAttemps):
global solutionsCount
......@@ -221,7 +262,8 @@ def computeResolvableGrid(grid, maxAttemps):
remainingAttemps = maxAttemps
while remainingAttemps > 0:
if debugComputeGameGrid:
sys.stdout.write('computeResolvableGrid / remainingAttemps: '+str(remainingAttemps)+'.\n')
sys.stdout.write(
'computeResolvableGrid / remainingAttemps: ' + str(remainingAttemps) + '.\n')
# Select a random cell that is not already empty
row = randint(0, boardSize - 1)
......@@ -236,26 +278,31 @@ def computeResolvableGrid(grid, maxAttemps):
solutionsCount = 0
if debugComputeGameGrid:
sys.stdout.write('computeResolvableGrid / Remove value in ['+str(row)+','+str(col)+'] (was '+str(savedCellValue)+').\n')
sys.stdout.write('computeResolvableGrid / Remove value in [' + str(
row) + ',' + str(col) + '] (was ' + str(savedCellValue) + ').\n')
drawGrid(grid)
sys.stdout.write('computeResolvableGrid / Check grid unique solution...\n')
sys.stdout.write(
'computeResolvableGrid / Check grid unique solution...\n')
solveGrid(copyGrid(grid), 0)
# Non unique solution => restore this cell value
if solutionsCount != 1:
if debugComputeGameGrid:
sys.stdout.write('computeResolvableGrid / Failed to solve grid (multiple solutions). Will try with clearing another cell.\n')
sys.stdout.write(
'computeResolvableGrid / Failed to solve grid (multiple solutions). Will try with clearing another cell.\n')
grid[row][col] = savedCellValue
remainingAttemps -= 1
else:
if debugComputeGameGrid:
sys.stdout.write('computeResolvableGrid / ok found unique solution.\n')
sys.stdout.write(
'computeResolvableGrid / ok found unique solution.\n')
if debugComputeGameGrid:
sys.stdout.write('computeResolvableGrid / ok found solvable grid.\n')
#########################
###########################################################################
grid = generateEmptyGrid(boardSize)
......@@ -271,5 +318,13 @@ computeResolvableGrid(grid, difficultyLevel)
sys.stdout.write('Generated grid:\n')
drawGrid(grid)
sys.stdout.write('Inline grid ['+str(size_horizontal)+'x'+str(size_vertical)+'], difficulty: '+difficulty+' (level '+str(difficultyLevel)+'):\n')
sys.stdout.write(
'Inline grid [' + str(size_horizontal) + 'x' + str(size_vertical) + ']'
+ ', '
+ 'difficulty: ' + difficulty
+ ' '
+ '(level ' + str(difficultyLevel) + ')'
+ ':'
+ '\n'
)
drawGridInline(grid)
#!/usr/bin/env bash
CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
# Generated grids (source)
GRIDS_FILE="$1"
# Game templates
GAME_TEMPLATES_FILE="${CURRENT_DIR}/../assets/files/templates.json"
# Parameters
ALLOWED_BLOCK_SIZE_VALUES="2x2 3x2 3x3 4x4"
MAX_GRIDS_COUNT_PER_LEVEL=20
# Fetch grid from input file
VALID_GRIDS="$(cat "${GRIDS_FILE}" | grep -v "FAILED" | grep "0" | sort | sed 's/ /_/g')"
OUTPUT="{\"templates\":{"
FIRST_BLOCK=1
function clean_grids() {
GRIDS_TO_CLEAN="$1"
echo "${GRIDS_TO_CLEAN}" | cut -d"_" -f4 | sed 's/^/"/' | sed 's/$/"/' | tr '\n' ',' | sed 's/,$//'
}
for BLOCK_SIZE in ${ALLOWED_BLOCK_SIZE_VALUES}; do
GRIDS=$(echo "${VALID_GRIDS}" | grep "${BLOCK_SIZE}");
GRIDS_COUNT=$(echo "${GRIDS}" | wc -l | awk '{print $1}')
echo "${BLOCK_SIZE}: found ${GRIDS_COUNT} grids"
# example with 100 grids, 10 per level:
# XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# | <- 1/3 -> | <- 1/3 -> | <- 1/3 -> |
# | | | |
# 1111111111------------------2222222222-----------------------3333333333-------------------4444444444
# | 10 | | 10 | | 10 | | 10 |
# easy
GRIDS_LEVEL_1="$(echo "${GRIDS}" | head -n ${MAX_GRIDS_COUNT_PER_LEVEL})"
# medium
GRIDS_LEVEL_2="$(echo "${GRIDS}" | head -n $(echo "${GRIDS_COUNT}/3 - ${MAX_GRIDS_COUNT_PER_LEVEL}/2" | bc) | tail -n ${MAX_GRIDS_COUNT_PER_LEVEL})"
# hard
GRIDS_LEVEL_3="$(echo "${GRIDS}" | tail -n $(echo "${GRIDS_COUNT}/3 + ${MAX_GRIDS_COUNT_PER_LEVEL}/2" | bc) | head -n ${MAX_GRIDS_COUNT_PER_LEVEL})"
# nightmare
GRIDS_LEVEL_4="$(echo "${GRIDS}" | tail -n ${MAX_GRIDS_COUNT_PER_LEVEL})"
if [ $FIRST_BLOCK -eq 1 ]; then
FIRST_BLOCK=0
else
OUTPUT="${OUTPUT},"
fi
OUTPUT="${OUTPUT} \"${BLOCK_SIZE}\": {"
OUTPUT="${OUTPUT} \"easy\": [$(clean_grids "${GRIDS_LEVEL_1}")],"
OUTPUT="${OUTPUT} \"medium\": [$(clean_grids "${GRIDS_LEVEL_2}")],"
OUTPUT="${OUTPUT} \"hard\": [$(clean_grids "${GRIDS_LEVEL_3}")],"
OUTPUT="${OUTPUT} \"nightmare\": [$(clean_grids "${GRIDS_LEVEL_4}")]"
OUTPUT="${OUTPUT} }"
done
OUTPUT="${OUTPUT} }}"
echo ${OUTPUT} | jq > "${GAME_TEMPLATES_FILE}"
echo "Ok, done. Grids saved in ${GAME_TEMPLATES_FILE}"
......@@ -12,7 +12,6 @@
#
import sys
import math
if (len(sys.argv) != 3):
print('Usage: solve.py block-size grid')
......@@ -21,7 +20,7 @@ if (len(sys.argv) != 3):
blocksize, gridTemplate = sys.argv[1], sys.argv[2]
if not blocksize in ['2x2', '3x2', '3x3', '4x4']:
if blocksize not in ['2x2', '3x2', '3x3', '4x4']:
print('wrong size given')
exit()
......@@ -39,10 +38,12 @@ debugSolveGrid = False
############################################################################
sys.stdout.write('Will solve grid: ['+str(size_horizontal)+'x'+str(size_vertical)+'] // '+gridTemplate+'\n')
sys.stdout.write('Will solve grid: [' + str(size_horizontal) +
'x' + str(size_vertical) + '] // ' + gridTemplate + '\n')
stringValues = '0123456789ABCDEFG'
# draw grid (array style)
def drawGrid(grid):
gridVerticalSize = len(grid)
......@@ -63,6 +64,7 @@ def drawGrid(grid):
sys.stdout.write(('' * horizontalLineLength) + '\n')
sys.stdout.write('\n')
# (deep) copy of grid
def copyGrid(grid):
copiedGrid = []
......@@ -72,6 +74,7 @@ def copyGrid(grid):
copiedGrid[row].append(grid[row][col])
return copiedGrid
# Init grid from given template
def initGrid(boardSize, gridTemplate):
grid = []
......@@ -83,6 +86,7 @@ def initGrid(boardSize, gridTemplate):
index += 1
return grid
# Check if grid is fully completed, without any empty cell
def isFullyCompleted(grid):
for row in range(len(grid)):
......@@ -91,11 +95,13 @@ def isFullyCompleted(grid):
return False
return True
# Check if a list contains duplicates (conflicts)
def containsDuplicates(list):
tmp_set = set(list)
return (len(list) != len(tmp_set))
# Check if given grid contains conflicts
def hasConflict(grid, size_horizontal, size_vertical):
# Check horizontal conflicts
......@@ -124,13 +130,11 @@ def hasConflict(grid, size_horizontal, size_vertical):
for blockRow in range(size_horizontal):
for blockCol in range(size_vertical):
# Get sub-square
blockColFrom = size_horizontal * int(col / size_horizontal)
blockRowFrom = size_vertical * int(row / size_vertical)
values = []
for rowInBlock in range(size_vertical):
for colInBlock in range(size_horizontal):
row = (blockRow * size_vertical) + rowInBlock;
col = (blockCol * size_horizontal) + colInBlock;
row = (blockRow * size_vertical) + rowInBlock
col = (blockCol * size_horizontal) + colInBlock
value = grid[row][col]
if value != 0:
values.append(value)
......@@ -140,6 +144,7 @@ def hasConflict(grid, size_horizontal, size_vertical):
return False
# Check if a value is allowed in a cell (without conflicting)
def isValueAllowed(grid, size_horizontal, size_vertical, row, col, candidateValue):
testGrid = copyGrid(grid)
......@@ -148,6 +153,7 @@ def isValueAllowed(grid, size_horizontal, size_vertical, row, col, candidateValu
return True
return False
# Get allowed values in a cell (witjout conflicting)
def findAllowedValuesForCell(grid, size_horizontal, size_vertical, row, col):
allowedValues = []
......@@ -162,7 +168,6 @@ def findAllowedValuesForCell(grid, size_horizontal, size_vertical, row, col):
def solve(grid, size_horizontal, size_vertical):
iterations = 0
maxIterations = 500
boardSize = size_horizontal * size_vertical
# Loop until grid is fully completed
while True:
......@@ -182,16 +187,19 @@ def solve(grid, size_horizontal, size_vertical):
for col in range(len(grid[row])):
if grid[row][col] == 0:
if debugSolveGrid:
print('Found empty cell ['+str(col)+','+str(row)+']')
print(
'Found empty cell [' + str(col) + ',' + str(row) + ']')
candidates.append([row, col])
if len(candidates):
for candidate in candidates:
candidateRow = candidate[0]
candidateCol = candidate[1]
allowedValues = findAllowedValuesForCell(grid, size_horizontal, size_vertical, candidateRow, candidateCol)
allowedValues = findAllowedValuesForCell(
grid, size_horizontal, size_vertical, candidateRow, candidateCol)
if debugSolveGrid:
print('Allowed values for cell ['+str(candidateCol)+','+str(candidateRow)+']: '+str(allowedValues))
print('Allowed values for cell [' + str(candidateCol) + ',' + str(
candidateRow) + ']: ' + str(allowedValues))
if len(allowedValues) != 1:
if debugSolveGrid:
print(' Non unique allowed value for cell. Skip to next cell')
......@@ -199,11 +207,13 @@ def solve(grid, size_horizontal, size_vertical):
value = allowedValues[0]
grid[candidateRow][candidateCol] = value
if debugSolveGrid:
print(' Found unique allowed value for cell ['+str(candidateCol)+','+str(candidateRow)+']: '+str(value))
print(' Found unique allowed value for cell [' + str(
candidateCol) + ',' + str(candidateRow) + ']: ' + str(value))
drawGrid(grid)
#########################
sys.stdout.write('Building start grid:\n')
grid = initGrid(boardSize, gridTemplate)
drawGrid(grid)
......@@ -213,7 +223,6 @@ if hasConflict(grid, size_horizontal, size_vertical):
sys.stdout.write(' - oups, initial conflict found.\n')
else:
sys.stdout.write(' - ok, no initial conflict found.\n')
sys.stdout.write('\n')
sys.stdout.write('Solving grid...\n')
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment