![]() |
Play the GAME here > |
Teaching reading, spelling, and sentence formation becomes more exciting through interactive learning games. This Jungle Connect-the-Dots Educational Game is designed for teachers, parents, and ESL/EFL learners who want a fun classroom activity for reading and language practice.
✨ Game Features
- ✅ Single-player mode
- ✅ Team competition mode
- ✅ Sentence formation activities
- ✅ Word spelling practice
- ✅ Audio effects and animations
- ✅ Teacher settings panel
- ✅ Import and export question database
PART 1 — How to Play the Game
🌟 Objective of the Game
Players connect the correct letters or words in order to form a complete word or sentence.
🕹️ Step-by-Step Guide
Step 1 — Open the Game
Open the HTML file in your browser and click:
🎮 Play Game
Step 2 — Read the Challenge
Students will see letters or words inside circles scattered around the screen.
Step 3 — Connect the Dots
Drag the mouse or finger from one circle to another in the correct order.
Examples:
- T → I → G → E → R
- She → is → happy.
Step 4 — Check the Answer
Click:
✅ Check Answer
The system checks the answer and gives feedback instantly.
🏆 Scoring System
✅ Correct Answer
- +1 point
- Celebration animation
- Victory sound effect
❌ Wrong Answer
- -1 point
- Students can try again
👥 Team Mode
Click:
👥 Switch Team Mode
This creates Team A and Team B for classroom competitions and collaborative learning activities.
⚙️ How to Use the Buttons
| Button | Purpose |
|---|---|
| 🎵 Music ON/OFF | Turns the background music on or off. |
| 🔊 Volume Slider | Adjusts the game audio volume. |
| 👥 Switch Team Mode | Changes the game into team competition mode. |
| ⚙️ Settings Panel | Opens the teacher control panel. |
| ✅ Check Answer | Checks the player's answer. |
| 🔄 Clear Paths | Removes all current connections. |
How Teachers Can Add Questions
- Open the ⚙️ Settings Panel
- Select:
- Sentence Formation
- Spelling Mode
- Type the content.
- Optional: Paste a clue image using Ctrl + V
- Click ➕ Append to Pool
Example Questions:
- She is happy.
- Tiger
- The monkey climbs trees.
Export and Import Features
Teachers can save and reuse classroom activities.
- 📤 Export Pool — Downloads question data as a JSON file.
- 📥 Import Pool — Loads saved activities from another device.
Educational Benefits
This game supports:
- Phonics learning
- Vocabulary recognition
- Sentence construction
- Reading fluency
- Critical thinking
- Collaborative learning
It is highly suitable for:
- Elementary students
- ESL learners
- EFL classrooms
- Interactive English learning activities
PART 2 — Prompts and Exact Codes
✨ Suggested AI Prompt
Create an HTML educational game with a jungle theme connect-the-dots activity. The game should: - allow sentence formation and spelling activities, - include draggable circles with letters or words, - have single-player and team modes, - include score tracking, - support background music and sound effects, - include a settings panel for teachers, - allow importing and exporting JSON question data, - support image clues, - use responsive design for desktop and mobile, - and have engaging animations for students. The design should be colorful, kid-friendly, and suitable for ESL/EFL learners.
📌 Paste Your Exact Game Code Below
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jungle Connect-the-Dots Educational Game</title>
<style>
/* Import an EFL-friendly, highly legible comic font for young learners */
@import url('https://fonts.googleapis.com/css2?family=Fredoka+One&family=Comic+Neue:wght@700&display=swap');
:root {
--jungle-green: #2d6a4f;
--light-green: #52b788;
--wood-brown: #8c6239;
--light-wood: #b38659;
--cream: #f4f1de;
--gold: #ffb703;
}
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
margin: 0;
padding: 0;
}
body {
font-family: 'Comic Neue', 'Fredoka One', cursive, sans-serif;
background-color: #1a3a2a;
color: var(--cream);
overflow: hidden;
width: 100vw;
height: 100vh;
}
/* App Wrapper */
#app-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-image: url('background.gif');
background-size: cover;
background-position: center;
}
/* Top Bar Interface */
header {
background: rgba(45, 106, 79, 0.9);
border-bottom: 5px solid var(--wood-brown);
padding: 12px 24px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 10;
}
.title-area h1 {
font-size: 28px;
color: var(--gold);
text-shadow: 2px 2px #000;
font-family: 'Fredoka One', cursive, sans-serif;
}
.global-controls {
display: flex;
gap: 12px;
align-items: center;
}
/* Buttons Style */
.jungle-btn {
font-family: 'Comic Neue', sans-serif;
font-weight: 700;
background: linear-gradient(to bottom, var(--wood-brown), #5c3a21);
color: var(--cream);
border: 3px solid #3d2514;
border-radius: 8px;
padding: 10px 20px;
cursor: pointer;
font-size: 16px;
text-shadow: 1px 1px #000;
box-shadow: 0 5px 0px #3d2514;
transition: all 0.1s ease;
}
.jungle-btn:active {
transform: translateY(3px);
box-shadow: 0 2px 0px #3d2514;
}
.jungle-btn.green-btn {
background: linear-gradient(to bottom, var(--light-green), var(--jungle-green));
border-color: #1b4332;
box-shadow: 0 5px 0px #1b4332;
}
/* Audio Control Sub-panel */
.audio-popover {
background: rgba(0,0,0,0.8);
padding: 10px 14px;
border-radius: 10px;
display: flex;
align-items: center;
gap: 12px;
border: 2px solid var(--light-green);
}
/* Game Modes Containers */
#game-stage {
flex: 1;
display: flex;
position: relative;
width: 100%;
height: calc(100% - 78px);
}
.game-zone {
flex: 1;
position: relative;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.game-zone:nth-child(2) {
border-left: 5px dashed var(--wood-brown);
background: rgba(0, 0, 0, 0.15);
}
/* Shared Wooden Floating Signboards */
.wooden-sign {
background: linear-gradient(135deg, var(--light-wood), var(--wood-brown));
border: 5px solid #4a3119;
border-radius: 12px;
padding: 14px 20px;
box-shadow: 0 8px 16px rgba(0,0,0,0.6), inset 0 0 12px rgba(0,0,0,0.3);
text-align: center;
margin: 12px auto;
width: 90%;
max-width: 90%;
z-index: 5;
position: relative;
}
.hud-row {
display: flex;
justify-content: space-between;
align-items: center;
width: 92%;
margin: 8px auto 0 auto;
z-index: 5;
}
.answer-box {
font-size: 32px;
font-weight: bold;
color: #fff;
min-height: 50px;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: 1.5px;
text-shadow: 2px 2px 3px #000;
}
.score-badge {
font-size: 18px;
background: rgba(0,0,0,0.7);
padding: 6px 14px;
border-radius: 20px;
border: 2px solid var(--gold);
color: var(--gold);
font-weight: bold;
}
/* Picture Clue Box Style */
.clue-pic-holder {
width: 110px;
height: 110px;
margin: 5px auto 0 auto;
border: 3px solid var(--gold);
border-radius: 8px;
background: rgba(0,0,0,0.4);
display: none;
overflow: hidden;
justify-content: center;
align-items: center;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
}
.clue-pic-holder img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
/* Canvas Overlay Area */
.canvas-container {
flex: 1;
position: relative;
width: 100%;
}
.dot-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
}
/* Celebration Confetti Foreground Canvas Layer */
.confetti-canvas {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none;
z-index: 4;
}
/* Enriched Circle Node Elements */
.dot-node {
position: absolute;
width: 110px;
height: 110px;
border-radius: 50%;
background: radial-gradient(circle, #ffeaa7, #fdcb6e);
border: 5px solid var(--wood-brown);
box-shadow: 0 6px 12px rgba(0,0,0,0.4), inset 0 0 10px rgba(255,255,255,0.7);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 24px;
color: #2d3436;
cursor: pointer;
z-index: 3;
transform: translate(-50%, -50%);
transition: transform 0.2s, background 0.3s;
text-align: center;
word-break: break-word;
padding: 8px;
line-height: 1.1;
}
.dot-node:hover {
transform: translate(-50%, -50%) scale(1.08);
}
.dot-node.selected {
background: radial-gradient(circle, #55efc4, #00b894);
border-color: #006266;
color: #fff;
box-shadow: 0 0 20px #55efc4;
}
/* Generic Overlay Layout style used by Welcome message and Feedback boards */
.feedback-overlay {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.75);
z-index: 90;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.feedback-overlay.active {
opacity: 1;
pointer-events: auto;
}
.feedback-card {
transform: scale(0.7);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
max-width: 550px !important;
width: 90%;
}
.feedback-overlay.active .feedback-card {
transform: scale(1);
}
.feedback-text {
font-size: 42px;
font-weight: bold;
margin-bottom: 15px;
text-shadow: 2px 2px #000;
font-family: 'Fredoka One', cursive, sans-serif;
}
.feedback-success { color: #2ecc71; }
.feedback-fail { color: #e74c3c; }
/* Settings System Panel Overlay */
#settings-panel {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(26, 58, 42, 0.98);
z-index: 100;
display: none;
padding: 24px;
overflow-y: auto;
}
.settings-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
max-width: 1200px;
margin: 0 auto;
}
@media (max-width: 768px) {
.settings-grid { grid-template-columns: 1fr; }
#game-stage { flex-direction: column; }
.game-zone:nth-child(2) { border-left: none; border-top: 5px dashed var(--wood-brown); }
.dot-node { width: 90px; height: 90px; font-size: 18px; }
.answer-box { font-size: 24px; }
}
.settings-block {
background: rgba(255,255,255,0.05);
border: 2px solid var(--wood-brown);
border-radius: 8px;
padding: 15px;
}
.settings-block h3 {
color: var(--gold);
margin-bottom: 12px;
border-bottom: 1px solid var(--wood-brown);
padding-bottom: 4px;
font-family: 'Fredoka One', sans-serif;
}
.form-group {
margin-bottom: 12px;
}
label {
display: block;
margin-bottom: 4px;
font-size: 15px;
color: var(--light-green);
}
input[type="text"], select, textarea {
font-family: inherit;
width: 100%;
background: #11261c;
border: 2px solid var(--wood-brown);
border-radius: 6px;
padding: 10px;
color: #fff;
font-size: 15px;
}
.paste-box-preview {
width: 100%;
height: 90px;
border: 2px dashed var(--light-green);
background: rgba(0,0,0,0.2);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #aaa;
cursor: pointer;
font-size: 14px;
margin-top: 5px;
overflow: hidden;
}
.paste-box-preview img {
max-height: 100%;
object-fit: contain;
}
.question-list-item {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(0,0,0,0.3);
padding: 8px 12px;
margin-bottom: 8px;
border-radius: 6px;
font-size: 14px;
}
.btn-sm {
padding: 6px 12px;
font-size: 14px;
}
.toggle-switch {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
.toggle-switch input {
width: 40px;
height: 20px;
}
</style>
</head>
<body>
<div id="app-container">
<header>
<div class="title-area">
<h1 id="game-title-text">Jungle Connect Game</h1>
</div>
<div class="global-controls">
<div class="audio-popover">
<button class="jungle-btn btn-sm" id="btn-toggle-audio">🎵 Music: OFF</button>
<input type="range" id="vol-slider" min="0" max="1" step="0.1" value="0.4" style="width:70px;">
</div>
<button class="jungle-btn" id="btn-toggle-mode">👥 Switch Team Mode</button>
<button class="jungle-btn green-btn" id="btn-open-settings">⚙️ Settings Panel</button>
</div>
</header>
<main id="game-stage">
<div class="game-zone" id="zone-left">
<div class="hud-row">
<div class="score-badge" id="score-left">Score: 0 / 0</div>
<div class="score-badge" id="mode-badge-left">Single Player Mode</div>
</div>
<div class="wooden-sign">
<div class="answer-box" id="ans-box-left">Connect dots to answer!</div>
<div class="clue-pic-holder" id="clue-holder-left"></div>
</div>
<div class="canvas-container" id="canvas-container-left">
<canvas class="dot-canvas" id="canvas-left"></canvas>
<canvas class="confetti-canvas" id="confetti-left"></canvas>
</div>
<div style="padding:15px; text-align:center; z-index:5;">
<button class="jungle-btn green-btn" id="btn-check-left" style="width:45%; max-width:220px; font-size:18px;">Check Answer</button>
<button class="jungle-btn" id="btn-reset-left" style="width:45%; max-width:220px; margin-left:10px; font-size:18px;">Clear Paths</button>
</div>
<div class="feedback-overlay" id="feedback-left">
<div class="wooden-sign feedback-card">
<div class="feedback-text" id="feedback-text-left">Well Done!</div>
<p id="feedback-sub-left" style="margin-bottom:20px; color:#fff; font-size: 20px;"></p>
<div style="display:flex; justify-content:center; gap:10px;">
<button class="jungle-btn green-btn" id="btn-feedback-next-left" style="font-size: 18px;">Next Challenge</button>
<button class="jungle-btn" id="btn-replay-left" style="font-size: 18px; display:none; background: linear-gradient(to bottom, #ffb703, #fb8500); border-color:#d4a373;">🔄 Replay Game</button>
</div>
</div>
</div>
</div>
<div class="game-zone" id="zone-right" style="display: none;">
<div class="hud-row">
<div class="score-badge" id="score-right">Score: 0 / 0</div>
<div class="score-badge" style="color:#55efc4; border-color:#55efc4;">Team B</div>
</div>
<div class="wooden-sign">
<div class="answer-box" id="ans-box-right">Connect dots to answer!</div>
<div class="clue-pic-holder" id="clue-holder-right"></div>
</div>
<div class="canvas-container" id="canvas-container-right">
<canvas class="dot-canvas" id="canvas-right"></canvas>
<canvas class="confetti-canvas" id="confetti-right"></canvas>
</div>
<div style="padding:15px; text-align:center; z-index:5;">
<button class="jungle-btn green-btn" id="btn-check-right" style="width:45%; max-width:220px; font-size:18px;">Check Answer</button>
<button class="jungle-btn" id="btn-reset-right" style="width:45%; max-width:220px; margin-left:10px; font-size:18px;">Clear Paths</button>
</div>
<div class="feedback-overlay" id="feedback-right">
<div class="wooden-sign feedback-card">
<div class="feedback-text" id="feedback-text-right">Well Done!</div>
<p id="feedback-sub-right" style="margin-bottom:20px; color:#fff; font-size: 20px;"></p>
<div style="display:flex; justify-content:center; gap:10px;">
<button class="jungle-btn green-btn" id="btn-feedback-next-right" style="font-size: 18px;">Next Challenge</button>
<button class="jungle-btn" id="btn-replay-right" style="font-size: 18px; display:none; background: linear-gradient(to bottom, #ffb703, #fb8500); border-color:#d4a373;">🔄 Replay Game</button>
</div>
</div>
</div>
</div>
</main>
<div class="feedback-overlay active" id="welcome-screen" style="z-index: 200;">
<div class="wooden-sign feedback-card" style="padding: 30px 20px;">
<div class="feedback-text" style="color: var(--gold);">🌴 Welcome to Jungle Connect! 🌴</div>
<div style="color: #fff; font-size: 20px; line-height: 1.6; margin-bottom: 25px; text-align: left; background: rgba(0,0,0,0.3); padding: 15px; border-radius: 8px;">
<p style="margin-bottom: 10px; font-weight: bold; text-align: center; color: #55efc4; font-size: 22px;">How to Play:</p>
<p>1. 👈 **Drag your finger or mouse** from dot to dot to connect letters or words in order.</p>
<p style="margin-top: 8px;">2. ✅ Click **"Check Answer"** to test your word sequence.</p>
<p style="margin-top: 8px; color: #ff7675;">⚠️ **Be careful!** If your answer is wrong, you will **lose 1 point** (-1)! You can try again until you solve it!</p>
</div>
<button class="jungle-btn green-btn" id="btn-close-welcome" style="font-size: 24px; padding: 12px 35px; font-family: 'Fredoka One', sans-serif;">🎮 Play Game</button>
</div>
</div>
<section id="settings-panel">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="color: var(--gold); font-family: 'Fredoka One', sans-serif;">⚙️ Teacher Administration Control Console</h2>
<button class="jungle-btn green-btn" id="btn-close-settings">💾 Save and Play</button>
</div>
<div class="settings-grid">
<div class="settings-block" style="grid-column: span 1;">
<h3>Add New Educational Challenge</h3>
<div class="form-group">
<label for="input-type">Challenge Type Mode</label>
<select id="input-type">
<option value="sentence">Sentence Formation (Split by spaces)</option>
<option value="spelling">Spelling Mode (Split by character tokens)</option>
</select>
</div>
<div class="form-group">
<label for="input-content">Content Data Text</label>
<input type="text" id="input-content" placeholder="e.g. She is happy. OR Tiger">
<small style="color:#aaa; font-size:12px;">Sentences split into words; spelling splits into individual letters.</small>
</div>
<div class="form-group">
<label>Optional Clue Picture (Click & press Ctrl+V to paste)</label>
<div class="paste-box-preview" id="image-paste-zone" tabIndex="0">
Click here & paste your photo (Ctrl+V)
</div>
</div>
<button class="jungle-btn green-btn btn-sm" id="btn-add-question" style="width: 100%;">➕ Append to Pool</button>
</div>
<div class="settings-block">
<h3>Game Rules & Mechanics Sync</h3>
<div class="form-group">
<div class="toggle-switch">
<input type="checkbox" id="sync-toggle" checked>
<label for="sync-toggle"><strong>Synchronize Target Pool Questions</strong> (Both teams get the same challenge order sequences)</label>
</div>
</div>
<hr style="border-color: var(--wood-brown); margin: 15px 0;">
<h3>Export and Import Data (.JSON Configuration)</h3>
<p style="font-size:12px; margin-bottom:10px; color:#bbb;">Save configuration profiles locally or backup database assets for separate devices.</p>
<div style="display:flex; gap:10px;">
<button class="jungle-btn btn-sm" id="btn-export-data">📤 Export Pool</button>
<button class="jungle-btn btn-sm" id="btn-import-trigger">📥 Load Pool File</button>
<input type="file" id="file-import-input" accept=".json" style="display: none;">
</div>
<button class="jungle-btn btn-sm" id="btn-clear-system" style="background:linear-gradient(to bottom, #d63031, #b2bec3); margin-top:15px;">⚠️ Hard Factory Reset Data</button>
</div>
<div class="settings-block" style="grid-column: span 2;">
<h3>Current Challenge Item Pool Database (<span id="pool-count-label">0</span> Questions loaded)</h3>
<div id="settings-questions-container" style="max-height: 250px; overflow-y: auto; margin-top:10px;">
</div>
</div>
</div>
</section>
</div>
<audio id="bg-audio" loop>
<source src="ES_Play Cabin - Andreas Ericson.mp3" type="audio/mpeg">
</audio>
<audio id="victory-audio">
<source src="winner sound effect.mp3" type="audio/mpeg">
</audio>
<script>
/************************************************************************
* STATE INITIALIZATION & CONSTANTS
************************************************************************/
const DEFAULT_QUESTIONS = [
{ id: 1, type: "sentence", tokens: ["She", "is", "happy."], answer: "She is happy.", image: "" },
{ id: 2, type: "spelling", tokens: ["T", "i", "g", "e", "r"], answer: "Tiger", image: "" },
{ id: 3, type: "sentence", tokens: ["The", "monkey", "climbs", "trees."], answer: "The monkey climbs trees.", image: "" },
{ id: 4, type: "spelling", tokens: ["J", "u", "n", "g", "l", "e"], answer: "Jungle", image: "" }
];
let questionPool = JSON.parse(localStorage.getItem("jungle_game_pool")) || DEFAULT_QUESTIONS;
let isTeamMode = false;
let syncQuestions = true;
let currentUploadedImageBase64 = "";
let state = {
left: { pool: [], index: 0, score: 0, currentChain: [], activeDrawing: false, lastMouseX: 0, lastMouseY: 0, confettiParticles: [], confettiActive: false },
right: { pool: [], index: 0, score: 0, currentChain: [], activeDrawing: false, lastMouseX: 0, lastMouseY: 0, confettiParticles: [], confettiActive: false }
};
const audioEl = document.getElementById('bg-audio');
const victoryAudioEl = document.getElementById('victory-audio');
const audioBtn = document.getElementById('btn-toggle-audio');
const volSlider = document.getElementById('vol-slider');
const AudioFX = {
ctx: null,
init() { if(!this.ctx) this.ctx = new (window.AudioContext || window.webkitAudioContext)(); },
playTone(freq, type, duration, startTimeOffset = 0, volumeMultiplier = 1.0) {
try {
this.init();
let osc = this.ctx.createOscillator();
let gain = this.ctx.createGain();
osc.type = type;
osc.frequency.value = freq;
// Boosted base sound effects to be distinctly heard over background music tracks
gain.gain.setValueAtTime(0.5 * volumeMultiplier, this.ctx.currentTime + startTimeOffset);
gain.gain.exponentialRampToValueAtTime(0.00001, this.ctx.currentTime + startTimeOffset + duration);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(this.ctx.currentTime + startTimeOffset);
osc.stop(this.ctx.currentTime + startTimeOffset + duration);
} catch(e) {}
},
success() {
this.playTone(523.25, 'sine', 0.18, 0, 1.2);
setTimeout(() => this.playTone(659.25, 'sine', 0.35, 0, 1.2), 100);
},
fail() {
this.playTone(220, 'sawtooth', 0.45, 0, 1.3);
},
victory() {
try {
// Trigger custom winner sound effect file at 100% full master volume
victoryAudioEl.currentTime = 0;
victoryAudioEl.volume = 1.0;
victoryAudioEl.play();
} catch(e) {
console.log("Audio playback error:", e);
}
}
};
/************************************************************************
* DOM LIFECYCLE ENGINE INITIALIZER
************************************************************************/
window.addEventListener('DOMContentLoaded', () => {
initDOMEvents();
loadRulesFromStorage();
rebuildGameSession();
renderSettingsUI();
setupPasteFeature();
window.addEventListener('resize', () => {
rebuildGameSession();
});
});
function loadRulesFromStorage() {
const savedSync = localStorage.getItem("jungle_game_sync");
if(savedSync !== null) {
syncQuestions = savedSync === 'true';
document.getElementById('sync-toggle').checked = syncQuestions;
}
}
function savePoolToStorage() {
try {
localStorage.setItem("jungle_game_pool", JSON.stringify(questionPool));
localStorage.setItem("jungle_game_sync", syncQuestions);
} catch(e) {
alert("Storage limit warning! Try using smaller/compressed pictures if updates fail to hold.");
}
renderSettingsUI();
}
function rebuildGameSession() {
if(questionPool.length === 0) {
questionPool = [...DEFAULT_QUESTIONS];
}
if (syncQuestions) {
state.left.pool = JSON.parse(JSON.stringify(questionPool));
state.right.pool = JSON.parse(JSON.stringify(questionPool));
} else {
state.left.pool = [...questionPool].sort(() => Math.random() - 0.5);
state.right.pool = [...questionPool].sort(() => Math.random() - 0.5);
}
state.left.index = 0;
state.left.score = 0;
state.right.index = 0;
state.right.score = 0;
stopConfettiAnimation('left');
stopConfettiAnimation('right');
document.getElementById('feedback-left').classList.remove('active');
document.getElementById('feedback-right').classList.remove('active');
applyLayoutUIModes();
}
function resetSingleTeamSession(side) {
const sideState = state[side];
if (syncQuestions) {
sideState.pool = JSON.parse(JSON.stringify(questionPool));
} else {
sideState.pool = [...questionPool].sort(() => Math.random() - 0.5);
}
sideState.index = 0;
sideState.score = 0;
stopConfettiAnimation(side);
document.getElementById(`feedback-${side}`).classList.remove('active');
setupChallengeRound(side);
}
function applyLayoutUIModes() {
const zoneRight = document.getElementById('zone-right');
const modeBadgeLeft = document.getElementById('mode-badge-left');
const titleText = document.getElementById('game-title-text');
if(isTeamMode) {
zoneRight.style.display = 'flex';
modeBadgeLeft.innerText = "Team A";
modeBadgeLeft.style.borderColor = "#fdcb6e";
modeBadgeLeft.style.color = "#fdcb6e";
titleText.innerText = "Jungle Connect: Team Faceoff";
} else {
zoneRight.style.display = 'none';
modeBadgeLeft.innerText = "Single Player Mode";
modeBadgeLeft.style.borderColor = "var(--gold)";
modeBadgeLeft.style.color = "var(--gold)";
titleText.innerText = "Jungle Connect Game";
}
setupChallengeRound('left');
if(isTeamMode) setupChallengeRound('right');
}
/************************************************************************
* ANTI-OVERLAP FORCE DISTRIBUTION & DETERMINISTIC PIXEL BUFFER GRID
************************************************************************/
function generateSpreadLayout(count, side) {
const container = document.getElementById(`canvas-container-${side}`);
const containerWidth = container.clientWidth || 600;
const containerHeight = container.clientHeight || 400;
const minDistancePixels = Math.max(140, containerWidth * 0.22);
let points = [];
let attempts = 0;
let success = false;
while (attempts < 150 && !success) {
points = [];
success = true;
for (let i = 0; i < count; i++) {
let p = {
x: 15 + Math.random() * 70,
y: 20 + Math.random() * 60
};
for (let j = 0; j < points.length; j++) {
let dx = (p.x - points[j].x) * (containerWidth / 100);
let dy = (p.y - points[j].y) * (containerHeight / 100);
let distance = Math.sqrt(dx * dx + dy * dy);
if (distance < minDistancePixels) {
success = false;
break;
}
}
if (!success) break;
points.push(p);
}
attempts++;
}
if (!success || points.length < count) {
points = [];
const cols = Math.ceil(Math.sqrt(count));
const rows = Math.ceil(count / cols);
let index = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (index >= count) break;
let gridX = 20 + (c * (60 / Math.max(1, cols - 1)));
let gridY = 25 + (r * (50 / Math.max(1, rows - 1)));
if (cols === 1) gridX = 50;
if (rows === 1) gridY = 50;
points.push({ x: gridX, y: gridY });
index++;
}
}
}
return points;
}
function setupChallengeRound(side) {
const sideState = state[side];
sideState.currentChain = [];
sideState.activeDrawing = false;
const container = document.getElementById(`canvas-container-${side}`);
container.querySelectorAll('.dot-node').forEach(n => n.remove());
document.getElementById(`ans-box-${side}`).innerText = "Connect dots in order!";
document.getElementById(`score-${side}`).innerText = `Score: ${sideState.score}`;
document.getElementById(`btn-feedback-next-${side}`).style.display = "inline-block";
document.getElementById(`btn-replay-${side}`).style.display = "none";
const clueHolder = document.getElementById(`clue-holder-${side}`);
clueHolder.innerHTML = "";
clueHolder.style.display = "none";
if(sideState.index >= sideState.pool.length) {
const teamLabelName = isTeamMode ? (side === 'left' ? "Team A" : "Team B") : "Single Player";
// Trigger Custom Victory MP3 Sound Effect + Confetti Celebration System
AudioFX.victory();
triggerConfettiExplosion(side);
document.getElementById(`feedback-text-${side}`).innerText = "🏆 Pool Completed!";
document.getElementById(`feedback-text-${side}`).className = "feedback-text feedback-success";
document.getElementById(`feedback-sub-${side}`).innerHTML = `<strong style="color:var(--gold); font-size:26px;">${teamLabelName} Score Card</strong><br><br>Final Points Earned: ${sideState.score} points!`;
document.getElementById(`btn-feedback-next-${side}`).style.display = "none";
document.getElementById(`btn-replay-${side}`).style.display = "inline-block";
document.getElementById(`feedback-${side}`).classList.add('active');
return;
}
const currentQuestion = sideState.pool[sideState.index];
if (currentQuestion.image) {
const imgNode = document.createElement('img');
imgNode.src = currentQuestion.image;
clueHolder.appendChild(imgNode);
clueHolder.style.display = "flex";
}
const rawTokens = currentQuestion.tokens;
const spreadCoordinates = generateSpreadLayout(rawTokens.length, side);
let tokenObjects = rawTokens.map((token, originalIndex) => {
return {
token: token,
id: originalIndex,
x: spreadCoordinates[originalIndex].x,
y: spreadCoordinates[originalIndex].y
};
});
let shuffledLayouts = [...tokenObjects].sort(() => Math.random() - 0.5);
shuffledLayouts.forEach((item) => {
const node = document.createElement('div');
node.className = 'dot-node';
node.id = `node-${side}-${item.id}`;
node.innerText = item.token;
node.style.left = `${item.x}%`;
node.style.top = `${item.y}%`;
node.dataset.itemId = item.id;
node.dataset.token = item.token;
node.addEventListener('mousedown', (e) => handleNodeSelectionStart(e, side, item, node));
node.addEventListener('touchstart', (e) => {
handleNodeSelectionStart(e, side, item, node);
}, {passive: true});
container.appendChild(node);
});
setTimeout(() => {
resizeCanvas(side);
drawCanvasVisuals(side);
}, 50);
}
/************************************************************************
* CONFETTI CELEBRATION PARTICLE ENGINE
************************************************************************/
function triggerConfettiExplosion(side) {
const canvas = document.getElementById(`confetti-${side}`);
const container = document.getElementById(`canvas-container-${side}`);
if(!canvas) return;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
const sideState = state[side];
sideState.confettiParticles = [];
sideState.confettiActive = true;
const colors = ['#ffb703', '#fb8500', '#52b788', '#2d6a4f', '#2ecc71', '#e74c3c', '#9b59b6', '#3498db'];
// Generate particles bursting upward from bottom center
for (let i = 0; i < 120; i++) {
sideState.confettiParticles.push({
x: canvas.width / 2,
y: canvas.height - 20,
size: Math.random() * 8 + 6,
color: colors[Math.floor(Math.random() * colors.length)],
speedX: (Math.random() - 0.5) * 15,
speedY: -(Math.random() * 12 + 10),
gravity: 0.3,
rotation: Math.random() * 360,
rotationSpeed: (Math.random() - 0.5) * 10
});
}
function updateConfettiFrame() {
if (!sideState.confettiActive) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
let activeParticles = false;
sideState.confettiParticles.forEach(p => {
p.x += p.speedX;
p.y += p.speedY;
p.speedY += p.gravity;
p.rotation += p.rotationSpeed;
if (p.y < canvas.height && p.x > 0 && p.x < canvas.width) {
activeParticles = true;
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation * Math.PI / 180);
ctx.fillStyle = p.color;
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
ctx.restore();
}
});
if (activeParticles) {
requestAnimationFrame(updateConfettiFrame);
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
requestAnimationFrame(updateConfettiFrame);
}
function stopConfettiAnimation(side) {
state[side].confettiActive = false;
const canvas = document.getElementById(`confetti-${side}`);
if (canvas) {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
/************************************************************************
* COPY-PASTE PICTURE OPTIMIZATION COMPRESSION PIPELINE
************************************************************************/
function setupPasteFeature() {
const pasteZone = document.getElementById('image-paste-zone');
pasteZone.addEventListener('paste', (e) => {
const clipboardItems = e.clipboardData.items;
for (let i = 0; i < clipboardItems.length; i++) {
if (clipboardItems[i].type.indexOf('image') !== -1) {
const blob = clipboardItems[i].getAsFile();
const reader = new FileReader();
reader.onload = function(event) {
compressImagePipeline(event.target.result, 200, (compressedBase64) => {
currentUploadedImageBase64 = compressedBase64;
pasteZone.innerHTML = `<img src="${compressedBase64}" alt="Clue thumbnail">`;
pasteZone.style.borderColor = "var(--gold)";
});
};
reader.readAsDataURL(blob);
e.preventDefault();
break;
}
}
});
}
function compressImagePipeline(base64Str, maxSize, callback) {
const img = new Image();
img.src = base64Str;
img.onload = function() {
const canvas = document.createElement('canvas');
let width = img.width;
let height = img.height;
if (width > height) {
if (width > maxSize) {
height *= maxSize / width;
width = maxSize;
}
} else {
if (height > maxSize) {
width *= maxSize / height;
height = maxSize;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
callback(canvas.toDataURL('image/jpeg', 0.7));
};
}
/************************************************************************
* CANVAS DIMENSION & DRAWING OPERATIONS (VINE GENERATOR GRAPHICS)
************************************************************************/
function resizeCanvas(side) {
const canvas = document.getElementById(`canvas-${side}`);
if(!canvas) return;
const container = document.getElementById(`canvas-container-${side}`);
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
const confCanvas = document.getElementById(`confetti-${side}`);
if(confCanvas) {
confCanvas.width = container.clientWidth;
confCanvas.height = container.clientHeight;
}
}
function drawCanvasVisuals(side) {
const canvas = document.getElementById(`canvas-${side}`);
if(!canvas) return;
const ctx = canvas.getContext('2d');
const container = document.getElementById(`canvas-container-${side}`);
ctx.clearRect(0, 0, canvas.width, canvas.height);
const sideState = state[side];
if(!sideState || sideState.currentChain.length === 0) return;
ctx.lineWidth = 8;
ctx.strokeStyle = '#2d6a4f';
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.shadowColor = 'rgba(0,0,0,0.4)';
ctx.shadowBlur = 4;
ctx.shadowOffsetY = 3;
ctx.beginPath();
sideState.currentChain.forEach((nodeId, index) => {
const element = document.getElementById(`node-${side}-${nodeId}`);
if(element) {
const rect = element.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const x = (rect.left + rect.width / 2) - containerRect.left;
const y = (rect.top + rect.height / 2) - containerRect.top;
if(index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
});
if(sideState.activeDrawing && sideState.currentChain.length > 0) {
ctx.lineTo(sideState.lastMouseX, sideState.lastMouseY);
}
ctx.stroke();
ctx.beginPath();
ctx.lineWidth = 3;
ctx.strokeStyle = '#52b788';
ctx.shadowBlur = 0;
ctx.shadowOffsetY = 0;
sideState.currentChain.forEach((nodeId, index) => {
const element = document.getElementById(`node-${side}-${nodeId}`);
if(element) {
const rect = element.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const x = (rect.left + rect.width / 2) - containerRect.left;
const y = (rect.top + rect.height / 2) - containerRect.top;
if(index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
});
if(sideState.activeDrawing && sideState.currentChain.length > 0) {
ctx.lineTo(sideState.lastMouseX, sideState.lastMouseY);
}
ctx.stroke();
}
/************************************************************************
* INTERACTIVE CONNECTIONS INTERFACES CONTROL AND TOUCH EVENTS DRIVERS
************************************************************************/
function handleNodeSelectionStart(e, side, item, node) {
e.stopPropagation();
const sideState = state[side];
if(sideState.currentChain.includes(item.id)) return;
sideState.currentChain.push(item.id);
node.classList.add('selected');
sideState.activeDrawing = true;
updateTrackingCoordinates(e, side);
updateAnswerPreviewDisplay(side);
drawCanvasVisuals(side);
}
function updateTrackingCoordinates(e, side) {
const container = document.getElementById(`canvas-container-${side}`);
const rect = container.getBoundingClientRect();
let clientX, clientY;
if(e.touches && e.touches.length > 0) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
state[side].lastMouseX = clientX - rect.left;
state[side].lastMouseY = clientY - rect.top;
}
function handlePointerTrackingMove(e, side) {
const sideState = state[side];
if(!sideState.activeDrawing) return;
updateTrackingCoordinates(e, side);
drawCanvasVisuals(side);
let clientX, clientY;
if(e.touches && e.touches.length > 0) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
const targetElement = document.elementFromPoint(clientX, clientY);
if(targetElement && targetElement.classList.contains('dot-node')) {
const targetSide = targetElement.id.split('-')[1];
const targetId = parseInt(targetElement.dataset.itemId);
if(targetSide === side && !sideState.currentChain.includes(targetId)) {
sideState.currentChain.push(targetId);
targetElement.classList.add('selected');
updateAnswerPreviewDisplay(side);
AudioFX.playTone(380, 'sine', 0.05, 0, 0.8);
}
}
}
function terminatePointerTracking(side) {
const sideState = state[side];
if(!sideState.activeDrawing) return;
sideState.activeDrawing = false;
drawCanvasVisuals(side);
}
function updateAnswerPreviewDisplay(side) {
const sideState = state[side];
const currentQuestion = sideState.pool[sideState.index];
if(!currentQuestion) return;
let outputTokens = sideState.currentChain.map(id => {
return currentQuestion.tokens[id];
});
const separator = (currentQuestion.type === 'sentence') ? ' ' : '';
const builtString = outputTokens.join(separator);
document.getElementById(`ans-box-${side}`).innerText = builtString || "Connect dots to answer!";
}
/************************************************************************
* GLOBAL INPUT EVENT PIPELINE ATTACHMENTS MANAGER
************************************************************************/
function initDOMEvents() {
document.getElementById('btn-close-welcome').addEventListener('click', () => {
document.getElementById('welcome-screen').classList.remove('active');
if(audioEl.paused) {
audioEl.volume = volSlider.value;
audioEl.play().then(() => {
audioBtn.innerText = "🎵 Music: ON";
audioBtn.style.background = "linear-gradient(to bottom, #2ecc71, #27ae60)";
}).catch(() => {});
}
});
['left', 'right'].forEach(side => {
const container = document.getElementById(`canvas-container-${side}`);
container.addEventListener('mousemove', (e) => handlePointerTrackingMove(e, side));
container.addEventListener('touchmove', (e) => handlePointerTrackingMove(e, side), {passive: true});
window.addEventListener('mouseup', () => terminatePointerTracking(side));
window.addEventListener('touchend', () => terminatePointerTracking(side));
document.getElementById(`btn-check-${side}`).addEventListener('click', () => evaluateCurrentSubmission(side));
document.getElementById(`btn-reset-${side}`).addEventListener('click', () => clearCurrentPaths(side));
document.getElementById(`btn-feedback-next-${side}`).addEventListener('click', () => {
document.getElementById(`feedback-${side}`).classList.remove('active');
setupChallengeRound(side);
});
document.getElementById(`btn-replay-${side}`).addEventListener('click', () => {
resetSingleTeamTeamSession(side);
});
});
document.getElementById('btn-toggle-mode').addEventListener('click', () => {
isTeamMode = !isTeamMode;
rebuildGameSession();
});
document.getElementById('btn-open-settings').addEventListener('click', () => {
document.getElementById('settings-panel').style.display = 'block';
});
document.getElementById('btn-close-settings').addEventListener('click', () => {
syncQuestions = document.getElementById('sync-toggle').checked;
savePoolToStorage();
document.getElementById('settings-panel').style.display = 'none';
rebuildGameSession();
});
document.getElementById('btn-add-question').addEventListener('click', executeAppendQuestionAction);
document.getElementById('btn-clear-system').addEventListener('click', executeSystemResetAction);
document.getElementById('btn-export-data').addEventListener('click', exportPoolDataJSON);
const fileInput = document.getElementById('file-import-input');
document.getElementById('btn-import-trigger').addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileImportEvent);
audioBtn.addEventListener('click', () => {
if(audioEl.paused) {
audioEl.play().then(() => {
audioBtn.innerText = "🎵 Music: ON";
audioBtn.style.background = "linear-gradient(to bottom, #2ecc71, #27ae60)";
}).catch(() => {
alert("Play interaction blocked by browser security. Tap anywhere on the stage window first.");
});
} else {
audioEl.pause();
audioBtn.innerText = "🎵 Music: OFF";
audioBtn.style.background = "linear-gradient(to bottom, var(--wood-brown), #5c3a21)";
}
});
volSlider.addEventListener('input', (e) => {
audioEl.volume = e.target.value;
});
}
window.resetSingleTeamTeamSession = function(side) {
resetSingleTeamSession(side);
};
/************************************************************************
* EVALUATION ENGINE & INTERACTIVE ANSWER CHECKER MECHANICS LOGIC
************************************************************************/
function clearCurrentPaths(side) {
const sideState = state[side];
sideState.currentChain = [];
sideState.activeDrawing = false;
const container = document.getElementById(`canvas-container-${side}`);
container.querySelectorAll('.dot-node').forEach(node => node.classList.remove('selected'));
updateAnswerPreviewDisplay(side);
drawCanvasVisuals(side);
}
function evaluateCurrentSubmission(side) {
const sideState = state[side];
const currentQuestion = sideState.pool[sideState.index];
if(!currentQuestion) return;
const separator = (currentQuestion.type === 'sentence') ? ' ' : '';
let userSubmission = sideState.currentChain.map(id => currentQuestion.tokens[id]).join(separator);
const sanitizedUser = userSubmission.trim().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"");
const sanitizedTarget = currentQuestion.answer.trim().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"");
const isCorrect = (sanitizedUser === sanitizedTarget) && (sideState.currentChain.length === currentQuestion.tokens.length);
const overlay = document.getElementById(`feedback-${side}`);
const textEl = document.getElementById(`feedback-text-${side}`);
const subEl = document.getElementById(`feedback-sub-${side}`);
const nextBtn = document.getElementById(`btn-feedback-next-${side}`);
if(isCorrect) {
AudioFX.success();
sideState.score++;
sideState.index++;
textEl.innerText = "🎉 WELL DONE!";
textEl.className = "feedback-text feedback-success";
subEl.innerHTML = `"${currentQuestion.answer}" is completely correct!<br><span style="color:var(--gold); font-weight:bold;">+1 Point Earned!</span>`;
nextBtn.innerText = "Continue Voyage";
} else {
AudioFX.fail();
sideState.score--;
if(sideState.score < 0) sideState.score = 0;
textEl.innerText = "❌ TRY AGAIN!";
textEl.className = "feedback-text feedback-fail";
subEl.innerHTML = `Expected Order: "${currentQuestion.answer}"<br><span style="color:#ff7675; font-weight:bold;">Incorrect submission check: -1 Point!</span>`;
nextBtn.innerText = "Retry Challenge";
clearCurrentPaths(side);
}
document.getElementById(`score-${side}`).innerText = `Score: ${sideState.score}`;
overlay.classList.add('active');
}
/************************************************************************
* TEACHER SETTINGS MODAL CONTROL SYSTEM OPERATIONS MANAGEMENT
************************************************************************/
function renderSettingsUI() {
const container = document.getElementById('settings-questions-container');
container.innerHTML = '';
document.getElementById('pool-count-label').innerText = questionPool.length;
questionPool.forEach((q, idx) => {
const row = document.createElement('div');
row.className = 'question-list-item';
let imgThumbHtml = q.image ? `<img src="${q.image}" style="max-height:35px; border-radius:4px; margin-right:8px; vertical-align:middle;">` : '';
row.innerHTML = `
<div>
${imgThumbHtml}
<strong>[${q.type.toUpperCase()}]</strong> ${q.answer}
<br><small style="color:#2ecc71;">Tokens: ${JSON.stringify(q.tokens)}</small>
</div>
<button class="jungle-btn btn-sm" style="background:#e74c3c; border-color:#c0392b; box-shadow:0 2px 0 #c0392b;" onclick="deleteQuestionItemByIndex(${idx})">Delete</button>
`;
container.appendChild(row);
});
}
function executeAppendQuestionAction() {
const type = document.getElementById('input-type').value;
let rawContent = document.getElementById('input-content').value;
rawContent = rawContent.replace(/\s+/g, ' ').trim();
if(!rawContent) {
alert("Please input context parameters to append!");
return;
}
let tokens = [];
let answer = rawContent;
if(type === 'sentence') {
tokens = rawContent.split(' ').filter(t => t.length > 0);
} else {
const cleanWord = rawContent.replace(/\s+/g, '');
tokens = cleanWord.split('');
answer = cleanWord;
}
if(tokens.length === 0) {
alert("Failed parsing text content into single array processing components tokens!");
return;
}
const newQuestion = {
id: Date.now() + Math.random(),
type: type,
tokens: tokens,
answer: answer,
image: currentUploadedImageBase64
};
questionPool.push(newQuestion);
document.getElementById('input-content').value = '';
const pasteZone = document.getElementById('image-paste-zone');
pasteZone.innerHTML = "Click here & paste your photo (Ctrl+V)";
pasteZone.style.borderColor = "var(--light-green)";
currentUploadedImageBase64 = "";
savePoolToStorage();
}
window.deleteQuestionItemByIndex = function(index) {
questionPool.splice(index, 1);
savePoolToStorage();
};
function executeSystemResetAction() {
if(confirm("Confirm structural purge? This resets configuration settings parameters to factory profiles definitions!")) {
localStorage.removeItem("jungle_game_pool");
questionPool = [...DEFAULT_QUESTIONS];
savePoolToStorage();
rebuildGameSession();
}
}
function exportPoolDataJSON() {
const outputString = JSON.stringify(questionPool, null, 2);
const dataBlob = new Blob([outputString], { type: 'application/json' });
const temporaryAnchor = document.createElement('a');
temporaryAnchor.href = URL.createObjectURL(dataBlob);
temporaryAnchor.download = `jungle_game_pool_${Date.now()}.json`;
temporaryAnchor.click();
URL.revokeObjectURL(temporaryAnchor.href);
}
function handleFileImportEvent(e) {
const TargetFile = e.target.files[0];
if(!TargetFile) return;
const readerInstance = new FileReader();
readerInstance.onload = function(event) {
try {
const compiledData = JSON.parse(event.target.result);
if(Array.isArray(compiledData)) {
questionPool = compiledData;
savePoolToStorage();
alert("Configuration pool data structure parsed and synchronized successfully!");
} else {
alert("Malformed data model pattern structure! JSON profile requires top level vector arrays mapping signatures.");
}
} catch(error) {
alert("Failure parsing validation target matrix records: " + error.message);
}
};
readerInstance.readAsText(TargetFile);
}
</script>
</body>
</html>
Interactive educational games make learning more engaging and meaningful for students. This Jungle Connect-the-Dots Game transforms reading and spelling activities into a fun classroom adventure.
Teachers can customize lessons, create team competitions, and reuse activity databases for future classes.
🌴 Happy Teaching and Learning! 🎮📚

0 Comments