Compare commits
4 Commits
000ef8f715
...
v1.15.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
b25d3f4015
|
|||
|
43629d72a4
|
|||
|
4138c99e20
|
|||
| 588a131d68 |
5
android/.gitignore
vendored
5
android/.gitignore
vendored
@@ -54,8 +54,9 @@ captures/
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
#*.keystore
|
||||
*.jks
|
||||
*.keystore
|
||||
keystore.properties
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
|
||||
@@ -3,12 +3,27 @@ apply plugin: 'com.android.application'
|
||||
android {
|
||||
namespace = "pl.nonograms.app"
|
||||
compileSdk = rootProject.ext.compileSdkVersion
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
storeFile = file(keystoreProperties['storeFile'])
|
||||
storePassword = keystoreProperties['storePassword']
|
||||
keyAlias = keystoreProperties['keyAlias']
|
||||
keyPassword = keystoreProperties['keyPassword']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "pl.nonograms.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
versionCode 1144
|
||||
versionName "1.14.4"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
@@ -18,8 +33,9 @@ android {
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
</manifest>
|
||||
|
||||
@@ -7,7 +7,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.13.0'
|
||||
classpath 'com.android.tools.build:gradle:9.0.1'
|
||||
classpath 'com.google.gms:google-services:4.4.4'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
||||
@@ -20,3 +20,13 @@ org.gradle.jvmargs=-Xmx1536m
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
android.defaults.buildfeatures.resvalues=true
|
||||
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
|
||||
android.enableAppCompileTimeRClass=false
|
||||
android.usesSdkInManifest.disallowed=false
|
||||
android.uniquePackageNames=false
|
||||
android.dependency.useConstraints=true
|
||||
android.r8.strictFullModeForKeepRules=false
|
||||
android.r8.optimizedResourceShrinking=false
|
||||
android.builtInKotlin=false
|
||||
android.newDsl=false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -82,7 +82,7 @@ define(['./workbox-7a5e81cd'], (function (workbox) { 'use strict';
|
||||
*/
|
||||
workbox.precacheAndRoute([{
|
||||
"url": "index.html",
|
||||
"revision": "0.kkc80cp3p5o"
|
||||
"revision": "0.n1n8rjsg38"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
container_name: nonograms-app
|
||||
nonograms:
|
||||
container_name: nonograms
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8081:80"
|
||||
expose:
|
||||
- "80"
|
||||
restart: unless-stopped
|
||||
# Uncomment the following lines if you want to mount the configuration locally for development/testing
|
||||
# volumes:
|
||||
# - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
networks:
|
||||
- npm_public
|
||||
|
||||
networks:
|
||||
npm_public:
|
||||
external: true
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app requires camera access to import nonograms from photos.</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "vue-nonograms-solid",
|
||||
"version": "1.14.4",
|
||||
"version": "1.15.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vue-nonograms-solid",
|
||||
"version": "1.14.4",
|
||||
"version": "1.15.0",
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^8.1.0",
|
||||
"@capacitor/cli": "^8.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vue-nonograms-solid",
|
||||
"version": "1.14.4",
|
||||
"version": "1.15.0",
|
||||
"homepage": "https://nonograms.7u.pl/",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -247,10 +247,10 @@ watch(() => store.size, async () => {
|
||||
<div class="corner-spacer"></div>
|
||||
|
||||
<!-- Column Hints -->
|
||||
<Hints :hints="colHints" orientation="col" :size="gridCols" :activeIndex="activeCol" />
|
||||
<Hints :hints="colHints" orientation="col" :size="gridCols" :activeIndex="activeCol" :completedLines="store.completedCols" />
|
||||
|
||||
<!-- Row Hints -->
|
||||
<Hints ref="rowHintsRef" :hints="rowHints" orientation="row" :size="gridRows" :activeIndex="activeRow" />
|
||||
<Hints ref="rowHintsRef" :hints="rowHints" orientation="row" :size="gridRows" :activeIndex="activeRow" :completedLines="store.completedRows" />
|
||||
|
||||
<!-- Grid -->
|
||||
<div
|
||||
|
||||
@@ -16,6 +16,10 @@ defineProps({
|
||||
activeIndex: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
completedLines: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -34,6 +38,7 @@ defineProps({
|
||||
class="hint-group"
|
||||
:class="{
|
||||
'is-active': index === activeIndex,
|
||||
'is-completed': completedLines[index],
|
||||
'guide-right': orientation === 'col' && (index + 1) % 5 === 0 && index !== size - 1,
|
||||
'guide-bottom': orientation === 'row' && (index + 1) % 5 === 0 && index !== size - 1
|
||||
}"
|
||||
@@ -81,6 +86,15 @@ defineProps({
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hint-group.is-completed {
|
||||
opacity: 0.5;
|
||||
background-color: var(--hint-bg);
|
||||
}
|
||||
|
||||
.hint-group.is-completed .hint-num {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.col .hint-group {
|
||||
flex-direction: column;
|
||||
padding: 4px 2px;
|
||||
|
||||
@@ -56,6 +56,31 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
||||
return Math.min(100, (filledCorrectly.value / totalCellsToFill.value) * 100);
|
||||
});
|
||||
|
||||
const completedRows = computed(() => {
|
||||
if (!solution.value.length || !playerGrid.value.length) return [];
|
||||
const rows = solution.value.length;
|
||||
return Array(rows).fill().map((_, r) => {
|
||||
const targetHints = calculateLineHints(solution.value[r]);
|
||||
const playerLine = playerGrid.value[r];
|
||||
return validateLine(playerLine, targetHints);
|
||||
});
|
||||
});
|
||||
|
||||
const completedCols = computed(() => {
|
||||
if (!solution.value.length || !playerGrid.value.length) return [];
|
||||
const rows = solution.value.length;
|
||||
const cols = solution.value[0].length;
|
||||
return Array(cols).fill().map((_, c) => {
|
||||
const col = [];
|
||||
for (let r = 0; r < rows; r++) {
|
||||
col.push(solution.value[r][c]);
|
||||
}
|
||||
const targetHints = calculateLineHints(col);
|
||||
const playerLine = playerGrid.value.map(row => row[c]);
|
||||
return validateLine(playerLine, targetHints);
|
||||
});
|
||||
});
|
||||
|
||||
// Actions
|
||||
function initGame(levelId = 'easy') {
|
||||
stopTimer();
|
||||
@@ -408,7 +433,9 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
||||
currentDensity,
|
||||
markGuideUsed,
|
||||
startInteraction,
|
||||
endInteraction
|
||||
endInteraction,
|
||||
completedRows,
|
||||
completedCols
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
70
src/stores/puzzle_completion.test.js
Normal file
70
src/stores/puzzle_completion.test.js
Normal file
@@ -0,0 +1,70 @@
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { setActivePinia, createPinia } from 'pinia';
|
||||
import { usePuzzleStore } from './puzzle';
|
||||
|
||||
describe('Puzzle Store - Completion Logic', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it('should correctly identify completed rows and columns', () => {
|
||||
const store = usePuzzleStore();
|
||||
|
||||
// Setup a simple 2x2 puzzle
|
||||
// Solution:
|
||||
// 1 0
|
||||
// 0 1
|
||||
// Row Hints: [1], [1]
|
||||
// Col Hints: [1], [1]
|
||||
store.solution = [
|
||||
[1, 0],
|
||||
[0, 1]
|
||||
];
|
||||
store.playerGrid = [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
];
|
||||
|
||||
// Initially nothing is completed
|
||||
expect(store.completedRows).toEqual([false, false]);
|
||||
expect(store.completedCols).toEqual([false, false]);
|
||||
|
||||
// Fill first row correctly: 1 0
|
||||
store.playerGrid[0][0] = 1;
|
||||
store.playerGrid[0][1] = 0;
|
||||
|
||||
expect(store.completedRows).toEqual([true, false]);
|
||||
|
||||
// Fill second row incorrectly (too many filled): 1 1
|
||||
store.playerGrid[1][0] = 1;
|
||||
store.playerGrid[1][1] = 1;
|
||||
|
||||
// Row 2 hint is [1], user has [2]. Should be false.
|
||||
expect(store.completedRows).toEqual([true, false]);
|
||||
|
||||
// Fix second row: 0 1
|
||||
store.playerGrid[1][0] = 0;
|
||||
store.playerGrid[1][1] = 1;
|
||||
|
||||
expect(store.completedRows).toEqual([true, true]);
|
||||
|
||||
// Check columns
|
||||
// Col 1: 1, 0 -> Hint [1]. Matches.
|
||||
// Col 2: 0, 1 -> Hint [1]. Matches.
|
||||
expect(store.completedCols).toEqual([true, true]);
|
||||
});
|
||||
|
||||
it('should mark row as completed if constraints are met even if wrong position', () => {
|
||||
const store = usePuzzleStore();
|
||||
// Solution: 1 0 0 (Hint 1)
|
||||
store.solution = [[1, 0, 0]];
|
||||
store.playerGrid = [[0, 0, 0]];
|
||||
|
||||
// User puts 0 0 1 (Hint 1)
|
||||
store.playerGrid[0] = [0, 0, 1];
|
||||
|
||||
// Should be marked as completed because it satisfies the hint "1"
|
||||
expect(store.completedRows).toEqual([true]);
|
||||
});
|
||||
});
|
||||
@@ -39,6 +39,6 @@ describe('Large Grid Solver', () => {
|
||||
console.log('Result:', result);
|
||||
|
||||
expect(result.percentSolved).toBeGreaterThan(0);
|
||||
expect(result.difficulty).toBeDefined();
|
||||
expect(result.difficultyScore).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user