Загрузка данных
```html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>🌈 Путешествие в мир фигур</title>
<style>
/* ============================================
БАЗОВЫЕ СТИЛИ И ПЕРЕМЕННЫЕ
============================================ */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
:root {
--bg-main: #FFF5F7;
--bg-card: #FFFFFF;
--pink: #FFB5C5;
--pink-dark: #FF8FA8;
--blue: #B5D8FF;
--blue-dark: #8FC0FF;
--mint: #B5EBCD;
--mint-dark: #8FD9B0;
--lavender: #E8D5FF;
--lavender-dark: #C9A8FF;
--peach: #FFD5B5;
--peach-dark: #FFB88A;
--yellow: #FFF3B0;
--text-dark: #4A4A6A;
--text-light: #7A7A9A;
--success: #A8E6CF;
--success-dark: #6BC99A;
--error: #FFB3BA;
--error-dark: #FF8A95;
--shadow: 0 8px 24px rgba(74, 74, 106, 0.12);
--shadow-hover: 0 12px 32px rgba(74, 74, 106, 0.2);
}
html, body {
height: 100%;
overflow: hidden;
}
body {
font-family: 'Comic Sans MS', 'Chalkboard SE', 'Marker Felt', system-ui, sans-serif;
background: linear-gradient(135deg, #FFF5F7 0%, #F0F7FF 50%, #F5FFF5 100%);
color: var(--text-dark);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
position: relative;
overflow: hidden;
}
/* Декоративные плавающие фигуры на фоне */
.bg-shape {
position: fixed;
opacity: 0.15;
pointer-events: none;
z-index: 0;
animation: float 20s infinite ease-in-out;
}
.bg-shape:nth-child(1) { top: 10%; left: 5%; width: 80px; height: 80px; background: var(--pink); border-radius: 50%; animation-delay: 0s; }
.bg-shape:nth-child(2) { top: 70%; left: 85%; width: 100px; height: 100px; background: var(--blue); animation-delay: -5s; }
.bg-shape:nth-child(3) { top: 80%; left: 10%; width: 70px; height: 70px; background: var(--mint); clip-path: polygon(50% 0%, 0% 100%, 100% 100%); animation-delay: -10s; }
.bg-shape:nth-child(4) { top: 20%; left: 90%; width: 90px; height: 60px; background: var(--lavender); border-radius: 50%; animation-delay: -15s; }
.bg-shape:nth-child(5) { top: 45%; left: 3%; width: 60px; height: 80px; background: var(--peach); animation-delay: -7s; }
@keyframes float {
0%, 100% { transform: translate(0, 0) rotate(0deg); }
25% { transform: translate(20px, -30px) rotate(10deg); }
50% { transform: translate(-15px, 20px) rotate(-10deg); }
75% { transform: translate(25px, 15px) rotate(5deg); }
}
/* ============================================
КОНТЕЙНЕР ПРИЛОЖЕНИЯ
============================================ */
#app {
width: 100%;
max-width: 900px;
height: 100vh;
max-height: 700px;
position: relative;
z-index: 1;
}
.screen {
position: absolute;
inset: 0;
display: none;
flex-direction: column;
padding: 20px;
animation: fadeIn 0.5s ease-out;
}
.screen.active {
display: flex;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* ============================================
СТАРТОВЫЙ ЭКРАН
============================================ */
#start-screen {
justify-content: center;
align-items: center;
text-align: center;
gap: 20px;
}
.mascot {
font-size: clamp(80px, 15vw, 140px);
animation: bounce 2s infinite ease-in-out;
filter: drop-shadow(0 8px 16px rgba(0,0,0,0.1));
}
@keyframes bounce {
0%, 100% { transform: translateY(0) rotate(-5deg); }
50% { transform: translateY(-15px) rotate(5deg); }
}
.title {
font-size: clamp(28px, 5vw, 44px);
color: var(--text-dark);
font-weight: bold;
text-shadow: 2px 2px 0 var(--pink);
line-height: 1.2;
}
.subtitle {
font-size: clamp(16px, 2.5vw, 20px);
color: var(--text-light);
max-width: 500px;
line-height: 1.5;
}
.btn {
padding: 16px 40px;
font-size: clamp(18px, 3vw, 24px);
font-family: inherit;
font-weight: bold;
border: none;
border-radius: 50px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: var(--shadow);
color: white;
background: linear-gradient(135deg, var(--pink-dark), var(--pink));
position: relative;
overflow: hidden;
}
.btn:hover {
transform: translateY(-3px) scale(1.03);
box-shadow: var(--shadow-hover);
}
.btn:active {
transform: translateY(0) scale(0.98);
}
.btn-secondary {
background: linear-gradient(135deg, var(--blue-dark), var(--blue));
}
.btn-success {
background: linear-gradient(135deg, var(--success-dark), var(--success));
}
.best-score {
background: var(--bg-card);
padding: 12px 24px;
border-radius: 30px;
box-shadow: var(--shadow);
font-size: 16px;
color: var(--text-light);
}
.best-score span {
color: var(--pink-dark);
font-weight: bold;
font-size: 20px;
}
/* ============================================
ИГРОВОЙ ЭКРАН
============================================ */
#game-screen {
gap: 15px;
}
.game-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.stat-box {
background: var(--bg-card);
padding: 10px 18px;
border-radius: 20px;
box-shadow: var(--shadow);
display: flex;
align-items: center;
gap: 8px;
font-size: clamp(14px, 2vw, 18px);
font-weight: bold;
flex: 1;
min-width: 100px;
justify-content: center;
}
.stat-box .icon {
font-size: 1.3em;
}
.stat-box.timer { color: var(--pink-dark); }
.stat-box.progress { color: var(--blue-dark); }
.stat-box.score { color: var(--mint-dark); }
.timer.warning {
color: var(--error-dark);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.progress-bar {
width: 100%;
height: 12px;
background: var(--bg-card);
border-radius: 10px;
overflow: hidden;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.05);
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--mint), var(--blue), var(--lavender));
border-radius: 10px;
transition: width 0.5s ease;
width: 0%;
}
.question-area {
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
background: var(--bg-card);
border-radius: 30px;
padding: 20px;
box-shadow: var(--shadow);
overflow: hidden;
position: relative;
}
.helper {
display: flex;
align-items: flex-start;
gap: 12px;
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
.helper-avatar {
font-size: clamp(40px, 7vw, 60px);
flex-shrink: 0;
animation: wiggle 3s infinite ease-in-out;
}
@keyframes wiggle {
0%, 100% { transform: rotate(-3deg); }
50% { transform: rotate(3deg); }
}
.helper-bubble {
background: linear-gradient(135deg, var(--yellow), #FFF9D6);
padding: 14px 20px;
border-radius: 20px;
border-top-left-radius: 5px;
font-size: clamp(15px, 2.3vw, 19px);
line-height: 1.4;
color: var(--text-dark);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
position: relative;
flex: 1;
}
.canvas-container {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
flex-wrap: wrap;
padding: 10px;
min-height: 150px;
}
.shape-canvas {
border-radius: 20px;
background: linear-gradient(135deg, #FAFAFF, #F5F0FF);
box-shadow: inset 0 2px 8px rgba(0,0,0,0.05);
max-width: 100%;
touch-action: manipulation;
}
.shape-canvas.clickable {
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.shape-canvas.clickable:hover {
transform: scale(1.05);
box-shadow: 0 8px 20px rgba(143, 192, 255, 0.4);
}
.shape-canvas.selected {
box-shadow: 0 0 0 4px var(--success-dark), 0 8px 20px rgba(107, 201, 154, 0.4);
transform: scale(1.05);
}
.options-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin-top: 10px;
}
.option-btn {
padding: 14px 18px;
font-size: clamp(15px, 2.3vw, 19px);
font-family: inherit;
font-weight: bold;
border: 3px solid transparent;
border-radius: 20px;
cursor: pointer;
transition: all 0.2s ease;
background: var(--bg-card);
color: var(--text-dark);
box-shadow: var(--shadow);
min-height: 55px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.option-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
border-color: var(--blue);
}
.option-btn:active:not(:disabled) {
transform: translateY(0);
}
.option-btn.correct {
background: var(--success);
border-color: var(--success-dark);
color: var(--text-dark);
animation: correctPop 0.5s ease;
}
.option-btn.wrong {
background: var(--error);
border-color: var(--error-dark);
animation: shake 0.5s ease;
}
.option-btn:disabled {
cursor: not-allowed;
opacity: 0.7;
}
@keyframes correctPop {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-8px); }
75% { transform: translateX(8px); }
}
.feedback {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: clamp(40px, 8vw, 80px);
pointer-events: none;
opacity: 0;
z-index: 10;
}
.feedback.show {
animation: feedbackPop 1.2s ease-out forwards;
}
@keyframes feedbackPop {
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.3) rotate(-20deg); }
30% { opacity: 1; transform: translate(-50%, -50%) scale(1.3) rotate(10deg); }
60% { opacity: 1; transform: translate(-50%, -50%) scale(1) rotate(0deg); }
100% { opacity: 0; transform: translate(-50%, -80%) scale(0.8); }
}
/* ============================================
ЭКРАН РЕЗУЛЬТАТОВ
============================================ */
#result-screen {
justify-content: center;
align-items: center;
text-align: center;
gap: 20px;
}
.result-mascot {
font-size: clamp(80px, 15vw, 140px);
animation: celebrate 1s ease-out;
}
@keyframes celebrate {
0% { transform: scale(0) rotate(-180deg); }
50% { transform: scale(1.2) rotate(180deg); }
100% { transform: scale(1) rotate(360deg); }
}
.result-title {
font-size: clamp(28px, 5vw, 44px);
color: var(--text-dark);
font-weight: bold;
}
.result-stats {
display: flex;
gap: 15px;
flex-wrap: wrap;
justify-content: center;
}
.result-stat {
background: var(--bg-card);
padding: 18px 24px;
border-radius: 25px;
box-shadow: var(--shadow);
min-width: 130px;
}
.result-stat .label {
font-size: 14px;
color: var(--text-light);
margin-bottom: 5px;
}
.result-stat .value {
font-size: clamp(22px, 4vw, 32px);
font-weight: bold;
color: var(--pink-dark);
}
.stars {
display: flex;
gap: 8px;
font-size: clamp(30px, 6vw, 50px);
justify-content: center;
}
.star {
opacity: 0.3;
transition: all 0.5s ease;
}
.star.earned {
opacity: 1;
animation: starPop 0.6s ease backwards;
}
@keyframes starPop {
0% { transform: scale(0) rotate(-180deg); }
60% { transform: scale(1.3) rotate(20deg); }
100% { transform: scale(1) rotate(0); }
}
.result-message {
font-size: clamp(16px, 2.5vw, 20px);
color: var(--text-light);
max-width: 500px;
line-height: 1.5;
padding: 0 10px;
}
.result-buttons {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: center;
}
/* Конфетти */
.confetti {
position: fixed;
width: 10px;
height: 10px;
pointer-events: none;
z-index: 100;
animation: confettiFall 3s linear forwards;
}
@keyframes confettiFall {
0% { transform: translateY(-100vh) rotate(0deg); opacity: 1; }
100% { transform: translateY(100vh) rotate(720deg); opacity: 0; }
}
/* ============================================
АДАПТИВНОСТЬ
============================================ */
@media (max-width: 600px) {
.game-header {
gap: 6px;
}
.stat-box {
padding: 8px 12px;
min-width: 80px;
font-size: 13px;
}
.question-area {
padding: 14px;
border-radius: 22px;
}
.options-grid {
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.option-btn {
padding: 12px 10px;
min-height: 48px;
font-size: 14px;
}
.canvas-container {
gap: 8px;
min-height: 120px;
}
}
@media (max-height: 600px) {
.mascot, .result-mascot { font-size: 70px; }
.title, .result-title { font-size: 24px; }
.subtitle, .result-message { font-size: 14px; }
}
/* Скрытый скроллбар */
::-webkit-scrollbar { width: 0; }
</style>
</head>
<body>
<!-- Декоративные фигуры на фоне -->
<div class="bg-shape"></div>
<div class="bg-shape"></div>
<div class="bg-shape"></div>
<div class="bg-shape"></div>
<div class="bg-shape"></div>
<div id="app">
<!-- ========== СТАРТОВЫЙ ЭКРАН ========== -->
<div id="start-screen" class="screen active">
<div class="mascot">🦉</div>
<h1 class="title">Путешествие в мир фигур</h1>
<p class="subtitle">
Привет, юный исследователь! Меня зовут Мудрая Сова.
Я приглашаю тебя в увлекательное путешествие, где мы будем изучать
геометрические фигуры! 🎨✨
</p>
<div class="best-score" id="best-score-box" style="display:none;">
🏆 Твой лучший результат: <span id="best-score-value">0</span>
</div>
<button class="btn" id="start-btn">🚀 Начать игру</button>
</div>
<!-- ========== ИГРОВОЙ ЭКРАН ========== -->
<div id="game-screen" class="screen">
<div class="game-header">
<div class="stat-box timer" id="timer-box">
<span class="icon">⏰</span>
<span id="timer">15:00</span>
</div>
<div class="stat-box progress">
<span class="icon">📍</span>
<span id="progress">1/10</span>
</div>
<div class="stat-box score">
<span class="icon">⭐</span>
<span id="score">0</span>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div class="question-area" id="question-area">
<div class="helper">
<div class="helper-avatar">🦉</div>
<div class="helper-bubble" id="helper-text">Задание появится здесь...</div>
</div>
<div class="canvas-container" id="canvas-container"></div>
<div class="options-grid" id="options-grid"></div>
<div class="feedback" id="feedback"></div>
</div>
</div>
<!-- ========== ЭКРАН РЕЗУЛЬТАТОВ ========== -->
<div id="result-screen" class="screen">
<div class="result-mascot" id="result-mascot">🎉</div>
<h1 class="result-title" id="result-title">Молодец!</h1>
<div class="stars" id="stars">
<span class="star">⭐</span>
<span class="star">⭐</span>
<span class="star">⭐</span>
<span class="star">⭐</span>
<span class="star">⭐</span>
</div>
<div class="result-stats">
<div class="result-stat">
<div class="label">Очки</div>
<div class="value" id="final-score">0</div>
</div>
<div class="result-stat">
<div class="label">Правильно</div>
<div class="value" id="final-correct">0/10</div>
</div>
<div class="result-stat">
<div class="label">Время</div>
<div class="value" id="final-time">00:00</div>
</div>
</div>
<p class="result-message" id="result-message">Отличная работа!</p>
<div class="result-buttons">
<button class="btn" id="restart-btn">🔄 Играть снова</button>
<button class="btn btn-secondary" id="home-btn">🏠 В начало</button>
</div>
</div>
</div>
<script>
/* ============================================
КОНФИГУРАЦИЯ И КОНСТАНТЫ
============================================ */
const CONFIG = {
TOTAL_QUESTIONS: 10,
GAME_DURATION: 15 * 60, // 15 минут в секундах
POINTS_PER_CORRECT: 10,
BONUS_TIME: 5, // бонус за быстрый ответ
FAST_ANSWER_THRESHOLD: 5, // секунд для бонуса
STORAGE_KEY: 'shapes_game_best_score'
};
// Палитра цветов для фигур
const COLORS = {
red: '#FF8A95',
blue: '#8FC0FF',
green: '#8FD9B0',
yellow: '#FFE08A',
purple: '#C9A8FF',
pink: '#FFB5C5',
orange: '#FFB88A'
};
const COLOR_NAMES = {
red: 'красный',
blue: 'синий',
green: 'зелёный',
yellow: 'жёлтый',
purple: 'фиолетовый',
pink: 'розовый',
orange: 'оранжевый'
};
// Типы фигур
const SHAPES = ['circle', 'square', 'triangle', 'rectangle', 'oval'];
const SHAPE_NAMES = {
circle: 'круг',
square: 'квадрат',
triangle: 'треугольник',
rectangle: 'прямоугольник',
oval: 'овал'
};
const SHAPE_PROPERTIES = {
circle: { corners: 0, sides: 0, hasCorners: false },
square: { corners: 4, sides: 4, hasCorners: true },
triangle: { corners: 3, sides: 3, hasCorners: true },
rectangle: { corners: 4, sides: 4, hasCorners: true },
oval: { corners: 0, sides: 0, hasCorners: false }
};
/* ============================================
СОСТОЯНИЕ ИГРЫ
============================================ */
const GameState = {
currentQuestion: 0,
score: 0,
correctAnswers: 0,
timeLeft: CONFIG.GAME_DURATION,
timerInterval: null,
questionStartTime: 0,
questions: [],
isAnswering: false,
startTime: 0
};
/* ============================================
УТИЛИТЫ
============================================ */
const Utils = {
// Случайный элемент массива
random(arr) {
return arr[Math.floor(Math.random() * arr.length)];
},
// Случайный элемент, исключая указанный
randomExcept(arr, excluded) {
const filtered = arr.filter(x => x !== excluded);
return filtered[Math.floor(Math.random() * filtered.length)];
},
// Перемешать массив
shuffle(arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
},
// Уникальные случайные элементы
uniqueRandom(arr, count) {
const shuffled = this.shuffle(arr);
return shuffled.slice(0, count);
},
// Форматирование времени
formatTime(seconds) {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
},
// Случайный цвет
randomColor() {
return Utils.random(Object.keys(COLORS));
},
// Случайная фигура
randomShape() {
return Utils.random(SHAPES);
},
// Случайный размер (большой/маленький)
randomSize() {
return Math.random() < 0.5 ? 'big' : 'small';
}
};
/* ============================================
РЕНДЕРИНГ ФИГУР НА CANVAS
============================================ */
const Renderer = {
// Отрисовка одной фигуры
drawShape(canvas, shape, color, size = 'big', clickable = false) {
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
const scale = size === 'big' ? 1 : 0.6;
ctx.clearRect(0, 0, w, h);
const cx = w / 2;
const cy = h / 2;
const baseSize = Math.min(w, h) * 0.35 * scale;
// Тень
ctx.shadowColor = 'rgba(74, 74, 106, 0.2)';
ctx.shadowBlur = 8;
ctx.shadowOffsetY = 4;
ctx.fillStyle = COLORS[color];
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
ctx.lineWidth = 3;
ctx.beginPath();
switch (shape) {
case 'circle':
ctx.arc(cx, cy, baseSize, 0, Math.PI * 2);
break;
case 'square':
const s = baseSize * 1.5;
ctx.rect(cx - s/2, cy - s/2, s, s);
break;
case 'rectangle':
const rw = baseSize * 2;
const rh = baseSize * 1.3;
ctx.rect(cx - rw/2, cy - rh/2, rw, rh);
break;
case 'triangle':
const ts = baseSize * 1.7;
ctx.moveTo(cx, cy - ts/2);
ctx.lineTo(cx - ts/2, cy + ts/2);
ctx.lineTo(cx + ts/2, cy + ts/2);
ctx.closePath();
break;
case 'oval':
ctx.ellipse(cx, cy, baseSize * 1.5, baseSize, 0, 0, Math.PI * 2);
break;
}
ctx.fill();
ctx.shadowColor = 'transparent';
ctx.stroke();
// Блик
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.beginPath();
if (shape === 'circle' || shape === 'oval') {
ctx.ellipse(cx - baseSize * 0.3, cy - baseSize * 0.3, baseSize * 0.3, baseSize * 0.2, 0, 0, Math.PI * 2);
} else {
ctx.ellipse(cx - baseSize * 0.3, cy - baseSize * 0.3, baseSize * 0.25, baseSize * 0.15, 0, 0, Math.PI * 2);
}
ctx.fill();
},
// Создать canvas элемент
createCanvas(width, height, clickable = false) {
const canvas = document.createElement('canvas');
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
canvas.className = 'shape-canvas';
if (clickable) canvas.classList.add('clickable');
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
return canvas;
}
};
/* ============================================
ГЕНЕРАТОР ЗАДАНИЙ
============================================ */
const QuestionGenerator = {
// Все типы заданий
generators: [],
init() {
this.generators = [
this.genNameTheShape.bind(this), // 1. Назови фигуру
this.genFindShape.bind(this), // 2. Найди фигуру
this.genCountCorners.bind(this), // 3. Сколько углов?
this.genFindOdd.bind(this), // 4. Найди лишнюю
this.genChooseByProperty.bind(this), // 5. Выбери по признаку
this.genCompareShapes.bind(this), // 6. Сравни фигуры
this.genFindByDescription.bind(this), // 7. Найди по описанию
this.genFindPair.bind(this), // 8. Найди пару
this.genGuessBySigns.bind(this), // 9. Угадай по признакам
this.genCollectAll.bind(this) // 10. Собери все
];
},
// 1. Назови фигуру - показывается фигура, нужно выбрать название
genNameTheShape(difficulty) {
const shape = Utils.randomShape();
const color = Utils.randomColor();
const size = Utils.randomSize();
// Варианты ответов - название фигуры + 3 других
const wrongOptions = Utils.uniqueExcept(SHAPES, shape, 3);
const options = Utils.shuffle([shape, ...wrongOptions]).map(s => SHAPE_NAMES[s]);
const correct = SHAPE_NAMES[shape];
return {
type: 'name',
helper: `Как называется эта фигура? 🤔`,
shape: { type: shape, color, size },
options,
correct,
points: 10
};
},
// 2. Найди фигуру - называется фигура, нужно кликнуть
genFindShape(difficulty) {
const target = Utils.randomShape();
const targetColor = Utils.randomColor();
// Создаём 4 фигуры, одна из них - целевая
const shapes = [];
const usedShapes = [target];
shapes.push({ type: target, color: targetColor, size: 'big', isTarget: true });
while (shapes.length < 4) {
const s = Utils.randomShape();
const c = Utils.randomColor();
// Чтобы не было слишком просто - хотя бы цвет или тип отличается
if (s !== target || c !== targetColor) {
shapes.push({ type: s, color: c, size: Utils.randomSize(), isTarget: false });
}
}
return {
type: 'find',
helper: `Найди и нажми на ${SHAPE_NAMES[target]}! 👆`,
shapes: Utils.shuffle(shapes),
correctIndex: shapes.findIndex(s => s.isTarget),
points: 10
};
},
// 3. Сколько углов?
genCountCorners(difficulty) {
// Выбираем фигуру с углами (не круг и не овал)
const shapesWithCorners = ['square', 'triangle', 'rectangle'];
const shape = Utils.random(shapesWithCorners);
const color = Utils.randomColor();
const correct = SHAPE_PROPERTIES[shape].corners;
const options = Utils.shuffle([0, 3, 4, 5]).map(String);
return {
type: 'corners',
helper: `Сколько углов у этой фигуры? 🔺`,
shape: { type: shape, color, size: 'big' },
options,
correct: String(correct),
points: 10
};
},
// 4. Найди лишнюю фигуру в ряду
genFindOdd(difficulty) {
// Три фигуры одного типа, одна - другого
const mainType = Utils.randomShape();
const oddType = Utils.randomExcept(SHAPES, mainType);
const color = Utils.randomColor();
const shapes = [];
const oddIndex = Math.floor(Math.random() * 4);
for (let i = 0; i < 4; i++) {
if (i === oddIndex) {
shapes.push({ type: oddType, color, size: 'big', isOdd: true });
} else {
shapes.push({ type: mainType, color, size: 'big', isOdd: false });
}
}
return {
type: 'odd',
helper: `Какая фигура здесь лишняя? Она не такая, как остальные! 🔍`,
shapes,
correctIndex: oddIndex,
points: 10
};
},
// 5. Выбери все фигуры с определённым признаком
genChooseByProperty(difficulty) {
// Признак: "без углов" (круг и овал) или "с углами"
const hasCorners = Math.random() < 0.5;
const description = hasCorners ? 'у которых есть углы' : 'у которых нет углов';
const shapes = [];
const correctIndices = [];
// 6 фигур, некоторые подходят, некоторые нет
for (let i = 0; i < 6; i++) {
const type = Utils.randomShape();
const color = Utils.randomColor();
const fits = SHAPE_PROPERTIES[type].hasCorners === hasCorners;
shapes.push({ type, color, size: 'small', fits });
if (fits) correctIndices.push(i);
}
// Гарантируем, что есть хотя бы 1 подходящая и 1 неподходящая
if (correctIndices.length === 0) {
const targetType = hasCorners ? 'square' : 'circle';
shapes[0] = { type: targetType, color: Utils.randomColor(), size: 'small', fits: true };
correctIndices.push(0);
}
return {
type: 'choose',
helper: `Выбери все фигуры ${description}! Нажимай на каждую подходящую 👆`,
shapes,
correctIndices,
points: 15
};
},
// 6. Сравни фигуры - у какой больше углов
genCompareShapes(difficulty) {
const shapesWithCorners = ['square', 'triangle', 'rectangle'];
const shape1 = Utils.random(shapesWithCorners);
let shape2 = Utils.randomExcept(shapesWithCorners, shape1);
const color1 = Utils.randomColor();
const color2 = Utils.randomExcept(Object.keys(COLORS), color1);
const c1 = SHAPE_PROPERTIES[shape1].corners;
const c2 = SHAPE_PROPERTIES[shape2].corners;
let correct;
if (c1 > c2) correct = 'первая';
else if (c2 > c1) correct = 'вторая';
else correct = 'одинаково';
return {
type: 'compare',
helper: `У какой фигуры больше углов? 🤔`,
shapes: [
{ type: shape1, color: color1, size: 'big' },
{ type: shape2, color: color2, size: 'big' }
],
options: ['Первая фигура', 'Вторая фигура', 'Одинаково'],
correct,
points: 10
};
},
// 7. Найди фигуру по описанию (цвет + размер + тип)
genFindByDescription(difficulty) {
const targetShape = Utils.randomShape();
const targetColor = Utils.randomColor();
const targetSize = Utils.randomSize();
const shapes = [];
const correctIndex = Math.floor(Math.random() * 4);
for (let i = 0; i < 4; i++) {
if (i === correctIndex) {
shapes.push({ type: targetShape, color: targetColor, size: targetSize, isTarget: true });
} else {
// Меняем хотя бы один признак
const variation = Math.floor(Math.random() * 3);
let type = targetShape, color = targetColor, size = targetSize;
if (variation === 0) type = Utils.randomExcept(SHAPES, targetShape);
else if (variation === 1) color = Utils.randomExcept(Object.keys(COLORS), targetColor);
else size = targetSize === 'big' ? 'small' : 'big';
shapes.push({ type, color, size, isTarget: false });
}
}
const sizeText = targetSize === 'big' ? 'большой' : 'маленький';
const colorText = COLOR_NAMES[targetColor];
const shapeText = SHAPE_NAMES[targetShape];
return {
type: 'findDesc',
helper: `Найди ${sizeText} ${colorText} ${shapeText}! 🔎`,
shapes: Utils.shuffle(shapes),
correctIndex: shapes.findIndex(s => s.isTarget),
points: 15
};
},
// 8. Найди пару - две одинаковые по форме фигуры
genFindPair(difficulty) {
const pairShape = Utils.randomShape();
const pairColor = Utils.randomColor();
const shapes = [];
// 6 фигур: 2 одинаковые (пара), остальные разные
const positions = [0, 1, 2, 3, 4, 5];
const pairPositions = Utils.uniqueRandom(positions, 2);
for (let i = 0; i < 6; i++) {
if (pairPositions.includes(i)) {
shapes.push({ type: pairShape, color: pairColor, size: 'small', isPair: true });
} else {
shapes.push({
type: Utils.randomShape(),
color: Utils.randomColor(),
size: 'small',
isPair: false
});
}
}
return {
type: 'pair',
helper: `Найди две одинаковые фигуры (одинаковые по форме и цвету)! 👯`,
shapes,
pairPositions,
points: 15
};
},
// 9. Угадай фигуру по признакам
genGuessBySigns(difficulty) {
const shape = Utils.randomShape();
const props = SHAPE_PROPERTIES[shape];
const color = Utils.randomColor();
let description = `У этой фигуры `;
if (props.hasCorners) {
description += `${props.corners} угла`;
} else {
description += `нет углов`;
}
description += `, и она ${COLOR_NAMES[color]}го цвета.`;
const options = Utils.shuffle([...SHAPES]).map(s => SHAPE_NAMES[s]);
const correct = SHAPE_NAMES[shape];
return {
type: 'guess',
helper: `Угадай фигуру: ${description} 🧩`,
options,
correct,
points: 15
};
},
// 10. Собери все фигуры определённого типа
genCollectAll(difficulty) {
const targetShape = Utils.randomShape();
const shapes = [];
const correctIndices = [];
// 8 фигур, 3 из них - целевые
const targetCount = 3;
const targetPositions = Utils.uniqueRandom([0,1,2,3,4,5,6,7], targetCount);
for (let i = 0; i < 8; i++) {
if (targetPositions.includes(i)) {
shapes.push({ type: targetShape, color: Utils.randomColor(), size: 'small', isTarget: true });
correctIndices.push(i);
} else {
shapes.push({ type: Utils.randomExcept(SHAPES, targetShape), color: Utils.randomColor(), size: 'small', isTarget: false });
}
}
return {
type: 'collect',
helper: `Собери все ${SHAPE_NAMES[targetShape]}! Нажимай на каждый по очереди 👆`,
shapes,
correctIndices,
points: 20
};
},
// Сгенерировать все вопросы
generateAll() {
const questions = [];
for (let i = 0; i < CONFIG.TOTAL_QUESTIONS; i++) {
const difficulty = i / CONFIG.TOTAL_QUESTIONS; // от 0 до 1
const gen = this.generators[i % this.generators.length];
questions.push(gen(difficulty));
}
return Utils.shuffle(questions);
}
};
// Вспомогательная функция
Utils.uniqueExcept = function(arr, excluded, count) {
const filtered = arr.filter(x => x !== excluded);
return this.uniqueRandom(filtered, count);
};
/* ============================================
МЕНЕДЖЕР ТАЙМЕРА
============================================ */
const Timer = {
start() {
this.update();
GameState.timerInterval = setInterval(() => {
GameState.timeLeft--;
this.update();
if (GameState.timeLeft <= 0) {
this.stop();
Game.end('time');
}
}, 1000);
},
stop() {
if (GameState.timerInterval) {
clearInterval(GameState.timerInterval);
GameState.timerInterval = null;
}
},
update() {
const timerEl = document.getElementById('timer');
const timerBox = document.getElementById('timer-box');
timerEl.textContent = Utils.formatTime(GameState.timeLeft);
if (GameState.timeLeft <= 60) {
timerBox.classList.add('warning');
} else {
timerBox.classList.remove('warning');
}
},
addBonus(seconds) {
GameState.timeLeft += seconds;
this.update();
}
};
/* ============================================
ХРАНЕНИЕ ДАННЫХ
============================================ */
const Storage = {
getBestScore() {
try {
return parseInt(localStorage.getItem(CONFIG.STORAGE_KEY) || '0', 10);
} catch (e) {
return 0;
}
},
saveBestScore(score) {
try {
const current = this.getBestScore();
if (score > current) {
localStorage.setItem(CONFIG.STORAGE_KEY, String(score));
return true;
}
return false;
} catch (e) {
return false;
}
}
};
/* ============================================
ЗВУКОВЫЕ ЭФФЕКТЫ (Web Audio API)
============================================ */
const Sound = {
ctx: null,
init() {
try {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
} catch (e) {
this.ctx = null;
}
},
play(type) {
if (!this.ctx) return;
try {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.connect(gain);
gain.connect(this.ctx.destination);
const now = this.ctx.currentTime;
if (type === 'correct') {
osc.frequency.setValueAtTime(523, now);
osc.frequency.setValueAtTime(659, now + 0.1);
osc.frequency.setValueAtTime(784, now + 0.2);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
osc.start(now);
osc.stop(now + 0.4);
} else if (type === 'wrong') {
osc.frequency.setValueAtTime(200, now);
osc.frequency.setValueAtTime(150, now + 0.15);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
} else if (type === 'click') {
osc.frequency.setValueAtTime(800, now);
gain.gain.setValueAtTime(0.08, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.05);
osc.start(now);
osc.stop(now + 0.05);
} else if (type === 'win') {
[523, 659, 784, 1047].forEach((freq, i) => {
const o = this.ctx.createOscillator();
const g = this.ctx.createGain();
o.connect(g);
g.connect(this.ctx.destination);
o.frequency.setValueAtTime(freq, now + i * 0.15);
g.gain.setValueAtTime(0.15, now + i * 0.15);
g.gain.exponentialRampToValueAtTime(0.01, now + i * 0.15 + 0.3);
o.start(now + i * 0.15);
o.stop(now + i * 0.15 + 0.3);
});
}
} catch (e) {
// Игнорируем ошибки звука
}
}
};
/* ============================================
ГЛАВНЫЙ ИГРОВОЙ МОДУЛЬ
============================================ */
const Game = {
init() {
QuestionGenerator.init();
Sound.init();
this.bindEvents();
this.showBestScore();
},
bindEvents() {
document.getElementById('start-btn').addEventListener('click', () => {
Sound.play('click');
this.start();
});
document.getElementById('restart-btn').addEventListener('click', () => {
Sound.play('click');
this.start();
});
document.getElementById('home-btn').addEventListener('click', () => {
Sound.play('click');
this.showScreen('start-screen');
this.showBestScore();
});
},
showBestScore() {
const best = Storage.getBestScore();
const box = document.getElementById('best-score-box');
const value = document.getElementById('best-score-value');
if (best > 0) {
value.textContent = best;
box.style.display = 'block';
} else {
box.style.display = 'none';
}
},
showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
},
start() {
// Сброс состояния
GameState.currentQuestion = 0;
GameState.score = 0;
GameState.correctAnswers = 0;
GameState.timeLeft = CONFIG.GAME_DURATION;
GameState.questions = QuestionGenerator.generateAll();
GameState.isAnswering = false;
GameState.startTime = Date.now();
this.showScreen('game-screen');
this.updateUI();
Timer.start();
this.showQuestion();
},
updateUI() {
document.getElementById('score').textContent = GameState.score;
document.getElementById('progress').textContent =
`${GameState.currentQuestion + 1}/${CONFIG.TOTAL_QUESTIONS}`;
const progressPercent = (GameState.currentQuestion / CONFIG.TOTAL_QUESTIONS) * 100;
document.getElementById('progress-fill').style.width = progressPercent + '%';
},
showQuestion() {
if (GameState.currentQuestion >= CONFIG.TOTAL_QUESTIONS) {
this.end('complete');
return;
}
GameState.isAnswering = false;
GameState.questionStartTime = Date.now();
const q = GameState.questions[GameState.currentQuestion];
const helperText = document.getElementById('helper-text');
const canvasContainer = document.getElementById('canvas-container');
const optionsGrid = document.getElementById('options-grid');
helperText.textContent = q.helper;
canvasContainer.innerHTML = '';
optionsGrid.innerHTML = '';
// Рендерим в зависимости от типа задания
switch (q.type) {
case 'name':
case 'corners':
case 'guess':
this.renderSingleShape(q);
this.renderOptions(q);
break;
case 'find':
case 'findDesc':
this.renderMultipleShapes(q, 'single');
break;
case 'odd':
this.renderMultipleShapes(q, 'single');
break;
case 'choose':
this.renderMultipleShapes(q, 'multi');
break;
case 'compare':
this.renderCompareShapes(q);
this.renderOptions(q);
break;
case 'pair':
this.renderMultipleShapes(q, 'pair');
break;
case 'collect':
this.renderMultipleShapes(q, 'collect');
break;
}
this.updateUI();
},
// Отобразить одну фигуру
renderSingleShape(q) {
const container = document.getElementById('canvas-container');
const canvas = Renderer.createCanvas(200, 200);
Renderer.drawShape(canvas, q.shape.type, q.shape.color, q.shape.size);
container.appendChild(canvas);
},
// Отобразить несколько фигур
renderMultipleShapes(q, mode) {
const container = document.getElementById('canvas-container');
const count = q.shapes.length;
const size = count > 4 ? 90 : (count > 2 ? 110 : 140);
q.shapes.forEach((shapeData, index) => {
const canvas = Renderer.createCanvas(size, size, true);
Renderer.drawShape(canvas, shapeData.type, shapeData.color, shapeData.size);
canvas.dataset.index = index;
canvas.addEventListener('click', () => {
this.handleShapeClick(q, index, canvas, mode);
});
container.appendChild(canvas);
});
},
// Отобразить сравнение двух фигур
renderCompareShapes(q) {
const container = document.getElementById('canvas-container');
q.shapes.forEach(shapeData => {
const canvas = Renderer.createCanvas(150, 150);
Renderer.drawShape(canvas, shapeData.type, shapeData.color, shapeData.size);
container.appendChild(canvas);
});
},
// Отобразить варианты ответов
renderOptions(q) {
const grid = document.getElementById('options-grid');
q.options.forEach(option => {
const btn = document.createElement('button');
btn.className = 'option-btn';
btn.textContent = option;
btn.addEventListener('click', () => this.handleOptionClick(q, option, btn));
grid.appendChild(btn);
});
},
// Обработка клика по фигуре (одиночный выбор)
handleShapeClick(q, index, canvas, mode) {
if (GameState.isAnswering) return;
Sound.play('click');
if (mode === 'single') {
GameState.isAnswering = true;
const isCorrect = index === q.correctIndex;
this.processAnswer(isCorrect, q.points, () => {
canvas.classList.add(isCorrect ? 'selected' : '');
if (!isCorrect) {
// Показать правильный ответ
const canvases = document.querySelectorAll('#canvas-container .shape-canvas');
canvases[q.correctIndex].classList.add('selected');
}
});
} else if (mode === 'pair') {
// Режим "найди пару" - нужно кликнуть 2 одинаковые
if (!q.selected) q.selected = [];
if (q.selected.includes(index)) return;
q.selected.push(index);
canvas.classList.add('selected');
if (q.selected.length === 2) {
GameState.isAnswering = true;
const [a, b] = q.selected;
const isCorrect = q.pairPositions.includes(a) && q.pairPositions.includes(b);
this.processAnswer(isCorrect, q.points);
}
} else if (mode === 'collect') {
// Режим "собери все" - нужно кликнуть все правильные
if (!q.selected) q.selected = [];
if (q.selected.includes(index)) return;
q.selected.push(index);
const isCorrect = q.correctIndices.includes(index);
if (isCorrect) {
canvas.classList.add('selected');
Sound.play('correct');
// Проверить, все ли выбраны
const allSelected = q.correctIndices.every(i => q.selected.includes(i));
if (allSelected) {
GameState.isAnswering = true;
this.processAnswer(true, q.points);
}
} else {
// Неправильный выбор - завершаем задание
GameState.isAnswering = true;
canvas.style.background = 'var(--error)';
Sound.play('wrong');
this.showFeedback('❌');
setTimeout(() => this.nextQuestion(), 1500);
}
} else if (mode === 'multi') {
// Режим "выбери все по признаку"
if (!q.selected) q.selected = [];
if (q.selected.includes(index)) return;
q.selected.push(index);
const isCorrectShape = q.correctIndices.includes(index);
if (isCorrectShape) {
canvas.classList.add('selected');
Sound.play('correct');
} else {
GameState.isAnswering = true;
canvas.style.background = 'var(--error)';
Sound.play('wrong');
this.showFeedback('❌');
// Показать все правильные
const canvases = document.querySelectorAll('#canvas-container .shape-canvas');
q.correctIndices.forEach(i => canvases[i].classList.add('selected'));
setTimeout(() => this.nextQuestion(), 1500);
return;
}
// Проверить, все ли правильные выбраны
const allCorrectSelected = q.correctIndices.every(i => q.selected.includes(i));
// И нет ли лишних
const hasWrong = q.selected.some(i => !q.correctIndices.includes(i));
if (allCorrectSelected && !hasWrong) {
GameState.isAnswering = true;
this.processAnswer(true, q.points);
}
}
},
// Обработка клика по варианту ответа
handleOptionClick(q, option, btn) {
if (GameState.isAnswering) return;
GameState.isAnswering = true;
const isCorrect = option === q.correct;
Sound.play(isCorrect ? 'correct' : 'wrong');
btn.classList.add(isCorrect ? 'correct' : 'wrong');
// Заблокировать все кнопки
document.querySelectorAll('.option-btn').forEach(b => {
b.disabled = true;
if (b.textContent === q.correct) b.classList.add('correct');
});
this.processAnswer(isCorrect, q.points);
},
// Обработка ответа
processAnswer(isCorrect, points, callback) {
if (isCorrect) {
GameState.score += points;
GameState.correctAnswers++;
// Бонус за быстрый ответ
const answerTime = (Date.now() - GameState.questionStartTime) / 1000;
if (answerTime < CONFIG.FAST_ANSWER_THRESHOLD) {
GameState.score += CONFIG.BONUS_TIME;
Timer.addBonus(2);
}
this.showFeedback('🎉');
Sound.play('correct');
} else {
this.showFeedback('😔');
Sound.play('wrong');
}
if (callback) callback();
this.updateUI();
setTimeout(() => this.nextQuestion(), 1800);
},
// Показать эмодзи обратной связи
showFeedback(emoji) {
const feedback = document.getElementById('feedback');
feedback.textContent = emoji;
feedback.classList.remove('show');
void feedback.offsetWidth; // reflow
feedback.classList.add('show');
},
// Следующий вопрос
nextQuestion() {
GameState.currentQuestion++;
if (GameState.currentQuestion >= CONFIG.TOTAL_QUESTIONS) {
this.end('complete');
} else {
this.showQuestion();
}
},
// Завершение игры
end(reason) {
Timer.stop();
Sound.play('win');
const timeSpent = Math.floor((Date.now() - GameState.startTime) / 1000);
const isNewBest = Storage.saveBestScore(GameState.score);
// Расчёт звёзд
const starsCount = Math.min(5, Math.ceil(GameState.correctAnswers / 2));
// Заполняем экран результатов
document.getElementById('final-score').textContent = GameState.score;
document.getElementById('final-correct').textContent =
`${GameState.correctAnswers}/${CONFIG.TOTAL_QUESTIONS}`;
document.getElementById('final-time').textContent = Utils.formatTime(timeSpent);
// Заголовок и сообщение в зависимости от результата
const percent = GameState.correctAnswers / CONFIG.TOTAL_QUESTIONS;
let title, message, mascot;
if (percent >= 0.9) {
title = 'Великолепно! 🏆';
message = 'Ты настоящий знаток фигур! Мудрая Сова гордится тобой!';
mascot = '🎉';
} else if (percent >= 0.7) {
title = 'Отлично! 🌟';
message = 'Ты очень хорошо знаешь фигуры! Продолжай в том же духе!';
mascot = '🎊';
} else if (percent >= 0.5) {
title = 'Хорошо! 👍';
message = 'Неплохой результат! Немного практики и будет идеально!';
mascot = '😊';
} else {
title = 'Молодец, что попробовал! 💪';
message = 'Фигуры — это интересно! Попробуй ещё раз, и у тебя всё получится!';
mascot = '🦉';
}
if (isNewBest) {
title += ' Новый рекорд!';
message += ' 🏆 Ты побил свой рекорд!';
}
document.getElementById('result-title').textContent = title;
document.getElementById('result-message').textContent = message;
document.getElementById('result-mascot').textContent = mascot;
// Звёздочки
const stars = document.querySelectorAll('#stars .star');
stars.forEach((star, i) => {
star.classList.remove('earned');
if (i < starsCount) {
setTimeout(() => star.classList.add('earned'), i * 200);
}
});
this.showScreen('result-screen');
// Конфетти при хорошем результате
if (percent >= 0.7) {
this.showConfetti();
}
},
// Эффект конфетти
showConfetti() {
const colors = ['#FFB5C5', '#B5D8FF', '#B5EBCD', '#E8D5FF', '#FFD5B5', '#FFF3B0'];
for (let i = 0; i < 50; i++) {
setTimeout(() => {
const confetti = document.createElement('div');
confetti.className = 'confetti';
confetti.style.left = Math.random() * 100 + 'vw';
confetti.style.background = Utils.random(colors);
confetti.style.animationDuration = (2 + Math.random() * 2) + 's';
confetti.style.animationDelay = Math.random() * 0.5 + 's';
if (Math.random() < 0.5) {
confetti.style.borderRadius = '50%';
} else {
confetti.style.borderRadius = '2px';
}
document.body.appendChild(confetti);
setTimeout(() => confetti.remove(), 4000);
}, i * 50);
}
}
};
/* ============================================
ЗАПУСК ПРИЛОЖЕНИЯ
============================================ */
document.addEventListener('DOMContentLoaded', () => {
Game.init();
});
// Инициализация аудио-контекста при первом взаимодействии
document.addEventListener('click', () => {
if (Sound.ctx && Sound.ctx.state === 'suspended') {
Sound.ctx.resume();
}
}, { once: true });
</script>
</body>
</html>
```