LGssGame / app.py
Translsis's picture
Update app.py
7b0637f verified
from flask import Flask, render_template_string, jsonify, request
from huggingface_hub import InferenceClient
import os
import random
app = Flask(__name__)
# Lấy token từ environment variable
HF_TOKEN = os.environ.get("HF_TOKEN", "")
# HTML Template
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>English Shooting Game</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.game-container {
width: 100%;
max-width: 1000px;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 30px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #667eea;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
}
.score-board {
display: flex;
justify-content: space-around;
margin-bottom: 30px;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
color: white;
}
.score-item {
text-align: center;
}
.score-item .label {
font-size: 0.9em;
opacity: 0.9;
}
.score-item .value {
font-size: 2em;
font-weight: bold;
margin-top: 5px;
}
.game-area {
position: relative;
height: 400px;
background: linear-gradient(to bottom, #87CEEB 0%, #98D8C8 100%);
border-radius: 15px;
overflow: hidden;
margin-bottom: 20px;
border: 3px solid #667eea;
}
.shooter {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
font-size: 60px;
transition: transform 0.1s;
}
.target {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
font-size: 80px;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateX(-50%) translateY(0px); }
50% { transform: translateX(-50%) translateY(-20px); }
}
.bullet {
position: absolute;
width: 8px;
height: 20px;
background: linear-gradient(to top, #ff6b6b, #feca57);
border-radius: 4px;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
box-shadow: 0 0 10px rgba(255, 107, 107, 0.8);
}
.question-section {
background: #f8f9fa;
padding: 25px;
border-radius: 15px;
margin-bottom: 20px;
border: 2px solid #e9ecef;
}
.question-text {
font-size: 1.3em;
color: #333;
margin-bottom: 20px;
line-height: 1.6;
}
.answer-input {
width: 100%;
padding: 15px;
font-size: 1.2em;
border: 2px solid #ddd;
border-radius: 10px;
transition: all 0.3s;
margin-bottom: 15px;
}
.answer-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.answer-input.correct {
border-color: #51cf66;
background: #d3f9d8;
}
.answer-input.incorrect {
border-color: #ff6b6b;
background: #ffe0e0;
}
.button-group {
display: flex;
gap: 15px;
justify-content: center;
}
.btn {
padding: 15px 40px;
font-size: 1.1em;
border: none;
border-radius: 10px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.btn-shoot {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-shoot:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.btn-shoot:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.btn-next {
background: linear-gradient(135deg, #feca57 0%, #ff9ff3 100%);
color: white;
}
.btn-next:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(254, 202, 87, 0.4);
}
.feedback {
text-align: center;
margin-top: 15px;
font-size: 1.2em;
font-weight: bold;
min-height: 30px;
}
.feedback.correct {
color: #51cf66;
}
.feedback.incorrect {
color: #ff6b6b;
}
.loading {
text-align: center;
padding: 20px;
font-size: 1.2em;
color: #667eea;
}
.explosion {
position: absolute;
font-size: 100px;
animation: explode 0.5s ease-out;
}
@keyframes explode {
0% {
transform: scale(0);
opacity: 1;
}
100% {
transform: scale(2);
opacity: 0;
}
}
@keyframes shoot {
0% {
bottom: 80px;
opacity: 1;
}
100% {
bottom: 400px;
opacity: 0;
}
}
@keyframes recoil {
0%, 100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(10px); }
}
</style>
</head>
<body>
<div class="game-container">
<div class="header">
<h1>🎯 English Shooting Game</h1>
</div>
<div class="score-board">
<div class="score-item">
<div class="label">Điểm</div>
<div class="value" id="score">0</div>
</div>
<div class="score-item">
<div class="label">Đúng</div>
<div class="value" id="correct">0</div>
</div>
<div class="score-item">
<div class="label">Sai</div>
<div class="value" id="incorrect">0</div>
</div>
</div>
<div class="game-area" id="gameArea">
<div class="shooter">🔫</div>
<div class="target">🎯</div>
</div>
<div class="question-section" id="questionSection">
<div class="loading">Đang tải câu hỏi...</div>
</div>
<div class="feedback" id="feedback"></div>
</div>
<script>
let score = 0;
let correctCount = 0;
let incorrectCount = 0;
let currentAnswer = '';
let isAnswered = false;
async function generateQuestion() {
const questionSection = document.getElementById('questionSection');
questionSection.innerHTML = '<div class="loading">Đang tải câu hỏi...</div>';
document.getElementById('feedback').textContent = '';
isAnswered = false;
try {
const response = await fetch('/api/question');
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
currentAnswer = data.answer.toLowerCase();
questionSection.innerHTML = `
<div class="question-text">${data.sentence}</div>
<input type="text" class="answer-input" id="answerInput" placeholder="Nhập từ vào đây...">
<div class="button-group">
<button class="btn btn-shoot" onclick="checkAndShoot()">🔫 Bắn!</button>
<button class="btn btn-next" onclick="generateQuestion()">⏭️ Câu mới</button>
</div>
`;
document.getElementById('answerInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') checkAndShoot();
});
document.getElementById('answerInput').focus();
} catch (error) {
console.error('Error:', error);
questionSection.innerHTML = '<div class="loading">❌ Lỗi tải câu hỏi. Vui lòng thử lại!</div>';
setTimeout(generateQuestion, 2000);
}
}
function checkAndShoot() {
if (isAnswered) return;
const input = document.getElementById('answerInput');
const userAnswer = input.value.trim().toLowerCase();
const feedback = document.getElementById('feedback');
if (!userAnswer) {
feedback.textContent = '⚠️ Vui lòng nhập câu trả lời!';
feedback.className = 'feedback';
return;
}
isAnswered = true;
const isCorrect = userAnswer === currentAnswer;
if (isCorrect) {
input.className = 'answer-input correct';
feedback.textContent = '✅ Chính xác! Bắn đạn!';
feedback.className = 'feedback correct';
score += 10;
correctCount++;
shootBullet(true);
} else {
input.className = 'answer-input incorrect';
feedback.textContent = `❌ Sai rồi! Đáp án đúng là: "${currentAnswer}"`;
feedback.className = 'feedback incorrect';
incorrectCount++;
shootBullet(false);
}
updateScoreboard();
input.disabled = true;
}
function shootBullet(hit) {
const gameArea = document.getElementById('gameArea');
const shooter = gameArea.querySelector('.shooter');
shooter.style.animation = 'recoil 0.3s';
setTimeout(() => {
shooter.style.animation = '';
}, 300);
if (hit) {
const bullet = document.createElement('div');
bullet.className = 'bullet';
gameArea.appendChild(bullet);
bullet.style.animation = 'shoot 0.8s ease-out';
setTimeout(() => {
const target = gameArea.querySelector('.target');
const explosion = document.createElement('div');
explosion.className = 'explosion';
explosion.textContent = '💥';
explosion.style.left = target.offsetLeft + 'px';
explosion.style.top = target.offsetTop + 'px';
gameArea.appendChild(explosion);
setTimeout(() => {
explosion.remove();
}, 500);
bullet.remove();
}, 800);
}
}
function updateScoreboard() {
document.getElementById('score').textContent = score;
document.getElementById('correct').textContent = correctCount;
document.getElementById('incorrect').textContent = incorrectCount;
}
generateQuestion();
</script>
</body>
</html>
"""
# Câu hỏi dự phòng
FALLBACK_QUESTIONS = [
{"sentence": "I ___ to school every day.", "answer": "go"},
{"sentence": "She ___ a book yesterday.", "answer": "read"},
{"sentence": "They are ___ soccer now.", "answer": "playing"},
{"sentence": "He ___ his homework last night.", "answer": "did"},
{"sentence": "We ___ going to the park tomorrow.", "answer": "are"},
{"sentence": "The cat ___ on the table.", "answer": "is"},
{"sentence": "I have ___ this movie before.", "answer": "seen"},
{"sentence": "She can ___ English very well.", "answer": "speak"},
{"sentence": "They ___ to Paris last year.", "answer": "went"},
{"sentence": "He ___ pizza for dinner.", "answer": "likes"},
]
@app.route('/')
def home():
return render_template_string(HTML_TEMPLATE)
@app.route('/api/question')
def get_question():
try:
# Thử sử dụng AI để tạo câu hỏi
if HF_TOKEN:
client = InferenceClient(token=HF_TOKEN)
prompt = """Create one English fill-in-the-blank question. Format:
SENTENCE: [sentence with ONE ___ for blank]
ANSWER: [correct word]
Example:
SENTENCE: I ___ to school every day.
ANSWER: go
Create a new question:"""
response = client.text_generation(
prompt,
model="mistralai/Mixtral-8x7B-Instruct-v0.1",
max_new_tokens=100,
temperature=0.9
)
# Parse response
lines = response.strip().split('\n')
sentence = ""
answer = ""
for line in lines:
if line.startswith('SENTENCE:'):
sentence = line.replace('SENTENCE:', '').strip()
elif line.startswith('ANSWER:'):
answer = line.replace('ANSWER:', '').strip()
if sentence and answer and '___' in sentence:
return jsonify({
'sentence': sentence,
'answer': answer
})
except Exception as e:
print(f"AI generation failed: {e}")
# Fallback: sử dụng câu hỏi có sẵn
question = random.choice(FALLBACK_QUESTIONS)
return jsonify(question)
if __name__ == '__main__':
port = int(os.environ.get('PORT', 7860))
app.run(host='0.0.0.0', port=port, debug=False)