import os import json from datetime import datetime, timedelta from flask import Flask, render_template, request, jsonify, session, redirect, url_for, flash from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user from flask_socketio import SocketIO, emit, join_room, leave_room, send from config import config import random import logging from werkzeug.security import generate_password_hash, check_password_hash # Настройка логирования logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) # Инициализация Flask app = Flask(__name__) app.config.from_object(config) # Инициализация расширений db = SQLAlchemy(app) login_manager = LoginManager(app) login_manager.login_view = 'login' socketio = SocketIO(app, cors_allowed_origins="*") # --- МОДЕЛИ БАЗЫ ДАННЫХ --- class GameRoom(db.Model): __tablename__ = 'game_rooms' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) code = db.Column(db.String(10), unique=True, nullable=False) creator_id = db.Column(db.Integer, db.ForeignKey('users.id')) created_at = db.Column(db.DateTime, default=datetime.utcnow) status = db.Column(db.String(20), default='waiting') # waiting, playing, finished current_month = db.Column(db.Integer, default=1) total_months = db.Column(db.Integer, default=12) start_capital = db.Column(db.Integer, default=100000) settings = db.Column(db.Text, default='{}') # JSON с настройками # Связи - убираем lazy='dynamic' для players players = db.relationship('GamePlayer', backref='room', lazy='select') # Изменено game_state = db.relationship('GameState', backref='room', uselist=False) class GamePlayer(db.Model): __tablename__ = 'game_players' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id')) room_id = db.Column(db.Integer, db.ForeignKey('game_rooms.id')) joined_at = db.Column(db.DateTime, default=datetime.utcnow) is_ready = db.Column(db.Boolean, default=False) is_admin = db.Column(db.Boolean, default=False) # Игровые данные capital = db.Column(db.Float, default=100000) ability = db.Column(db.String(50)) assets = db.Column(db.Text, default='[]') # JSON с активами position = db.Column(db.Integer, default=0) # Позиция в рейтинге class User(UserMixin, db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) password_hash = db.Column(db.String(256)) created_at = db.Column(db.DateTime, default=datetime.utcnow) last_seen = db.Column(db.DateTime, default=datetime.utcnow) # Статистика total_games = db.Column(db.Integer, default=0) games_won = db.Column(db.Integer, default=0) total_earnings = db.Column(db.Float, default=0) is_active = db.Column(db.Boolean, default=True) # Связи game_players = db.relationship('GamePlayer', backref='user', lazy='dynamic') rooms_created = db.relationship('GameRoom', backref='creator', lazy='dynamic') def set_password(self, password): """Установка хеша пароля""" self.password_hash = generate_password_hash(password) def check_password(self, password): """Проверка пароля""" if not self.password_hash: return False return check_password_hash(self.password_hash, password) def __repr__(self): return f'' class GameState(db.Model): __tablename__ = 'game_states' id = db.Column(db.Integer, primary_key=True) room_id = db.Column(db.Integer, db.ForeignKey('game_rooms.id'), unique=True) phase = db.Column(db.String(20), default='action') # action, market, event, results phase_end = db.Column(db.DateTime) market_data = db.Column(db.Text, default='{}') # JSON с данными рынка events = db.Column(db.Text, default='[]') # JSON с событиями history = db.Column(db.Text, default='[]') # JSON с историей updated_at = db.Column(db.DateTime, default=datetime.utcnow) # --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ --- def generate_room_code(): """Генерация уникального кода комнаты""" import string characters = string.ascii_uppercase + string.digits return ''.join(random.choice(characters) for _ in range(6)) def calculate_market_changes(room_id): """Расчет изменений на рынке (по формулам из концепции)""" room = GameRoom.query.get(room_id) players = GamePlayer.query.filter_by(room_id=room_id).all() # Пример расчета (упрощенный) market_data = json.loads(room.game_state.market_data) if room.game_state else {} # Активы и их базовые цены assets = { 'stock_gazprom': {'price': 1000, 'volatility': 0.2}, 'real_estate': {'price': 3000000, 'volatility': 0.1}, 'bitcoin': {'price': 1850000, 'volatility': 0.3}, 'oil': {'price': 5000, 'volatility': 0.25} } # Считаем спрос по каждому активу for asset_id, asset_data in assets.items(): demand = 0 for player in players: player_assets = json.loads(player.assets) for player_asset in player_assets: if player_asset.get('id') == asset_id: demand += player_asset.get('quantity', 0) # Формула влияния спроса total_players = len(players) if total_players > 0: price_change = asset_data['volatility'] * (demand / (total_players * 10)) new_price = asset_data['price'] * (1 + price_change) # Эффект перегрева if new_price > asset_data['price'] * (1 + 2 * asset_data['volatility']): new_price *= random.uniform(0.7, 0.9) assets[asset_id]['price'] = new_price # Случайные события events = [ ('boom_oil', 'Бум нефти', {'oil': 1.3}, 'positive'), ('cyber_attack', 'Кибератака', {'bitcoin': 0.5}, 'negative'), ('elections', 'Выборы президента', {'all': 0.95}, 'neutral'), ('sanctions', 'Санкции', {'stock_gazprom': 0.65, 'oil': 0.8}, 'negative') ] event_name, event_description, event_effects, event_type = random.choice(events) # Применяем эффекты события for asset_id, multiplier in event_effects.items(): if asset_id == 'all': for key in assets.keys(): assets[key]['price'] *= multiplier elif asset_id in assets: assets[asset_id]['price'] *= multiplier return { 'assets': assets, 'event': { 'name': event_name, 'description': event_description, 'type': event_type, 'effects': event_effects }, 'timestamp': datetime.utcnow().isoformat() } # --- FLASK-LOGIN --- @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) # --- РОУТЫ --- # Главная страница теперь будет index.html с описанием @app.route('/') def index(): return render_template('index.html') # Старый маршрут index теперь перенаправляет на главную @app.route('/home') def home(): return redirect(url_for('index')) @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('rooms')) if request.method == 'POST': username = request.form.get('username', '').strip() password = request.form.get('password', '') remember = request.form.get('remember', False) user = User.query.filter_by(username=username).first() if user and user.check_password(password): if not user.is_active: flash('Аккаунт заблокирован', 'error') else: login_user(user, remember=remember) user.last_seen = datetime.utcnow() db.session.commit() flash(f'Добро пожаловать, {username}!', 'success') # Редирект на следующую страницу или rooms next_page = request.args.get('next') return redirect(next_page or url_for('rooms')) else: flash('Неверное имя пользователя или пароль', 'error') return render_template('login.html') @app.route('/quick_login/') def quick_login(username): """Быстрый вход для тестирования (только для разработки!)""" user = User.query.filter_by(username=username).first() if user: login_user(user) flash(f'Быстрый вход как {username}', 'info') return redirect(url_for('rooms')) # Если пользователя нет, создаем его user = User( username=username, email=f'{username}@test.com', is_active=True ) user.set_password('test123') db.session.add(user) db.session.commit() login_user(user) flash(f'Создан и вошли как {username}', 'success') return redirect(url_for('rooms')) @app.route('/register', methods=['GET', 'POST']) def register(): if current_user.is_authenticated: return redirect(url_for('rooms')) if request.method == 'POST': username = request.form.get('username', '').strip() email = request.form.get('email', '').strip() password = request.form.get('password', '') password2 = request.form.get('password2', '') # Простая валидация errors = [] if not username: errors.append('Введите имя пользователя') elif len(username) < 3: errors.append('Имя пользователя должно быть не менее 3 символов') elif User.query.filter_by(username=username).first(): errors.append('Имя пользователя уже занято') if not email: errors.append('Введите email') elif '@' not in email: errors.append('Введите корректный email') elif User.query.filter_by(email=email).first(): errors.append('Email уже используется') if not password: errors.append('Введите пароль') elif len(password) < 4: errors.append('Пароль должен быть не менее 4 символов') elif password != password2: errors.append('Пароли не совпадают') if errors: for error in errors: flash(error, 'error') return render_template('register.html') try: # Создаем пользователя user = User( username=username, email=email ) user.set_password(password) db.session.add(user) db.session.commit() login_user(user, remember=True) flash('Регистрация успешна! Добро пожаловать!', 'success') return redirect(url_for('rooms')) except Exception as e: db.session.rollback() flash(f'Ошибка при регистрации: {str(e)}', 'error') return render_template('register.html') @app.route('/logout') @login_required def logout(): logout_user() return redirect(url_for('index')) @app.route('/rooms') @login_required def rooms(): """Страница списка комнат""" try: # Получаем все активные комнаты all_rooms = GameRoom.query.filter( GameRoom.status.in_(['waiting', 'playing']) ).order_by(GameRoom.created_at.desc()).all() # Для каждой комнаты получаем количество игроков rooms_with_counts = [] for room in all_rooms: # Получаем количество игроков player_count = GamePlayer.query.filter_by(room_id=room.id).count() # Получаем создателя creator = User.query.get(room.creator_id) if room.creator_id else None # Добавляем комнату с дополнительной информацией rooms_with_counts.append({ 'id': room.id, 'name': room.name, 'code': room.code, 'status': room.status, 'creator': creator, 'creator_id': room.creator_id, 'current_month': room.current_month, 'total_months': room.total_months, 'start_capital': room.start_capital, 'settings': room.settings, 'player_count': player_count, 'players': [] # Пустой список, так как не загружаем всех игроков }) # Комнаты текущего пользователя user_rooms = [] user_room_ids = GamePlayer.query.filter_by( user_id=current_user.id ).with_entities(GamePlayer.room_id).all() user_room_ids = [rid for (rid,) in user_room_ids] for room_data in rooms_with_counts: if room_data['id'] in user_room_ids: user_rooms.append(room_data) print(f"Found {len(rooms_with_counts)} rooms, user in {len(user_rooms)} rooms") return render_template('rooms.html', rooms=rooms_with_counts, user_rooms=user_rooms, config=app.config) except Exception as e: print(f"Error in rooms route: {str(e)}") import traceback traceback.print_exc() db.session.rollback() flash(f'Ошибка при загрузке комнат: {str(e)}', 'error') return render_template('rooms.html', rooms=[], user_rooms=[], config=app.config) @app.route('/room/create', methods=['POST']) @login_required def create_room(): """Создание новой комнаты""" try: # Получаем данные из формы room_name = request.form.get('name', 'Новая комната').strip() total_months = int(request.form.get('total_months', 12)) start_capital = int(request.form.get('start_capital', 100000)) allow_loans = request.form.get('allow_loans') == 'on' allow_black_market = request.form.get('allow_black_market') == 'on' private_room = request.form.get('private_room') == 'on' # Валидация if not room_name: return jsonify({'error': 'Введите название комнаты'}), 400 # Генерируем уникальный код комнаты import string import random def generate_room_code(): chars = string.ascii_uppercase + string.digits return ''.join(random.choice(chars) for _ in range(6)) room_code = generate_room_code() # Создаем комнату room = GameRoom( name=room_name, code=room_code, creator_id=current_user.id, total_months=total_months, start_capital=start_capital, settings=json.dumps({ 'allow_loans': allow_loans, 'allow_black_market': allow_black_market, 'private_room': private_room }) ) db.session.add(room) db.session.commit() # Добавляем создателя как игрока-администратора player = GamePlayer( user_id=current_user.id, room_id=room.id, is_admin=True, is_ready=True, capital=start_capital, ability=random.choice([ 'crisis_investor', 'lobbyist', 'predictor', 'golden_pillow', 'shadow_accountant', 'credit_magnate' ]) ) db.session.add(player) db.session.commit() # Создаем начальное состояние игры game_state = GameState( room_id=room.id, market_data=json.dumps({ 'initialized': True, 'assets': { 'stock_gazprom': {'price': 1000, 'volatility': 0.2}, 'real_estate': {'price': 3000000, 'volatility': 0.1}, 'bitcoin': {'price': 1850000, 'volatility': 0.3}, 'oil': {'price': 5000, 'volatility': 0.25} }, 'last_update': datetime.utcnow().isoformat() }) ) db.session.add(game_state) db.session.commit() # Отправляем успешный ответ return jsonify({ 'success': True, 'room_code': room.code, 'redirect': url_for('lobby', room_code=room.code) }) except Exception as e: db.session.rollback() print(f"Error creating room: {str(e)}") import traceback traceback.print_exc() return jsonify({'error': f'Ошибка при создании комнаты: {str(e)}'}), 500 @app.route('/room/') @login_required def lobby(room_code): """Лобби комнаты""" room = GameRoom.query.filter_by(code=room_code).first_or_404() # Получаем всех игроков комнаты players = GamePlayer.query.filter_by(room_id=room.id).all() # Получаем информацию о пользователях для игроков players_with_users = [] for player in players: user = User.query.get(player.user_id) player.user = user # Добавляем объект пользователя к игроку players_with_users.append(player) # Проверяем, есть ли текущий пользователь в комнате current_player = GamePlayer.query.filter_by( user_id=current_user.id, room_id=room.id ).first() # Если игрока нет и комната в ожидании, добавляем его if not current_player and room.status == 'waiting': # Проверяем, есть ли место в комнате if len(players) >= app.config['MAX_PLAYERS_PER_ROOM']: flash('Комната заполнена', 'error') return redirect(url_for('rooms')) # Случайная способность import random abilities = [ 'crisis_investor', 'lobbyist', 'predictor', 'golden_pillow', 'shadow_accountant', 'credit_magnate', 'bear_raid', 'fake_news', 'dividend_king', 'raider_capture', 'mafia_connections', 'economic_advisor', 'currency_speculator' ] current_player = GamePlayer( user_id=current_user.id, room_id=room.id, is_admin=False, is_ready=False, capital=room.start_capital, ability=random.choice(abilities) ) db.session.add(current_player) db.session.commit() players_with_users.append(current_player) flash(f'Вы присоединились к комнате "{room.name}"', 'success') elif not current_player: flash('Вы не можете присоединиться к этой комнате', 'error') return redirect(url_for('rooms')) # Получаем создателя комнаты creator = User.query.get(room.creator_id) if room.creator_id else None return render_template('lobby.html', room=room, players=players_with_users, current_player=current_player, creator=creator, config=app.config) @app.route('/room//start', methods=['POST']) @login_required def start_room_game(room_code): """Начало игры в комнате""" room = GameRoom.query.filter_by(code=room_code).first_or_404() # Проверяем права администратора player = GamePlayer.query.filter_by( user_id=current_user.id, room_id=room.id, is_admin=True ).first() if not player: return jsonify({'error': 'Только администратор может начать игру'}), 403 if room.status != 'waiting': return jsonify({'error': 'Игра уже начата или завершена'}), 400 # Проверяем минимальное количество игроков player_count = GamePlayer.query.filter_by(room_id=room.id).count() if player_count < 2: return jsonify({'error': 'Нужно минимум 2 игрока для начала игры'}), 400 # Меняем статус комнаты room.status = 'playing' db.session.commit() # Отправляем событие через WebSocket socketio.emit('game_started', { 'room': room.code, 'message': 'Игра началась!' }, room=room.code) return jsonify({'success': True}) @app.route('/room//ready', methods=['POST']) @login_required def toggle_player_ready(room_code): """Изменение статуса готовности игрока""" room = GameRoom.query.filter_by(code=room_code).first_or_404() player = GamePlayer.query.filter_by( user_id=current_user.id, room_id=room.id ).first_or_404() data = request.get_json() is_ready = data.get('ready', not player.is_ready) player.is_ready = is_ready db.session.commit() # Отправляем событие через WebSocket socketio.emit('player_ready_changed', { 'user_id': current_user.id, 'username': current_user.username, 'is_ready': is_ready }, room=room.code) return jsonify({'success': True, 'is_ready': is_ready}) @app.route('/api/room//status') @login_required def get_room_status(room_code): """Получение статуса комнаты""" room = GameRoom.query.filter_by(code=room_code).first_or_404() player_count = GamePlayer.query.filter_by(room_id=room.id).count() ready_count = GamePlayer.query.filter_by(room_id=room.id, is_ready=True).count() return jsonify({ 'status': room.status, 'player_count': player_count, 'ready_count': ready_count, 'current_month': room.current_month }) # @app.route('/game/') # @login_required # def game(room_code): # room = GameRoom.query.filter_by(code=room_code).first_or_404() # # # Проверяем, что игрок в комнате и игра идет # player = GamePlayer.query.filter_by( # user_id=current_user.id, # room_id=room.id # ).first_or_404() # # if room.status != 'playing': # return redirect(url_for('lobby', room_code=room_code)) # # # Получаем данные игры # game_state = room.game_state # market_data = json.loads(game_state.market_data) if game_state else {} # # # Получаем всех игроков для лидерборда # players = GamePlayer.query.filter_by(room_id=room.id).order_by(GamePlayer.capital.desc()).all() # # return render_template('game.html', # room=room, # player=player, # players=players, # game_state=game_state, # market_data=market_data) @app.route('/room//update', methods=['POST']) @login_required def update_room_settings(room_code): """Обновление настроек комнаты""" room = GameRoom.query.filter_by(code=room_code).first_or_404() # Проверяем права администратора player = GamePlayer.query.filter_by( user_id=current_user.id, room_id=room.id, is_admin=True ).first() if not player: return jsonify({'error': 'Только администратор может изменять настройки'}), 403 if room.status != 'waiting': return jsonify({'error': 'Нельзя изменять настройки во время игры'}), 400 try: room.name = request.form.get('name', room.name) room.total_months = int(request.form.get('total_months', room.total_months)) room.start_capital = int(request.form.get('start_capital', room.start_capital)) # Обновляем настройки settings = json.loads(room.settings) if room.settings else {} settings['allow_loans'] = request.form.get('allow_loans') == 'on' settings['allow_black_market'] = request.form.get('allow_black_market') == 'on' room.settings = json.dumps(settings) db.session.commit() socketio.emit('room_updated', { 'room': room.code, 'name': room.name }, room=room.code) return jsonify({'success': True}) except Exception as e: db.session.rollback() return jsonify({'error': f'Ошибка при обновлении настроек: {str(e)}'}), 500 @app.route('/room//kick/', methods=['POST']) @login_required def kick_player(room_code, user_id): """Выгнать игрока из комнаты""" room = GameRoom.query.filter_by(code=room_code).first_or_404() # Проверяем права администратора admin_player = GamePlayer.query.filter_by( user_id=current_user.id, room_id=room.id, is_admin=True ).first() if not admin_player: return jsonify({'error': 'Только администратор может выгонять игроков'}), 403 # Нельзя выгнать себя или другого администратора if user_id == current_user.id: return jsonify({'error': 'Нельзя выгнать самого себя'}), 400 player_to_kick = GamePlayer.query.filter_by( user_id=user_id, room_id=room.id ).first() if not player_to_kick: return jsonify({'error': 'Игрок не найден в комнате'}), 404 if player_to_kick.is_admin: return jsonify({'error': 'Нельзя выгнать другого администратора'}), 400 # Удаляем игрока db.session.delete(player_to_kick) db.session.commit() socketio.emit('player_kicked', { 'user_id': user_id, 'username': User.query.get(user_id).username if User.query.get(user_id) else 'Игрок' }, room=room.code) return jsonify({'success': True}) @app.route('/room//kick_all', methods=['POST']) @login_required def kick_all_players(room_code): """Выгнать всех игроков из комнаты""" room = GameRoom.query.filter_by(code=room_code).first_or_404() # Проверяем права администратора admin_player = GamePlayer.query.filter_by( user_id=current_user.id, room_id=room.id, is_admin=True ).first() if not admin_player: return jsonify({'error': 'Только администратор может выгонять игроков'}), 403 # Удаляем всех игроков кроме администратора GamePlayer.query.filter_by(room_id=room.id).filter( GamePlayer.user_id != current_user.id ).delete(synchronize_session=False) db.session.commit() socketio.emit('all_players_kicked', { 'room': room.code }, room=room.code) return jsonify({'success': True}) @app.route('/room//reset', methods=['POST']) @login_required def reset_room(room_code): """Сбросить комнату к начальному состоянию""" room = GameRoom.query.filter_by(code=room_code).first_or_404() # Проверяем права администратора admin_player = GamePlayer.query.filter_by( user_id=current_user.id, room_id=room.id, is_admin=True ).first() if not admin_player: return jsonify({'error': 'Только администратор может сбрасывать комнату'}), 403 # Сбрасываем статус комнаты room.status = 'waiting' room.current_month = 1 # Сбрасываем всех игроков GamePlayer.query.filter_by(room_id=room.id).update({ 'is_ready': False, 'capital': room.start_capital, 'assets': '[]', 'position': 0 }) # Сбрасываем состояние игры game_state = GameState.query.filter_by(room_id=room.id).first() if game_state: game_state.phase = 'action' game_state.phase_end = None game_state.market_data = json.dumps({ 'initialized': True, 'assets': { 'stock_gazprom': {'price': 1000, 'volatility': 0.2}, 'real_estate': {'price': 3000000, 'volatility': 0.1}, 'bitcoin': {'price': 1850000, 'volatility': 0.3}, 'oil': {'price': 5000, 'volatility': 0.25} }, 'last_update': datetime.utcnow().isoformat() }) game_state.events = '[]' game_state.history = '[]' db.session.commit() socketio.emit('room_reset', { 'room': room.code }, room=room.code) return jsonify({'success': True}) @app.route('/api/room//players') @login_required def get_room_players(room_code): """Получение списка игроков комнаты без перезагрузки страницы""" room = GameRoom.query.filter_by(code=room_code).first_or_404() # Проверяем, состоит ли пользователь в комнате current_player = GamePlayer.query.filter_by( user_id=current_user.id, room_id=room.id ).first() if not current_player: return jsonify({'error': 'Вы не в этой комнате'}), 403 # Получаем всех игроков players = GamePlayer.query.filter_by(room_id=room.id).all() players_data = [] for player in players: user = User.query.get(player.user_id) players_data.append({ 'user_id': player.user_id, 'username': user.username if user else 'Игрок', 'is_admin': player.is_admin, 'is_ready': player.is_ready, 'capital': player.capital, 'ability_name': get_ability_name(player.ability), 'ability_description': get_ability_description(player.ability) }) return jsonify({ 'success': True, 'players': players_data, 'room_status': room.status }) @app.route('/room//delete', methods=['POST']) @login_required def delete_room(room_code): """Удалить комнату""" room = GameRoom.query.filter_by(code=room_code).first_or_404() # Проверяем права администратора admin_player = GamePlayer.query.filter_by( user_id=current_user.id, room_id=room.id, is_admin=True ).first() if not admin_player: return jsonify({'error': 'Только администратор может удалить комнату'}), 403 # Удаляем комнату и все связанные данные GamePlayer.query.filter_by(room_id=room.id).delete() GameState.query.filter_by(room_id=room.id).delete() db.session.delete(room) db.session.commit() return jsonify({'success': True}) @app.route('/api/game//assets') @login_required def get_assets(room_code): room = GameRoom.query.filter_by(code=room_code).first_or_404() game_state = room.game_state if not game_state: return jsonify({'error': 'Game state not found'}), 404 market_data = json.loads(game_state.market_data) return jsonify(market_data) @app.route('/api/game//action', methods=['POST']) @login_required def perform_action(room_code): """Выполнение действия игрока""" room = GameRoom.query.filter_by(code=room_code).first_or_404() # Проверяем, что сейчас фаза действий if room.game_state.phase != 'action': return jsonify({'error': 'Not action phase'}), 400 data = request.json action_type = data.get('type') player = GamePlayer.query.filter_by( user_id=current_user.id, room_id=room.id ).first_or_404() # Обработка разных типов действий if action_type == 'buy_asset': asset_id = data.get('asset_id') quantity = data.get('quantity', 1) # Здесь должна быть логика покупки актива # ... # Обновляем капитал игрока player.capital -= data.get('cost', 0) # Сохраняем активы assets = json.loads(player.assets) assets.append({ 'id': asset_id, 'quantity': quantity, 'purchase_price': data.get('price'), 'timestamp': datetime.utcnow().isoformat() }) player.assets = json.dumps(assets) db.session.commit() # Отправляем обновление через WebSocket socketio.emit('player_action', { 'player_id': current_user.id, 'player_name': current_user.username, 'action': action_type, 'asset_id': asset_id, 'quantity': quantity }, room=room.code) return jsonify({'success': True, 'new_capital': player.capital}) return jsonify({'error': 'Unknown action type'}), 400 @app.template_filter('from_json') def from_json_filter(value): """Преобразует JSON строку в объект""" if isinstance(value, str): return json.loads(value) return value @app.template_filter('format_currency') def format_currency_filter(value): """Форматирует число как валюту""" if value is None: return "0 ₽" return f"{value:,.0f} ₽".replace(",", " ") # --- WEBSOCKET ОБРАБОТЧИКИ --- @socketio.on('connect') def handle_connect(): if current_user.is_authenticated: logger.info(f'User {current_user.username} connected') emit('connected', {'user_id': current_user.id, 'username': current_user.username}) @socketio.on('join_room') def handle_join_room(data): room_code = data.get('room') if room_code and current_user.is_authenticated: join_room(room_code) logger.info(f'User {current_user.username} joined room {room_code}') # Проверяем, было ли уже уведомление о входе session_key = f'has_notified_join_{room_code}' if not session.get(session_key): # Отправляем уведомление другим игрокам emit('player_joined', { 'user_id': current_user.id, 'username': current_user.username, 'timestamp': datetime.utcnow().isoformat(), 'is_reconnect': False }, room=room_code, include_self=False) # Отмечаем в сессии, что уведомление отправлено session[session_key] = True else: # Это реконнект, отправляем тихое обновление emit('player_reconnected', { 'user_id': current_user.id, 'username': current_user.username }, room=room_code, include_self=False) @socketio.on('leave_room') def handle_leave_room(data): room_code = data.get('room') if room_code: leave_room(room_code) logger.info(f'User {current_user.username} left room {room_code}') emit('player_left', { 'user_id': current_user.id, 'username': current_user.username }, room=room_code) @socketio.on('player_ready') def handle_player_ready(data): room_code = data.get('room') is_ready = data.get('ready', True) room = GameRoom.query.filter_by(code=room_code).first() if room: player = GamePlayer.query.filter_by( user_id=current_user.id, room_id=room.id ).first() if player: player.is_ready = is_ready db.session.commit() emit('player_ready_changed', { 'user_id': current_user.id, 'username': current_user.username, 'is_ready': is_ready }, room=room_code) @socketio.on('chat_message') def handle_chat_message(data): room_code = data.get('room') message = data.get('message', '').strip() if message and room_code: emit('chat_message', { 'user_id': current_user.id, 'username': current_user.username, 'message': message, 'timestamp': datetime.utcnow().isoformat() }, room=room_code) @socketio.on('join_global_room') def handle_join_global_room(): """Присоединение к глобальной комнате для обновлений""" join_room('global_updates') print(f'User {current_user.username if current_user else "Guest"} joined global room') @socketio.on('user_online') def handle_user_online(data): """Обработка статуса онлайн пользователя""" if current_user.is_authenticated: current_user.last_seen = datetime.utcnow() db.session.commit() print(f'User {current_user.username} is online') # Добавим обработчик для обновления комнат @socketio.on('get_rooms') def handle_get_rooms(): """Отправка списка комнат""" rooms = GameRoom.query.filter(GameRoom.status != 'finished').all() rooms_data = [] for room in rooms: players_count = GamePlayer.query.filter_by(room_id=room.id).count() rooms_data.append({ 'id': room.id, 'name': room.name, 'code': room.code, 'status': room.status, 'players': players_count, 'max_players': app.config['MAX_PLAYERS_PER_ROOM'], 'month': room.current_month, 'total_months': room.total_months }) emit('rooms_list', {'rooms': rooms_data}, room=request.sid) # --- КОМАНДЫ УПРАВЛЕНИЯ --- @app.cli.command('init-db') def init_db_command(): """Инициализация базы данных""" db.create_all() print('База данных инициализирована.') @app.cli.command('create-test-data') def create_test_data(): """Создание тестовых данных""" from werkzeug.security import generate_password_hash # Создаем тестового пользователя test_user = User( username='test', email='test@example.com', password_hash=generate_password_hash('test123') ) db.session.add(test_user) db.session.commit() print('Тестовые данные созданы.') @app.cli.command('create-test-users') def create_test_users(): """Создание тестовых пользователей""" test_users = [ {'username': 'Игрок1', 'email': 'player1@test.com', 'password': '123456'}, {'username': 'Игрок2', 'email': 'player2@test.com', 'password': '123456'}, {'username': 'Инвестор', 'email': 'investor@test.com', 'password': '123456'}, {'username': 'Трейдер', 'email': 'trader@test.com', 'password': '123456'}, {'username': 'Банкир', 'email': 'banker@test.com', 'password': '123456'}, {'username': 'admin', 'email': 'admin@test.com', 'password': 'admin123'}, ] created_count = 0 for user_data in test_users: # Проверяем, существует ли пользователь existing_user = User.query.filter_by(username=user_data['username']).first() if not existing_user: user = User( username=user_data['username'], email=user_data['email'], is_active=True ) user.set_password(user_data['password']) db.session.add(user) created_count += 1 print(f'✓ Создан пользователь: {user_data["username"]}') else: print(f'⏭ Пользователь уже существует: {user_data["username"]}') try: db.session.commit() print(f'\n✅ Создано {created_count} тестовых пользователей!') print('\nТестовые учетные данные:') print('------------------------') for user_data in test_users: print(f'Логин: {user_data["username"]}, Пароль: {user_data["password"]}') except Exception as e: db.session.rollback() print(f'❌ Ошибка при создании пользователей: {str(e)}') @app.cli.command('create-demo-rooms') def create_demo_rooms(): """Создание демонстрационных комнат""" import random demo_rooms = [ { 'name': '🤑 Быстрая игра для новичков', 'total_months': 6, 'start_capital': 50000, 'allow_loans': False, 'allow_black_market': False }, { 'name': '📈 Турнир профессионалов', 'total_months': 12, 'start_capital': 200000, 'allow_loans': True, 'allow_black_market': True }, { 'name': '👥 Игра с друзьями', 'total_months': 12, 'start_capital': 100000, 'allow_loans': True, 'allow_black_market': False }, { 'name': '⚡ Экспресс-торги', 'total_months': 3, 'start_capital': 75000, 'allow_loans': False, 'allow_black_market': True }, ] # Получаем случайного пользователя для создания комнат users = User.query.all() if not users: print('❌ Нет пользователей. Сначала создайте тестовых пользователей.') return created_count = 0 for room_data in demo_rooms: creator = random.choice(users) # Генерируем уникальный код import string chars = string.ascii_uppercase + string.digits room_code = ''.join(random.choice(chars) for _ in range(6)) # Проверяем, существует ли комната с таким кодом while GameRoom.query.filter_by(code=room_code).first(): room_code = ''.join(random.choice(chars) for _ in range(6)) # Создаем комнату room = GameRoom( name=room_data['name'], code=room_code, creator_id=creator.id, total_months=room_data['total_months'], start_capital=room_data['start_capital'], settings=json.dumps({ 'allow_loans': room_data['allow_loans'], 'allow_black_market': room_data['allow_black_market'], 'private_room': False }) ) db.session.add(room) db.session.commit() # Добавляем создателя в комнату player = GamePlayer( user_id=creator.id, room_id=room.id, is_admin=True, is_ready=True, capital=room_data['start_capital'], ability=random.choice([ 'crisis_investor', 'lobbyist', 'predictor', 'golden_pillow', 'shadow_accountant', 'credit_magnate' ]) ) db.session.add(player) # Создаем начальное состояние игры game_state = GameState( room_id=room.id, market_data=json.dumps({ 'initialized': True, 'assets': { 'stock_gazprom': {'price': 1000, 'volatility': 0.2}, 'real_estate': {'price': 3000000, 'volatility': 0.1}, 'bitcoin': {'price': 1850000, 'volatility': 0.3}, 'oil': {'price': 5000, 'volatility': 0.25} }, 'last_update': datetime.utcnow().isoformat() }) ) db.session.add(game_state) db.session.commit() # Добавляем еще 2-3 случайных игроков в комнату other_users = [u for u in users if u.id != creator.id] if other_users: for _ in range(random.randint(2, 3)): if other_users: random_user = random.choice(other_users) other_users.remove(random_user) player = GamePlayer( user_id=random_user.id, room_id=room.id, is_admin=False, is_ready=random.choice([True, False]), capital=room_data['start_capital'], ability=random.choice([ 'crisis_investor', 'lobbyist', 'predictor', 'golden_pillow', 'shadow_accountant', 'credit_magnate' ]) ) db.session.add(player) db.session.commit() created_count += 1 print(f'✓ Создана демо-комната: {room_data["name"]} (код: {room_code})') print(f'\n✅ Создано {created_count} демонстрационных комнат!') @app.cli.command('reset-db') def reset_db(): """Полный сброс базы данных""" confirmation = input('⚠️ Вы уверены, что хотите сбросить всю базу данных? (yes/no): ') if confirmation.lower() != 'yes': print('❌ Отменено') return # Удаляем все таблицы db.drop_all() print('🗑️ Таблицы удалены') # Создаем заново db.create_all() print('✅ Таблицы созданы заново') # Создаем тестовых пользователей print('\n👥 Создание тестовых пользователей...') ctx = app.test_request_context() with ctx: create_test_users.callback() # Создаем демо-комнаты print('\n🏢 Создание демонстрационных комнат...') with ctx: create_demo_rooms.callback() print('\n🎉 База данных успешно сброшена и заполнена тестовыми данными!') # --- ОБРАБОТЧИКИ ОШИБОК --- from datetime import datetime @app.errorhandler(404) def not_found_error(error): return render_template('404.html'), 404 @app.errorhandler(500) def internal_error(error): db.session.rollback() return render_template('500.html', now=datetime.now()), 500 @app.errorhandler(403) def forbidden_error(error): return render_template('403.html'), 403 # Создадим и 403 ошибку для полноты @app.route('/403') def forbidden_page(): return render_template('403.html'), 403 # Дополнительные функции def get_ability_name(ability_code): """Получить название способности по коду""" abilities = { 'crisis_investor': 'Кризисный инвестор', 'lobbyist': 'Лоббист', 'predictor': 'Предсказатель', 'golden_pillow': 'Золотая подушка', 'shadow_accountant': 'Теневая бухгалтерия', 'credit_magnate': 'Кредитный магнат', 'bear_raid': 'Медвежий набег', 'fake_news': 'Фейковые новости', 'dividend_king': 'Король дивидендов', 'raider_capture': 'Рейдерский захват', 'mafia_connections': 'Мафиозные связи', 'economic_advisor': 'Экономический советник', 'currency_speculator': 'Валютный спекулянт' } return abilities.get(ability_code, 'Неизвестная способность') def get_ability_description(ability_code): """Получить описание способности по коду""" descriptions = { 'crisis_investor': '+20% к доходу при падении рынка', 'lobbyist': 'Может временно менять правила налогообложения', 'predictor': 'Видит следующее случайное событие', 'golden_pillow': 'Может защитить 20% капитала от кризисов', 'shadow_accountant': 'Раз в игру уменьшить налоги на 50%', 'credit_magnate': 'Может давать кредиты другим игрокам', 'bear_raid': 'Вызвать искусственное падение цены актива на 15%', 'fake_news': 'Подменить случайное событие на выгодное', 'dividend_king': 'Получать +10% дохода от всех акций', 'raider_capture': 'Попытаться отобрать 5% капитала у лидера', 'mafia_connections': 'Отменить одно негативное событие', 'economic_advisor': 'Раз в 3 месяца изменить налоговую ставку', 'currency_speculator': 'Отвязать рубль от нефти на 1 месяц' } return descriptions.get(ability_code, 'Описание отсутствует') # Добавим функции в контекст шаблонов @app.context_processor def utility_processor(): return dict( get_ability_name=get_ability_name, get_ability_description=get_ability_description ) # --- ТОЧКА ВХОДА --- if __name__ == '__main__': # Создаем таблицы если их нет with app.app_context(): db.create_all() # Запускаем сервер socketio.run(app, host='0.0.0.0', port=5000, debug=True, allow_unsafe_werkzeug=True)