feat: enhance custom game mode with fill rate slider and difficulty indicator

This commit is contained in:
2026-02-10 22:35:28 +01:00
parent 7d405ef0f6
commit c7834bd8bf
4 changed files with 86 additions and 44 deletions

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
import { usePuzzleStore } from '@/stores/puzzle';
import { useI18n } from '@/composables/useI18n';
@@ -8,6 +8,7 @@ const store = usePuzzleStore();
const { t } = useI18n();
const customSize = ref(10);
const fillRate = ref(50);
const errorMsg = ref('');
const snapToStep = (value, step) => {
@@ -19,6 +20,26 @@ const handleSnap = () => {
customSize.value = snapToStep(Number(customSize.value), 5);
};
const difficultyLevel = computed(() => {
const rate = fillRate.value;
const dist = Math.abs(rate - 50);
if (dist <= 5) return 'extreme';
if (dist <= 15) return 'hardest';
if (dist <= 25) return 'harder';
return 'easy';
});
const difficultyColor = computed(() => {
switch(difficultyLevel.value) {
case 'extreme': return '#ff3333';
case 'hardest': return '#ff9933';
case 'harder': return '#ffff33';
case 'easy': return '#33ff33';
default: return '#33ff33';
}
});
const confirm = () => {
const size = parseInt(customSize.value);
if (isNaN(size) || size < 5 || size > 80) {
@@ -26,7 +47,7 @@ const confirm = () => {
return;
}
store.initCustomGame(size);
store.initCustomGame(size, fillRate.value / 100);
emit('close');
};
</script>
@@ -53,6 +74,29 @@ const confirm = () => {
</div>
</div>
<p>{{ t('custom.fillRate') }}</p>
<div class="input-group">
<div class="range-value">{{ fillRate }}%</div>
<input
type="range"
v-model="fillRate"
min="10"
max="90"
step="5"
/>
<div class="range-scale">
<span>10%</span>
<span>90%</span>
</div>
</div>
<div class="difficulty-indicator">
<span class="label">{{ t('custom.difficulty') }}:</span>
<span class="value" :style="{ color: difficultyColor }">
{{ t(`difficulty.${difficultyLevel}`) }}
</span>
</div>
<p v-if="errorMsg" class="error">{{ errorMsg }}</p>
<div class="actions">
@@ -87,6 +131,7 @@ const confirm = () => {
border: 1px solid var(--accent-cyan);
box-shadow: 0 0 50px rgba(0, 242, 255, 0.2);
animation: slideUp 0.3s ease;
transition: all 0.3s ease-in-out;
}
h2 {
@@ -161,6 +206,26 @@ input[type="range"]::-moz-range-thumb {
font-size: 0.85rem;
}
.difficulty-indicator {
margin: 20px 0;
font-size: 1.2rem;
display: flex;
justify-content: center;
gap: 10px;
align-items: center;
}
.difficulty-indicator .label {
color: var(--text-color);
}
.difficulty-indicator .value {
font-weight: bold;
text-transform: uppercase;
text-shadow: 0 0 10px currentColor;
transition: color 0.3s ease;
}
.error {
color: #ff4d4d;
font-size: 0.9rem;

View File

@@ -29,6 +29,12 @@ const messages = {
'custom.cancel': 'Anuluj',
'custom.start': 'Start',
'custom.sizeError': 'Rozmiar musi być między 5 a 80!',
'custom.fillRate': 'Wypełnienie',
'custom.difficulty': 'Poziom trudności',
'difficulty.easy': 'Łatwy',
'difficulty.harder': 'Trudniejszy',
'difficulty.hardest': 'Najtrudniejszy',
'difficulty.extreme': 'Ekstremalny',
'win.title': 'GRATULACJE!',
'win.message': 'Rozwiązałeś zagadkę!',
'win.time': 'Czas:',
@@ -128,6 +134,12 @@ const messages = {
'custom.cancel': 'Cancel',
'custom.start': 'Start',
'custom.sizeError': 'Size must be between 5 and 80!',
'custom.fillRate': 'Fill Rate',
'custom.difficulty': 'Difficulty',
'difficulty.easy': 'Easy',
'difficulty.harder': 'Harder',
'difficulty.hardest': 'Hardest',
'difficulty.extreme': 'Extreme',
'win.title': 'CONGRATULATIONS!',
'win.message': 'You solved the puzzle!',
'win.time': 'Time:',

View File

@@ -120,15 +120,16 @@ export const usePuzzleStore = defineStore('puzzle', () => {
hasUsedGuide.value = false;
elapsedTime.value = 0;
startTimer();
saveState();
}
function initCustomGame(customSize) {
function initCustomGame(customSize, density = 0.5) {
stopTimer();
currentLevelId.value = 'custom';
size.value = customSize;
// Generate random grid
solution.value = generateRandomGrid(customSize);
solution.value = generateRandomGrid(customSize, density);
resetGrid();
isGameWon.value = false;
@@ -275,44 +276,9 @@ export const usePuzzleStore = defineStore('puzzle', () => {
return false;
}
function initGame(levelId = 'easy') {
// If init called without args and we have save, load it?
// User might want to start fresh if clicking buttons.
// Let's add explicit 'continue' logic or just auto-load on first run.
// For now, let's just stick to explicit init, but maybe load on mount if exists?
// The user didn't explicitly ask for "Continue", but "features from HTML".
// HTML usually auto-saves and loads.
// Duplicate initGame removed
stopTimer();
currentLevelId.value = levelId;
let puzzle = PUZZLES[levelId];
if (!puzzle) {
puzzle = PUZZLES['easy'];
}
size.value = puzzle.size;
solution.value = puzzle.grid;
resetGrid();
isGameWon.value = false;
elapsedTime.value = 0;
startTimer();
saveState();
}
// Modify initCustomGame similarly
function initCustomGame(customSize) {
stopTimer();
currentLevelId.value = 'custom';
size.value = customSize;
solution.value = generateRandomGrid(customSize);
resetGrid();
isGameWon.value = false;
elapsedTime.value = 0;
startTimer();
saveState();
}
// Duplicate initCustomGame removed
// Duplicate toggleCell/setCell removed

View File

@@ -40,13 +40,12 @@ export function calculateHints(grid) {
return { rowHints, colHints };
}
export function generateRandomGrid(size) {
export function generateRandomGrid(size, density = 0.5) {
const grid = [];
for (let i = 0; i < size; i++) {
const row = [];
for (let j = 0; j < size; j++) {
// ~25% empty cells
row.push(Math.random() > 0.25 ? 1 : 0);
row.push(Math.random() < density ? 1 : 0);
}
grid.push(row);
}