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

Merge branch '64-add-improve-levels' into 'master'

Resolve "Add/improve levels"

Closes #64

See merge request !58
parents 9281d9b7 03f1dd39
No related branches found
No related tags found
1 merge request!58Resolve "Add/improve levels"
Pipeline #2691 passed
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 register or to comment