import os
from flask import Flask, request, render_template
from flask_socketio import SocketIO, emit, join_room, leave_room
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime, timedelta
import uuid
import threading
import time
app = Flask(__name__)
app.config['SECRET_KEY'] = 'super-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///game.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
socketio = SocketIO(app, cors_allowed_origins="*")
# --- Models ---
class Session(db.Model):
id = db.Column(db.String(36), primary_key=True)
room_id = db.Column(db.String(50), unique=True, nullable=False)
player1_id = db.Column(db.String(36), nullable=True)
player2_id = db.Column(db.String(36), nullable=True)
current_turn_player_id = db.Column(db.String(36), nullable=True)
status = db.Column(db.String(20), default='active') # active, finished, abandoned
last_action_at = db.Column(db.DateTime, default=datetime.utcnow)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class Player(db.Model):
id = db.Column(db.String(36), primary_key=True)
session_id = db.Column(db.String(36), db.ForeignKey('session.id'))
nickname = db.Column(db.String(50), nullable=False)
side = db.Column(db.String(10)) # player1, player2
player_token = db.Column(db.String(100), unique=True)
sid = db.Column(db.String(100), nullable=True)
class Unit(db.Model):
id = db.Column(db.String(36), primary_key=True)
session_id = db.Column(db.String(36), db.ForeignKey('session.id'))
owner_id = db.Column(db.String(36), db.ForeignKey('player.id'))
unit_type = db.Column(db.String(20))
x = db.Column(db.Integer)
y = db.Column(db.Integer)
health = db.Column(db.Integer)
# --- Routes ---
@app.route('/')
def index():
return render_template('index.html')
@app.route('/game/<room_id>')
def game(room_id):
return render_template('game.html', room_id=room_id)
# --- Helpers ---
def update_session_activity(session_id):
session = Session.query.get(session_id)
if session:
session.last_action_at = datetime.utcnow()
db.session.commit()
# --- SocketIO Events ---
@socketio.on('create_game')
def handle_create_game(data):
nickname = data.get('nickname')
room_id = str(uuid.uuid4())[:8]
session_id = str(uuid.uuid4())
player_id = str(uuid.uuid4())
player_token = str(uuid.uuid4())
new_session = Session(id=session_id, room_id=room_id, status='active', current_turn_player_id=player_id)
new_player = Player(id=player_id, session_id=session_id, nickname=nickname, side='player1', player_token=player_token, sid=request.sid)
db.session.add(new_session)
db.session.add(new_player)
new_session.player1_id = player_id
db.session.commit()
join_room(room_id)
emit('game_created', {'room_id': room_id, 'player_token': player_token}, room=request.sid)
@socketio.on('join_game')
def handle_join_game(data):
room_id = data.get('room_id')
session = Session.query.filter_by(room_id=room_id).first()
player_token = data.get('player_token')
nickname = data.get('nickname')
if not session or session.status != 'active':
emit('error', {'message': 'Room not found or game ended'})
return
if player_token:
# Case 1: Re-connecting player (creator)
player = Player.query.filter_by(player_token=player_token).first()
if player and player.session_id == session.id:
player.sid = request.sid
if nickname: # Update nickname if provided during reconnect
player.nickname = nickname
db.session.commit()
join_room(room_id)
emit('game_joined', {'room_id': room_id}, room=request.sid)
return
else:
emit('error', {'message': 'Invalid player token'})
return
# Case 2: Second player joining via link
if session.player2_id is None:
player_id = str(uuid.uuid4())
new_player = Player(id=player_id, session_id=session.id, nickname=nickname, side='player2', player_token=str(uuid.uuid4()), sid=request.sid)
session.player2_id = player_id
db.session.add(new_player)
db.session.commit()
join_room(room_id)
emit('game_joined', {'room_id': room_id}, room=request.sid)
emit('player_joined', {'nickname': nickname}, room=room_id)
else:
emit('error', {'message': 'Room full'})
@socketio.on('connect')
def handle_connect():
# Logic for reconnection would go here:
# Check if sid is associated with an existing player_token in the database
pass
# --- Background Task ---
def cleanup_task():
with app.app_context(): # Note: app.app_context()
while True:
time.sleep(60)
timeout_limit = datetime.utcnow() - timedelta(minutes=20)
expired_sessions = Session.query.filter(Session.status == 'active', Session.last_action_at < timeout_limit).all()
for s in expired_sessions:
s.status = 'abandoned'
db.session.commit()
print(f"Session {s.room_id} abandoned due to inactivity.")
if __name__ == '__main__':
with app.app_context():
db.create_all()
threading.Thread(target=cleanup_task, daemon=True).start()
socketio.run(app, debug=True, port=5000)