| 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) |