| | from flask import Flask, render_template_string, jsonify, request |
| | from huggingface_hub import InferenceClient |
| | import os |
| | import random |
| |
|
| | app = Flask(__name__) |
| |
|
| | |
| | HF_TOKEN = os.environ.get("HF_TOKEN", "") |
| |
|
| | |
| | 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> |
| | """ |
| |
|
| | |
| | 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: |
| | |
| | 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 |
| | ) |
| |
|
| | |
| | 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}") |
| | |
| | |
| | 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) |