Initial commit: Flask quiz game 'Уроки французского'

This commit is contained in:
eka
2026-03-01 08:36:02 +05:00
commit 9a9c58afc0
9 changed files with 1769 additions and 0 deletions

931
templates/index.html Normal file
View File

@@ -0,0 +1,931 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Уроки французского - Своя игра</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Source+Sans+Pro:wght@400;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--surface: #0f3460;
--primary: #e94560;
--text: #eaeaea;
--gold: #ffd700;
--success: #4ade80;
--error: #f87171;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Source Sans Pro', sans-serif;
background: var(--bg-primary);
color: var(--text);
min-height: 100vh;
background-image:
radial-gradient(ellipse at top, #1a1a2e 0%, #0f0f1a 100%),
repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(233, 69, 96, 0.03) 10px, rgba(233, 69, 96, 0.03) 20px);
}
header {
text-align: center;
padding: 2rem 1rem;
background: linear-gradient(180deg, var(--bg-secondary) 0%, transparent 100%);
}
h1 {
font-family: 'Playfair Display', serif;
font-size: 2.5rem;
color: var(--primary);
text-shadow: 0 0 30px rgba(233, 69, 96, 0.5);
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 1.1rem;
color: rgba(234, 234, 234, 0.7);
font-style: italic;
}
.score-panel {
display: flex;
justify-content: center;
gap: 2rem;
margin: 1.5rem 0;
flex-wrap: wrap;
}
.score-item {
background: var(--surface);
padding: 0.75rem 2rem;
border-radius: 50px;
font-size: 1.2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(233, 69, 96, 0.3);
}
.score-value {
color: var(--gold);
font-weight: 600;
margin-left: 0.5rem;
}
.restart-btn {
background: var(--primary);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 25px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s ease;
}
.restart-btn:hover {
background: #c73e54;
transform: scale(1.05);
}
.team-setup {
text-align: center;
margin: 1rem 0;
}
.team-setup select {
background: var(--surface);
color: var(--text);
padding: 0.5rem;
border: 1px solid var(--primary);
border-radius: 4px;
margin-left: 0.5rem;
}
.team-setup select option {
background: var(--surface);
}
.team-names {
margin: 1rem 0;
}
.team-names input {
margin: 1rem 0;
}
.team-names input {
background: var(--surface);
color: var(--text);
padding: 0.5rem;
border: 1px solid var(--primary);
border-radius: 4px;
margin: 0 0.5rem;
width: 120px;
}
.start-btn {
background: var(--success);
color: var(--bg-primary);
border: none;
padding: 0.5rem 1.5rem;
border-radius: 25px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
margin-left: 1rem;
}
.start-btn:hover {
transform: scale(1.05);
}
.score-panel {
display: flex;
justify-content: center;
gap: 1rem;
margin: 1.5rem 0;
flex-wrap: wrap;
}
.score-item {
background: var(--surface);
padding: 0.75rem 1.5rem;
border-radius: 50px;
font-size: 1.2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(233, 69, 96, 0.3);
display: flex;
align-items: center;
gap: 0.5rem;
}
.team-active {
border-color: var(--gold);
box-shadow: 0 0 15px rgba(255, 215, 0, 0.5);
}
.team-selector {
display: flex;
gap: 0.5rem;
justify-content: center;
margin-bottom: 1rem;
}
.team-btn {
padding: 0.5rem 1rem;
border-radius: 20px;
border: 2px solid var(--primary);
background: var(--surface);
color: var(--text);
cursor: pointer;
font-weight: 600;
}
.team-btn.active {
background: var(--primary);
color: white;
}
.team-btn:hover {
background: var(--bg-secondary);
}
.game-container {
max-width: 1400px;
margin: 0 auto;
padding: 1rem;
overflow-x: auto;
}
.game-board {
display: grid;
grid-template-columns: repeat(5, 1fr);
column-gap: 0.5rem;
row-gap: 0.5rem;
max-width: 1000px;
margin: 0 auto;
}
.category-header {
background: linear-gradient(135deg, var(--primary) 0%, #c73e54 100%);
color: white;
padding: 0.5rem;
text-align: center;
font-family: 'Playfair Display', serif;
font-weight: 700;
font-size: 1rem;
border-radius: 8px 8px 0 0;
min-height: 80px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 15px rgba(233, 69, 96, 0.4);
}
.question-cell {
background: var(--surface);
aspect-ratio: 1.4;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: 600;
color: var(--gold);
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid rgba(233, 69, 96, 0.2);
border-radius: 4px;
min-height: 50px;
padding: 0.25rem;
}
.question-cell:hover:not(.answered) {
transform: scale(1.05);
background: var(--bg-secondary);
box-shadow: 0 0 25px rgba(233, 69, 96, 0.4);
border-color: var(--primary);
}
.question-cell.answered {
background: rgba(15, 52, 96, 0.5);
color: rgba(234, 234, 234, 0.3);
cursor: not-allowed;
border-color: transparent;
}
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
z-index: 1000;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s ease;
}
.modal-overlay.active {
display: flex;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal {
background: linear-gradient(145deg, var(--bg-secondary) 0%, var(--surface) 100%);
border-radius: 20px;
padding: 2.5rem;
max-width: 600px;
width: 90%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 40px rgba(233, 69, 96, 0.2);
border: 1px solid rgba(233, 69, 96, 0.3);
animation: slideUp 0.4s ease;
}
@keyframes slideUp {
from { transform: translateY(30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal h2 {
font-family: 'Playfair Display', serif;
color: var(--primary);
margin-bottom: 1.5rem;
font-size: 1.5rem;
}
.modal-cost {
position: absolute;
top: -15px;
right: 20px;
background: var(--gold);
color: var(--bg-primary);
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 700;
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.4);
}
.modal-body {
position: relative;
}
.question-text {
font-size: 1.2rem;
margin-bottom: 2rem;
line-height: 1.6;
}
.options {
display: grid;
gap: 1rem;
}
.option {
background: var(--bg-primary);
padding: 1rem 1.5rem;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
font-size: 1rem;
}
.option:hover {
border-color: var(--primary);
transform: translateX(5px);
}
.option.correct {
background: var(--success);
color: var(--bg-primary);
border-color: var(--success);
}
.option.wrong {
background: var(--error);
color: var(--bg-primary);
border-color: var(--error);
}
.result-message {
text-align: center;
padding: 1.5rem;
border-radius: 10px;
margin-top: 1.5rem;
font-weight: 600;
display: none;
}
.result-message.show {
display: block;
animation: popIn 0.3s ease;
}
.result-message.success {
background: rgba(74, 222, 128, 0.2);
color: var(--success);
border: 1px solid var(--success);
}
.result-message.error {
background: rgba(248, 113, 113, 0.2);
color: var(--error);
border: 1px solid var(--error);
}
@keyframes popIn {
from { transform: scale(0.8); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.close-btn {
display: none;
margin: 1.5rem auto 0;
padding: 0.75rem 2rem;
background: var(--primary);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 1rem;
transition: all 0.3s ease;
}
.close-btn.show {
display: block;
}
.close-btn:hover {
transform: scale(1.05);
box-shadow: 0 4px 20px rgba(233, 69, 96, 0.4);
}
.final-screen {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-primary);
z-index: 2000;
justify-content: center;
align-items: center;
flex-direction: column;
}
.final-screen.active {
display: flex;
animation: fadeIn 0.5s ease;
}
.final-screen h2 {
font-family: 'Playfair Display', serif;
font-size: 3rem;
color: var(--gold);
margin-bottom: 1rem;
text-shadow: 0 0 30px rgba(255, 215, 0, 0.5);
}
.podium {
display: flex;
justify-content: center;
align-items: flex-end;
gap: 20px;
margin: 2rem 0;
min-height: 300px;
}
.podium-place {
display: flex;
flex-direction: column;
align-items: center;
width: 150px;
}
.podium-team {
background: var(--surface);
padding: 1rem;
border-radius: 10px;
text-align: center;
width: 100%;
border: 2px solid var(--primary);
}
.podium-team-name {
font-size: 1rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.podium-team-score {
font-size: 1.5rem;
color: var(--gold);
}
.podium-block {
background: linear-gradient(180deg, var(--primary) 0%, #c73e54 100%);
width: 100%;
border-radius: 8px 8px 0 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
font-weight: bold;
color: white;
}
.podium-1 .podium-block {
height: 150px;
background: linear-gradient(180deg, #ffd700 0%, #daa520 100%);
}
.podium-2 .podium-block {
height: 100px;
background: linear-gradient(180deg, #c0c0c0 0%, #a8a8a8 100%);
}
.podium-3 .podium-block {
height: 70px;
background: linear-gradient(180deg, #cd7f32 0%, #b87333 100%);
}
.podium-1 { order: 2; }
.podium-2 { order: 1; }
.podium-3 { order: 3; }
.all-teams {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
max-width: 400px;
}
.team-result {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--surface);
padding: 15px 20px;
border-radius: 10px;
border: 2px solid var(--primary);
}
.team-place {
font-size: 1.5rem;
width: 40px;
}
.team-name {
flex: 1;
font-size: 1.2rem;
font-weight: bold;
text-align: center;
}
.team-points {
font-size: 1.2rem;
color: var(--gold);
font-weight: bold;
}
.-gradient(180deg, var(--primary) 0%, #c73e54 100%);
width: 100%;
border-radius: 8px 8px 0 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
font-weight: bold;
color: white;
}
.podium-1 .podium-block {
height: 150px;
background: linear-gradient(180deg, #ffd700 0%, #daa520 100%);
}
.podium-2 .podium-block {
height: 100px;
background: linear-gradient(180deg, #c0c0c0 0%, #a8a8a8 100%);
}
.podium-3 .podium-block {
height: 70px;
background: linear-gradient(180deg, #cd7f32 0%, #b87333 100%);
}
.podium-1 .podium-block {
order: 2;
}
.podium-2 .podium-block {
order: 1;
}
.podium-3 .podium-block {
order: 3;
}
.play-again-btn {
padding: 1rem 3rem;
background: var(--primary);
color: white;
border: none;
border-radius: 50px;
font-size: 1.2rem;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 2rem;
}
.play-again-btn:hover {
transform: scale(1.1);
box-shadow: 0 10px 30px rgba(233, 69, 96, 0.5);
}
@media (max-width: 768px) {
h1 { font-size: 1.8rem; }
.category-header { font-size: 0.85rem; min-height: 60px; padding: 0.5rem; }
.question-cell { font-size: 1.1rem; min-height: 50px; }
.modal { padding: 1.5rem; }
.question-text { font-size: 1rem; }
}
</style>
</head>
<body>
<header>
<h1>Своя игра</h1>
<p class="subtitle">Рассказ В.Г. Распутина «Уроки французского»</p>
<div class="team-setup" id="teamSetup">
<label>Количество команд:
<select id="teamCount" onchange="setupTeams()">
<option value="1">1 команда</option>
<option value="2">2 команды</option>
<option value="3">3 команды</option>
</select>
</label>
<div id="teamNames"></div>
<button class="start-btn" id="startBtn" onclick="startGame()">Начать игру</button>
</div>
<div class="score-panel" id="scorePanel">
<div class="team-selector" id="teamSelector"></div>
<div class="score-item">Вопросов: <span class="score-value" id="answered">0</span>/<span id="totalQuestions">30</span></div>
<button class="restart-btn" onclick="if(confirm('Вы точно хотите начать заново?')) restartGame()">Начать заново</button>
</div>
</header>
<div class="game-container">
<div class="game-board" id="gameBoard">
{% for cat_idx, category in enumerate(questions.categories) %}
<div>
<div class="category-header">{{ category.name }}</div>
{% for q_idx, q in enumerate(category.questions) %}
<div class="question-cell"
data-cat="{{ cat_idx }}"
data-q="{{ q_idx }}">
{{ q.cost }}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
<div class="modal-overlay" id="modalOverlay">
<div class="modal">
<div class="modal-body">
<span class="modal-cost" id="modalCost">100</span>
<h2>Вопрос</h2>
<p class="question-text" id="questionText"></p>
<div class="options" id="options"></div>
<div class="result-message" id="resultMessage"></div>
<button class="close-btn" id="closeBtn">Продолжить</button>
</div>
</div>
</div>
<div class="final-screen" id="finalScreen">
<h2>Игра окончена!</h2>
<div class="podium" id="podium"></div>
<button class="play-again-btn" onclick="location.reload()">Играть снова</button>
</div>
<script>
var allQuestions = {{ questions | tojson }};
var STORAGE_KEY = 'francuzskiy_game';
var currentTeam = 0;
var teams = [];
setupTeams();
function setupTeams() {
var count = parseInt(document.getElementById('teamCount').value);
var container = document.getElementById('teamNames');
container.innerHTML = '';
for (var i = 0; i < count; i++) {
var input = document.createElement('input');
input.placeholder = 'Команда ' + (i + 1);
input.value = 'Команда ' + (i + 1);
input.id = 'teamName' + i;
container.appendChild(input);
}
}
function startGame() {
document.getElementById('startBtn').style.display = 'none';
var count = parseInt(document.getElementById('teamCount').value);
teams = [];
for (var i = 0; i < count; i++) {
teams.push({
name: document.getElementById('teamName' + i).value || 'Команда ' + (i + 1),
score: 0
});
}
var totalQuestions = 30;
document.getElementById('totalQuestions').textContent = totalQuestions;
currentTeam = 0;
updateScoreDisplay();
initGame();
}
function selectTeam(index) {
currentTeam = index;
var btns = document.querySelectorAll('.team-btn');
for (var i = 0; i < btns.length; i++) {
btns[i].classList.toggle('active', i === index);
}
}
function updateScoreDisplay() {
var container = document.getElementById('teamSelector');
var html = '';
for (var i = 0; i < teams.length; i++) {
html += '<button class="team-btn' + (i === currentTeam ? ' active' : '') + '" onclick="selectTeam(' + i + ')">' + teams[i].name + ': <span id="score' + i + '">' + teams[i].score + '</span></button>';
}
container.innerHTML = html;
}
function loadGameState() {
var saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
return JSON.parse(saved);
}
return { score: 0, answered: [], userAnswers: {} };
}
function saveGameState(state) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
function initGame() {
const state = loadGameState();
let answeredCount = 0;
state.answered.forEach(function(key) {
const [cat, q] = key.split('_');
const cell = document.querySelector('.question-cell[data-cat="' + cat + '"][data-q="' + q + '"]');
if (cell) {
cell.classList.add('answered');
answeredCount++;
}
});
document.getElementById('answered').textContent = answeredCount;
}
const modalOverlay = document.getElementById('modalOverlay');
const questionText = document.getElementById('questionText');
const optionsContainer = document.getElementById('options');
const modalCost = document.getElementById('modalCost');
const resultMessage = document.getElementById('resultMessage');
const answeredEl = document.getElementById('answered');
const finalScreen = document.getElementById('finalScreen');
const finalScore = document.getElementById('finalScore');
let currentCat = 0;
let currentQ = 0;
let correctAnswer = 0;
let currentCost = 0;
let canAnswer = true;
initGame();
var cells = document.querySelectorAll('.question-cell');
for (var i = 0; i < cells.length; i++) {
cells[i].onclick = function() {
var cat = parseInt(this.dataset.cat, 10);
var q = parseInt(this.dataset.q, 10);
var questionData = allQuestions.categories[cat].questions[q];
var isAnswered = this.classList.contains('answered');
openQuestionModal(cat, q, questionData.question, questionData.options, questionData.answer, questionData.cost, this, isAnswered);
};
}
function openQuestionModal(catIndex, qIndex, question, options, answer, cost, cell, viewOnly) {
currentCat = catIndex;
currentQ = qIndex;
correctAnswer = answer;
currentCost = cost;
questionText.textContent = question;
modalCost.textContent = cost;
optionsContainer.innerHTML = '';
var userAnswerData = null;
if (viewOnly) {
var state = loadGameState();
var key = catIndex + '_' + qIndex;
userAnswerData = state.userAnswers[key];
}
options.forEach(function(opt, i) {
var btn = document.createElement('div');
btn.className = 'option';
btn.textContent = opt;
if (viewOnly) {
btn.style.pointerEvents = 'none';
if (i === answer) {
btn.classList.add('correct');
}
if (userAnswerData && i === userAnswerData.answer && i !== answer) {
btn.classList.add('wrong');
}
} else {
btn.onclick = function() { selectAnswer(i, cell); };
}
optionsContainer.appendChild(btn);
});
resultMessage.className = 'result-message';
resultMessage.textContent = '';
if (viewOnly) {
resultMessage.textContent = 'Этот вопрос уже отыгран';
resultMessage.className = 'result-message show';
}
var closeBtnEl = document.getElementById('closeBtn');
closeBtnEl.className = 'close-btn show';
canAnswer = !viewOnly;
modalOverlay.classList.add('active');
}
function selectAnswer(index, cell) {
if (!canAnswer) return;
canAnswer = false;
const options = document.querySelectorAll('.option');
options.forEach((opt, i) => {
opt.style.pointerEvents = 'none';
if (i === correctAnswer) opt.classList.add('correct');
});
const isCorrect = index === correctAnswer;
if (!isCorrect) {
options[index].classList.add('wrong');
}
const state = loadGameState();
const key = `${currentCat}_${currentQ}`;
if (!state.answered.includes(key)) {
state.answered.push(key);
}
state.userAnswers[key] = { team: currentTeam, answer: index };
if (isCorrect) {
teams[currentTeam].score += currentCost;
resultMessage.textContent = 'Правильно! ' + teams[currentTeam].name + ' получает +' + currentCost + ' очков';
} else {
teams[currentTeam].score -= currentCost;
resultMessage.textContent = 'Неправильно! ' + teams[currentTeam].name + ' теряет -' + currentCost + ' очков';
}
saveGameState(state);
updateScoreDisplay();
answeredEl.textContent = state.answered.length;
if (teams.length > 1) {
currentTeam = (currentTeam + 1) % teams.length;
selectTeam(currentTeam);
}
resultMessage.className = 'result-message show ' + (isCorrect ? 'success' : 'error');
cell.classList.add('answered');
var closeBtnEl = document.getElementById('closeBtn');
closeBtnEl.classList.add('show');
setTimeout(function() { checkGameEnd(); }, 500);
}
document.getElementById('closeBtn').onclick = function() {
modalOverlay.classList.remove('active');
};
function checkGameEnd() {
var totalQuestions = parseInt(document.getElementById('totalQuestions').textContent);
var state = loadGameState();
if (state.answered.length >= totalQuestions) {
var sortedTeams = teams.slice().sort(function(a, b) { return b.score - a.score; });
var podiumHtml = '<div class="all-teams">';
for (var i = 0; i < sortedTeams.length; i++) {
var place = i + 1;
var medal = '';
if (place === 1) medal = '🥇';
else if (place === 2) medal = '🥈';
else if (place === 3) medal = '🥉';
else medal = place + '.';
podiumHtml += '<div class="team-result">';
podiumHtml += '<span class="team-place">' + medal + '</span>';
podiumHtml += '<span class="team-name">' + sortedTeams[i].name + '</span>';
podiumHtml += '<span class="team-points">' + sortedTeams[i].score + ' очков</span>';
podiumHtml += '</div>';
}
podiumHtml += '</div>';
document.getElementById('podium').innerHTML = podiumHtml;
setTimeout(function() {
finalScreen.classList.add('active');
}, 500);
}
}
document.querySelector('.play-again-btn').onclick = function() {
localStorage.removeItem(STORAGE_KEY);
location.reload();
};
function restartGame() {
localStorage.removeItem(STORAGE_KEY);
location.reload();
}
</script>
</body>
</html>