The Challenge of Moving from CLI to Web MVC

When transitioning a console-based game (or application) to a web-based framework like Flask, developers often run into architectural roadblocks. In a CLI application, you control the execution flow sequentially: you ask for input, wait, process it, and output the result. However, web frameworks operate on an event-driven, request-response lifecycle. This paradigm shift is the root cause of the cyclic dependencies and RuntimeError: Working outside of request context errors in your Flask MVC setup.

Why Your Current Approach Fails

Your current code encounters two major architectural issues:

  • Flask's Blocking Nature: When you call self.viewer.run(), Flask starts a blocking local web server. Python execution halts there, waiting for incoming HTTP requests. Consequently, self.start_game() is never reached.
  • The Request Context: If you call get_input() before running the app, Flask throws a RuntimeError. This happens because Flask's request object is a thread-local proxy that only exists when an active HTTP request is being handled by the server. You cannot access it during the initial startup sequence.

The Solution: Restructuring the MVC Pattern for the Web

To resolve this, you must let Flask drive the application lifecycle. Instead of the Game class driving the Viewer, the Viewer (Flask) should receive HTTP requests, consult the Game (Model/Controller) for state updates, and render the appropriate template (View).

Because HTTP is stateless, we should also use Flask's session to keep track of the game state between requests.

1. The Refactored Game Engine (game.py)

Let's decouple the game logic so it doesn't know or care about Flask. It simply manages tasks, answers, and scores:

import random

class GameEngine:
    def generate_task(self):
        num1 = random.randint(1, 10)
        num2 = random.randint(1, 10)
        return {
            "question": f"{num1} x {num2}",
            "answer": num1 * num2
        }

    def check_answer(self, user_answer, correct_answer):
        try:
            return int(user_answer) == int(correct_answer)
        except (ValueError, TypeError):
            return False

2. The Flask View and Controller (app.py)

Now, we set up Flask to handle the routing. It instantiates the GameEngine and uses session to persist the current task and player score:

from flask import Flask, render_template, request, redirect, url_for, session
from game import GameEngine

app = Flask(__name__)
app.secret_key = "super-secret-key-for-sessions"
game = GameEngine()

@app.route("/", methods=["GET", "POST"])
def index():
    # Initialize session variables if they don't exist
    if "score" not in session:
        session["score"] = 0
    if "current_task" not in session:
        session["current_task"] = game.generate_task()

    feedback = None

    if request.method == "POST":
        user_answer = request.form.get("answer")
        correct_answer = session["current_task"]["answer"]

        if game.check_answer(user_answer, correct_answer):
            session["score"] += 1
            feedback = "Correct! Well done."
        else:
            session["score"] -= 1
            feedback = f"Incorrect. The correct answer was {correct_answer}."

        # Generate a new task for the next turn
        session["current_task"] = game.generate_task()

    return render_template(
        "table.html",
        question=session["current_task"]["question"],
        score=session["score"],
        feedback=feedback
    ) 

if __name__ == "__main__":
    app.run(debug=True)

3. The HTML Template (templates/table.html)

Create a simple template to display the game state and accept user input:

<!DOCTYPE html>
<html lang='en'>
<head>
    <meta charset='UTF-8'>
    <title>Multiplication Game</title>
</head>
<body>
    <h1>Math Challenge</h1>
    <p>Current Score: <strong>{{ score }}</strong></p>

    {% if feedback %}
        <p><em>{{ feedback }}</em></p>
    {% endif %}

    <form method='POST'>
        <label for='answer'>What is {{ question }}?</label>
        <input type='number' name='answer' id='answer' required autofocus>
        <button type='submit'>Submit</button>
    </form>
</body>
</html>

Key Takeaways

  • Invert the Control: Do not try to run an active loop inside a web application. Let Flask's routing mechanism handle user inputs asynchronously.
  • Manage State with Sessions: Since HTTP is stateless, use Flask's built-in session dictionary to store user-specific data like scores and current active tasks.
  • Avoid Global State: Storing game state in global variables will cause issues when multiple players access the application. Stick to client-side sessions or a database.