Files
cm/app.py
2026-04-20 14:39:59 +03:00

3137 lines
121 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
import sys
sys.path.append('.')
from game_balance.config import BalanceConfig
from game_balance.validator import BalanceValidator
from game_balance.assets_config import AssetsConfig
from game_balance.players_config import PlayersConfig
# Настройка логирования
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 с настройками
balance_mode = db.Column(db.String(20), default='standard')
# Связи - убираем 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)
initial_capital = db.Column(db.Float, default=100000) # Для расчета прибыли
ability = db.Column(db.String(50))
assets = db.Column(db.Text, default='[]')
position = db.Column(db.Integer, default=0)
total_profit = db.Column(db.Float, default=0) # Общая прибыль
last_income_month = 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'<User {self.username}>'
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), nullable=True) # Разрешаем NULL
phase_end = db.Column(db.DateTime, nullable=True)
market_data = db.Column(db.Text, default='{}')
events = db.Column(db.Text, default='[]')
history = db.Column(db.Text, default='[]')
updated_at = db.Column(db.DateTime, default=datetime.utcnow)
class AssetInventory(db.Model):
"""Остатки активов на рынке"""
__tablename__ = 'asset_inventories'
id = db.Column(db.Integer, primary_key=True)
room_id = db.Column(db.Integer, db.ForeignKey('game_rooms.id'))
asset_id = db.Column(db.String(50), nullable=False)
total_quantity = db.Column(db.Float, nullable=False)
remaining_quantity = db.Column(db.Float, nullable=False)
current_price = db.Column(db.Float, nullable=False)
base_price = db.Column(db.Float, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
__table_args__ = (
db.UniqueConstraint('room_id', 'asset_id', name='unique_room_asset'),
)
class Transaction(db.Model):
"""История транзакций"""
__tablename__ = 'transactions'
id = db.Column(db.Integer, primary_key=True)
room_id = db.Column(db.Integer, db.ForeignKey('game_rooms.id'))
player_id = db.Column(db.Integer, db.ForeignKey('game_players.id'))
asset_id = db.Column(db.String(50), nullable=False)
transaction_type = db.Column(db.String(20), nullable=False) # buy, sell
quantity = db.Column(db.Float, nullable=False)
price = db.Column(db.Float, nullable=False)
total_amount = db.Column(db.Float, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Связи
player = db.relationship('GamePlayer', backref='transactions')
# --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
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/<username>')
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))
balance_mode = request.form.get('balance_mode', 'standard')
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
# Загружаем баланс
balance = BalanceConfig.load_balance_mode(balance_mode)
players_config = balance.get_players_config()
assets_config = balance.get_assets_config()
economy_config = balance.get_economy_config()
# Генерируем код комнаты
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,
balance_mode=balance_mode,
status='waiting', # Статус комнаты, не фаза
settings=json.dumps({
'allow_loans': allow_loans,
'allow_black_market': allow_black_market,
'private_room': private_room
})
)
db.session.add(room)
db.session.commit()
# Добавляем создателя как игрока
abilities = list(players_config.get('ABILITIES', PlayersConfig.ABILITIES).keys())
player = GamePlayer(
user_id=current_user.id,
room_id=room.id,
is_admin=True,
is_ready=True,
capital=start_capital,
ability=random.choice(abilities) if abilities else None
)
db.session.add(player)
db.session.commit()
# Создаем состояние игры
market_data = {
'initialized': True,
'assets': assets_config,
'balance_mode': balance_mode,
'last_update': datetime.utcnow().isoformat(),
'correlations': economy_config.get('ASSET_CORRELATIONS', {})
}
game_state = GameState(
room_id=room.id,
phase=None, # Не 'waiting', а None
phase_end=None,
market_data=json.dumps(market_data)
)
db.session.add(game_state)
db.session.commit()
# ========== ДОБАВЛЯЕМ ИНВЕНТАРЬ ЗДЕСЬ ==========
# Создаем инвентарь для ограниченных активов
from game_balance.assets_config import AssetsConfig # импорт в начале файла
for asset_id, asset_data in assets_config.items():
if asset_data.get('total_quantity') is not None:
inventory = AssetInventory(
room_id=room.id,
asset_id=asset_id,
total_quantity=asset_data['total_quantity'],
remaining_quantity=asset_data['total_quantity'],
current_price=asset_data['base_price'],
base_price=asset_data['base_price']
)
db.session.add(inventory)
# ================================================
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/<room_code>')
@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/<room_code>/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': f'Игра уже начата или завершена. Текущий статус: {room.status}'}), 400
# Проверяем минимальное количество игроков
player_count = GamePlayer.query.filter_by(room_id=room.id).count()
if player_count < 2:
return jsonify({'error': 'Нужно минимум 2 игрока для начала игры'}), 400
# Меняем статус комнаты
room.status = 'playing'
room.current_month = 1
# Получаем длительность фазы из баланса
try:
balance = BalanceConfig.load_balance_mode(room.balance_mode)
game_config = balance.get_game_config()
phase_duration = game_config.get('PHASE_DURATIONS', {}).get('action', 120)
except Exception as e:
print(f"Error loading balance: {str(e)}")
phase_duration = 120
# Создаем или обновляем состояние игры
if not room.game_state:
# Загружаем конфигурацию активов из баланса
try:
balance = BalanceConfig.load_balance_mode(room.balance_mode)
assets_config = balance.get_assets_config()
economy_config = balance.get_economy_config()
except Exception as e:
print(f"Error loading assets config: {str(e)}")
from game_balance.assets_config import AssetsConfig
from game_balance.economy_config import EconomyConfig
assets_config = AssetsConfig.ASSETS
economy_config = {'ASSET_CORRELATIONS': EconomyConfig.ASSET_CORRELATIONS}
# Создаем инвентарь для ограниченных активов
for asset_id, asset_data in assets_config.items():
if asset_data.get('total_quantity') is not None:
# Проверяем, существует ли уже инвентарь
existing = AssetInventory.query.filter_by(
room_id=room.id,
asset_id=asset_id
).first()
if not existing:
inventory = AssetInventory(
room_id=room.id,
asset_id=asset_id,
total_quantity=asset_data['total_quantity'],
remaining_quantity=asset_data['total_quantity'],
current_price=asset_data['base_price'],
base_price=asset_data['base_price']
)
db.session.add(inventory)
# Устанавливаем время окончания фазы
phase_end_time = datetime.utcnow() + timedelta(seconds=phase_duration)
# Создаем новое состояние
game_state = GameState(
room_id=room.id,
phase='action',
phase_end=phase_end_time,
market_data=json.dumps({
'initialized': True,
'assets': assets_config,
'balance_mode': room.balance_mode,
'last_update': datetime.utcnow().isoformat(),
'correlations': economy_config.get('ASSET_CORRELATIONS', {})
})
)
db.session.add(game_state)
print(f"✅ Created new game state for room {room.code}")
else:
# Обновляем существующее состояние
room.game_state.phase = 'action'
room.game_state.phase_end = datetime.utcnow() + timedelta(seconds=phase_duration)
room.game_state.updated_at = datetime.utcnow()
print(f"✅ Updated existing game state for room {room.code}")
# Сбрасываем статус готовности всех игроков
GamePlayer.query.filter_by(room_id=room.id).update({
'is_ready': False,
'capital': room.start_capital, # Сбрасываем капитал
'assets': '[]' # Очищаем активы
})
db.session.commit()
# Проверяем что phase_end установлен
print(f"⏰ Phase end time: {room.game_state.phase_end}")
print(f"⏰ ISO format: {room.game_state.phase_end.isoformat()}")
print(f"⏰ Seconds remaining: {(room.game_state.phase_end - datetime.utcnow()).total_seconds()}")
# Отправляем событие через WebSocket
try:
socketio.emit('game_started', {
'room': room.code,
'message': 'Игра началась!',
'phase': 'action',
'phase_end': phase_end_time.isoformat() + 'Z',
'month': room.current_month,
'total_months': room.total_months
}, room=room.code)
print(f"📤 Game started event sent to room {room.code}")
except Exception as e:
print(f"❌ WebSocket error: {str(e)}")
return jsonify({'success': True})
@app.route('/room/<room_code>/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/<room_code>/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/<room_code>')
# @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/<room_code>/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/<room_code>/kick/<int:user_id>', 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/<room_code>/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/<room_code>/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/<room_code>/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/<room_code>/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('/room/<room_code>/exit', methods=['POST'])
@login_required
def exit_lobby(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()
if not player:
return jsonify({'error': 'Вы не находитесь в этой комнате'}), 400
# Если игрок - последний в комнате, удаляем комнату
player_count = GamePlayer.query.filter_by(room_id=room.id).count()
# Если игрок - администратор и в комнате есть другие игроки
if player.is_admin and player_count > 1:
# Назначаем нового администратора
new_admin = GamePlayer.query.filter_by(
room_id=room.id,
user_id=current_user.id
).first()
if new_admin:
new_admin.is_admin = True
# Удаляем игрока из комнаты
db.session.delete(player)
# Если игрок был последним, удаляем комнату и состояние игры
if player_count == 1:
# Удаляем всех игроков (хотя остался только текущий)
GamePlayer.query.filter_by(room_id=room.id).delete()
# Удаляем состояние игры
game_state = GameState.query.filter_by(room_id=room.id).first()
if game_state:
db.session.delete(game_state)
# Удаляем комнату
db.session.delete(room)
db.session.commit()
# Отправляем WebSocket событие
socketio.emit('player_left', {
'user_id': current_user.id,
'username': current_user.username,
'reason': 'exited'
}, room=room.code)
return jsonify({
'success': True,
'message': 'Вы вышли из лобби',
'room_deleted': player_count == 1
})
@app.route('/api/game/<room_code>/assets')
@login_required
def get_game_assets(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()
# Загружаем баланс
balance = BalanceConfig.load_balance_mode(room.balance_mode)
assets_config = balance.get_assets_config()
# Получаем текущие цены из game_state
game_state = room.game_state
market_data = json.loads(game_state.market_data) if game_state else {}
market_assets = market_data.get('assets', {})
# Получаем инвентарь для ограниченных активов
inventories = {}
inventory_list = AssetInventory.query.filter_by(room_id=room.id).all()
for inv in inventory_list:
inventories[inv.asset_id] = inv
# Получаем активы игрока
player_assets = json.loads(player.assets) if player.assets else []
player_assets_dict = {}
for pa in player_assets:
player_assets_dict[pa['id']] = pa['quantity']
# Формируем ответ
result = {}
for asset_id, asset_config in assets_config.items():
asset_data = asset_config.copy()
# Обновляем цену
if asset_id in market_assets:
asset_data['price'] = market_assets[asset_id].get('price', asset_config['base_price'])
else:
asset_data['price'] = asset_config['base_price']
# Добавляем информацию о доступности
if asset_config.get('total_quantity') is not None:
inv = inventories.get(asset_id)
if inv:
asset_data['available'] = inv.remaining_quantity
asset_data['total'] = inv.total_quantity
asset_data['current_price'] = inv.current_price
else:
# Если инвентаря нет, создаем его
new_inv = AssetInventory(
room_id=room.id,
asset_id=asset_id,
total_quantity=asset_config['total_quantity'],
remaining_quantity=asset_config['total_quantity'],
current_price=asset_data['price'],
base_price=asset_config['base_price']
)
db.session.add(new_inv)
db.session.commit()
asset_data['available'] = asset_config['total_quantity']
asset_data['total'] = asset_config['total_quantity']
else:
asset_data['available'] = None
asset_data['total'] = None
# Добавляем информацию о владении игрока
asset_data['player_quantity'] = player_assets_dict.get(asset_id, 0)
asset_data['max_per_player'] = asset_config.get('max_per_player')
asset_data['min_purchase'] = asset_config.get('min_purchase', 1)
result[asset_id] = asset_data
# Определяем категории активов прямо здесь
categories = {
'bonds': {'name': 'Облигации', 'icon': '🏦'},
'stocks': {'name': 'Акции', 'icon': '📈'},
'real_estate': {'name': 'Недвижимость', 'icon': '🏠'},
'crypto': {'name': 'Криптовалюта', 'icon': '💰'},
'commodities': {'name': 'Сырьевые товары', 'icon': ''},
'business': {'name': 'Бизнес', 'icon': '🏢'},
'unique': {'name': 'Уникальные активы', 'icon': '🏆'}
}
return jsonify({
'success': True,
'assets': result,
'categories': categories,
'player_capital': player.capital # Добавляем капитал игрока
})
@app.route('/api/game/<room_code>/transactions')
@login_required
def get_transactions(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()
# Получаем последние 50 транзакций
transactions = Transaction.query.filter_by(
room_id=room.id,
player_id=player.id
).order_by(Transaction.created_at.desc()).limit(50).all()
result = []
for t in transactions:
result.append({
'id': t.id,
'type': t.transaction_type,
'asset_id': t.asset_id,
'quantity': t.quantity,
'price': t.price,
'total': t.total_amount,
'created_at': t.created_at.isoformat()
})
return jsonify({
'success': True,
'transactions': result
})
@app.route('/api/game/<room_code>/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 ₽"
try:
return f"{value:,.0f}".replace(",", " ")
except:
return f"{int(value)}"
# --- 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')
reason = data.get('reason', 'left') # 'left', 'exited', 'kicked'
if room_code and current_user.is_authenticated:
leave_room(room_code)
logger.info(f'User {current_user.username} left room {room_code} (reason: {reason})')
messages = {
'left': 'покинул комнату',
'exited': 'вышел из лобби',
'kicked': 'был выгнан'
}
emit('player_left', {
'user_id': current_user.id,
'username': current_user.username,
'reason': reason,
'message': f'{current_user.username} {messages.get(reason, "покинул комнату")}',
'timestamp': datetime.utcnow().isoformat()
}, room=room_code, include_self=False)
# Очищаем флаг уведомления в сессии
session_key = f'has_notified_join_{room_code}'
session.pop(session_key, None)
@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🎉 База данных успешно сброшена и заполнена тестовыми данными!')
@app.cli.command('validate-balance')
def validate_balance_command():
"""Проверка всех режимов баланса"""
from game_balance.validator import BalanceValidator
modes = BalanceConfig.get_available_modes()
print(f"Проверка баланса для {len(modes)} режимов...")
print("-" * 50)
all_valid = True
for mode_key, mode_name in modes.items():
print(f"\nРежим: {mode_name}")
balance = BalanceConfig.load_balance_mode(mode_key)
errors = BalanceValidator.validate_balance_mode(balance)
if errors:
print(f"❌ Найдено {len(errors)} ошибок:")
for error in errors:
print(f" - {error}")
all_valid = False
else:
print("✅ Баланс корректен")
if all_valid:
print("\nВсе режимы баланса валидны!")
else:
print("\n❌ Обнаружены ошибки в балансе")
@app.cli.command('test-income')
def test_income_command():
"""Тестирование начисления дохода"""
with app.app_context():
# Берем первую активную комнату
room = GameRoom.query.filter_by(status='playing').first()
if not room:
print("No active game rooms found")
return
print(f"Testing income calculation for room {room.code}")
calculate_player_income(room.id)
# --- ОБРАБОТЧИКИ ОШИБОК ---
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, 'Описание отсутствует')
def calculate_player_income(room_id):
"""Начисление дохода от активов игрокам"""
with app.app_context():
try:
room = GameRoom.query.get(room_id)
if not room or room.status != 'playing':
return
print(f"💰 Calculating income for room {room_id}, month {room.current_month}")
# Загружаем баланс
try:
balance = BalanceConfig.load_balance_mode(room.balance_mode)
assets_config = balance.get_assets_config()
except:
from game_balance.assets_config import AssetsConfig
assets_config = AssetsConfig.ASSETS
# Получаем текущие цены из market_data
game_state = room.game_state
market_data = json.loads(game_state.market_data) if game_state and game_state.market_data else {}
assets = market_data.get('assets', {})
# Получаем всех игроков
players = GamePlayer.query.filter_by(room_id=room_id).all()
total_income_distributed = 0
players_with_income = 0
for player in players:
player_assets = json.loads(player.assets) if player.assets else []
monthly_income = 0
for asset in player_assets:
asset_id = asset['id']
quantity = asset['quantity']
# Получаем конфигурацию актива
asset_config = assets_config.get(asset_id, {})
base_income_rate = asset_config.get('income_per_month', 0)
if base_income_rate > 0 and quantity > 0:
# Получаем текущую цену актива
current_price = None
# Из инвентаря для ограниченных активов
if asset_config.get('total_quantity') is not None:
inventory = AssetInventory.query.filter_by(
room_id=room_id,
asset_id=asset_id
).first()
if inventory:
current_price = inventory.current_price
# Из market_data для безлимитных
if current_price is None and asset_id in assets:
current_price = assets[asset_id].get('price')
# Если цену не нашли, используем базовую
if current_price is None:
current_price = asset_config.get('base_price', 1000)
# Рассчитываем доход
asset_value = current_price * quantity
income_rate = base_income_rate
# Проверяем способность "Король дивидендов"
if player.ability == 'dividend_king':
income_rate *= 1.1 # +10% к доходности
asset_income = asset_value * income_rate
monthly_income += asset_income
if monthly_income > 0:
# Начисляем доход
old_capital = player.capital
player.capital += monthly_income
player.total_profit = (player.total_profit or 0) + monthly_income
player.last_income_month = room.current_month
total_income_distributed += monthly_income
players_with_income += 1
print(
f" Player {player.user_id}: +{monthly_income:.0f} income, capital: {old_capital:.0f}{player.capital:.0f}")
if total_income_distributed > 0:
db.session.commit()
print(f"✅ Total income distributed: {total_income_distributed:.0f} to {players_with_income} players")
# Отправляем уведомление о начислении дохода
try:
socketio.emit('income_distributed', {
'room': room.code,
'month': room.current_month,
'total_income': total_income_distributed,
'message': f'💰 Начислен доход от активов за месяц {room.current_month}: {format_currency_filter(total_income_distributed)}'
}, room=room.code)
except Exception as e:
print(f"WebSocket error: {str(e)}")
else:
print(f" No income distributed for month {room.current_month}")
except Exception as e:
print(f"❌ Error calculating player income: {str(e)}")
import traceback
traceback.print_exc()
db.session.rollback()
# Добавим функции в контекст шаблонов
@app.context_processor
def utility_processor():
return dict(
get_ability_name=get_ability_name,
get_ability_description=get_ability_description
)
# Логика игры
@app.route('/game/<room_code>')
@login_required
def game_page(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':
flash('Игра еще не началась или уже завершена', 'warning')
return redirect(url_for('lobby', room_code=room_code))
# Получаем состояние игры
game_state = room.game_state
if not game_state:
flash('Состояние игры не найдено', 'error')
return redirect(url_for('lobby', room_code=room_code))
# Если фаза не установлена, устанавливаем action
if game_state.phase is None:
game_state.phase = 'action'
game_state.phase_end = datetime.utcnow() + timedelta(seconds=120)
db.session.commit()
# if not game_state:
# # Создаем начальное состояние игры если его нет
# balance = BalanceConfig.load_balance_mode(room.balance_mode)
# assets_config = balance.get_assets_config()
# economy_config = balance.get_economy_config()
# game_config = balance.get_game_config()
# phase_duration = game_config.get('PHASE_DURATIONS', {}).get('action', 120)
#
# game_state = GameState(
# room_id=room.id,
# phase='action',
# phase_end=datetime.utcnow() + timedelta(seconds=phase_duration),
# market_data=json.dumps({
# 'initialized': True,
# 'assets': assets_config,
# 'balance_mode': room.balance_mode,
# 'last_update': datetime.utcnow().isoformat(),
# 'correlations': economy_config.get('ASSET_CORRELATIONS', {})
# })
# )
# db.session.add(game_state)
# db.session.commit()
# Получаем всех игроков для лидерборда
players = GamePlayer.query.filter_by(room_id=room.id).all()
# Словарь названий активов для шаблона
asset_names = {
'gov_bonds': 'Гособлигации',
'stock_gazprom': 'Акции Газпрома',
'stock_sberbank': 'Акции Сбербанка',
'stock_yandex': 'Акции Яндекса',
'apartment_small': 'Небольшая квартира',
'apartment_elite': 'Элитная квартира',
'bitcoin': 'Биткоин',
'oil': 'Нефть Brent',
'natural_gas': 'Природный газ',
'coffee_shop': 'Кофейня',
'it_startup': 'IT-стартап',
'shopping_mall': 'Торговый центр',
'oil_field': 'Нефтяное месторождение'
}
return render_template('game.html',
room=room,
player=player,
players=players,
game_state=game_state,
asset_names=asset_names)
# API эндпоинты для игры
@app.route('/api/game/<room_code>/state')
@login_required
def get_game_state_api(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()
game_state = room.game_state
if not game_state:
return jsonify({
'success': False,
'error': 'Game state not found'
}), 404
# Форматируем phase_end для отправки
phase_end_iso = None
if game_state.phase_end:
phase_end_iso = game_state.phase_end.isoformat()
print(f"📤 API: Sending phase_end = {phase_end_iso}")
else:
print(f"⚠️ API: phase_end is None for room {room.code}")
return jsonify({
'success': True,
'phase': game_state.phase,
'phase_end': phase_end_iso,
'current_month': room.current_month,
'total_months': room.total_months,
'player_capital': player.capital,
'room_status': room.status
})
@app.route('/api/game/<room_code>/buy', methods=['POST'])
@login_required
def buy_asset(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.game_state and room.game_state.phase != 'action':
return jsonify({'success': False, 'error': 'Покупка доступна только в фазу действий'}), 400
data = request.json
asset_id = data.get('asset_id')
quantity = float(data.get('quantity', 1))
if not asset_id or quantity <= 0:
return jsonify({'success': False, 'error': 'Не указан актив или количество'}), 400
# Загружаем баланс комнаты
balance = BalanceConfig.load_balance_mode(room.balance_mode)
assets_config = balance.get_assets_config()
# Получаем конфигурацию актива
asset_config = assets_config.get(asset_id)
if not asset_config:
return jsonify({'success': False, 'error': 'Актив не найден'}), 404
# Проверяем минимальное количество для покупки
min_purchase = asset_config.get('min_purchase', 1)
if quantity < min_purchase:
return jsonify({'success': False, 'error': f'Минимальное количество для покупки: {min_purchase}'}), 400
# Получаем текущую цену из инвентаря или конфига
inventory = None
current_price = asset_config['base_price']
if asset_config.get('total_quantity') is not None:
# Ограниченный актив - берем из инвентаря
inventory = AssetInventory.query.filter_by(
room_id=room.id,
asset_id=asset_id
).first()
if not inventory:
return jsonify({'success': False, 'error': 'Инвентарь актива не найден'}), 404
# Проверяем остаток
if inventory.remaining_quantity < quantity:
return jsonify({'success': False,
'error': f'Доступно только {inventory.remaining_quantity} {asset_config["name"]}'}), 400
current_price = inventory.current_price
else:
# Безлимитный актив - берем из game_state
game_state = room.game_state
market_data = json.loads(game_state.market_data) if game_state else {}
assets = market_data.get('assets', {})
if asset_id in assets:
current_price = assets[asset_id].get('price', asset_config['base_price'])
# Рассчитываем стоимость
total_cost = current_price * quantity
# Проверяем достаточно ли средств
if player.capital < total_cost:
return jsonify({'success': False, 'error': f'Недостаточно средств. Нужно: {format_currency_filter(total_cost)}'}), 400
# Проверяем лимиты на владение
if asset_config.get('max_per_player'):
# Считаем сколько уже есть у игрока
current_assets = json.loads(player.assets) if player.assets else []
current_quantity = 0
for asset in current_assets:
if asset['id'] == asset_id:
current_quantity += asset['quantity']
if current_quantity + quantity > asset_config['max_per_player']:
return jsonify({
'success': False,
'error': f'Максимум {asset_config["max_per_player"]} {asset_config["name"]} на игрока'
}), 400
# Проверяем месячные лимиты - ИСПРАВЛЕНО
players_config = balance.get_players_config()
purchase_limits = players_config.get('PURCHASE_LIMITS_BY_MONTH', {})
month_limit = purchase_limits.get(room.current_month, 1.0)
# Проверяем только если есть лимит на игрока
if asset_config.get('max_per_player') is not None:
max_allowed = asset_config['max_per_player']
max_this_month = max_allowed * month_limit
if current_quantity + quantity > max_this_month:
return jsonify({
'success': False,
'error': f'В этом месяце можно купить не более {max_this_month} {asset_config["name"]}'
}), 400
# ВСЕ ПРОВЕРКИ ПРОЙДЕНЫ - ВЫПОЛНЯЕМ ПОКУПКУ
# 1. Обновляем инвентарь (для ограниченных активов)
if inventory:
inventory.remaining_quantity -= quantity
inventory.updated_at = datetime.utcnow()
# 2. Обновляем активы игрока
player_assets = json.loads(player.assets) if player.assets else []
# Ищем существующий актив
found = False
for asset in player_assets:
if asset['id'] == asset_id:
asset['quantity'] += quantity
asset['purchase_price'] = (asset['purchase_price'] * asset['quantity'] + current_price * quantity) / (
asset['quantity'] + quantity)
found = True
break
# Если не нашли, добавляем новый
if not found:
player_assets.append({
'id': asset_id,
'quantity': quantity,
'purchase_price': current_price,
'purchase_date': datetime.utcnow().isoformat()
})
player.assets = json.dumps(player_assets)
# 3. Списываем деньги
player.capital -= total_cost
# 4. Сохраняем транзакцию
transaction = Transaction(
room_id=room.id,
player_id=player.id,
asset_id=asset_id,
transaction_type='buy',
quantity=quantity,
price=current_price,
total_amount=total_cost
)
db.session.add(transaction)
db.session.commit()
# 5. Отправляем WebSocket уведомление
socketio.emit('player_action', {
'room': room.code,
'player_id': player.user_id,
'player_name': current_user.username,
'action': 'buy',
'asset_id': asset_id,
'asset_name': asset_config['name'],
'quantity': quantity,
'price': current_price,
'total': total_cost,
'timestamp': datetime.utcnow().isoformat()
}, room=room.code)
return jsonify({
'success': True,
'new_capital': player.capital,
'asset_name': asset_config['name'],
'quantity': quantity,
'price': current_price,
'total': total_cost,
'remaining': inventory.remaining_quantity if inventory else None,
'message': f'Куплено {quantity} {asset_config["name"]} за {format_currency_filter(total_cost)}'
})
@app.route('/api/game/<room_code>/sell', methods=['POST'])
@login_required
def sell_asset(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.game_state and room.game_state.phase != 'action':
return jsonify({'success': False, 'error': 'Продажа доступна только в фазу действий'}), 400
data = request.json
asset_id = data.get('asset_id')
quantity = float(data.get('quantity', 1))
if not asset_id or quantity <= 0:
return jsonify({'success': False, 'error': 'Не указан актив или количество'}), 400
# Загружаем баланс комнаты
try:
balance = BalanceConfig.load_balance_mode(room.balance_mode)
assets_config = balance.get_assets_config()
economy_config = balance.get_economy_config()
except Exception as e:
print(f"Error loading balance: {str(e)}")
from game_balance.assets_config import AssetsConfig
from game_balance.economy_config import EconomyConfig
assets_config = AssetsConfig.ASSETS
economy_config = EconomyConfig()
# Получаем конфигурацию актива
asset_config = assets_config.get(asset_id)
if not asset_config:
return jsonify({'success': False, 'error': 'Актив не найден'}), 404
# Получаем текущие активы игрока
player_assets = json.loads(player.assets) if player.assets else []
# Ищем актив для продажи
asset_to_sell = None
for asset in player_assets:
if asset['id'] == asset_id:
asset_to_sell = asset
break
if not asset_to_sell:
return jsonify({'success': False, 'error': f'У вас нет {asset_config["name"]}'}), 400
if asset_to_sell['quantity'] < quantity:
return jsonify({
'success': False,
'error': f'У вас только {asset_to_sell["quantity"]} {asset_config["name"]}'
}), 400
# Получаем текущую цену
current_price = None
inventory = None
# Для ограниченных активов - берем из инвентаря
if asset_config.get('total_quantity') is not None:
inventory = AssetInventory.query.filter_by(
room_id=room.id,
asset_id=asset_id
).first()
if inventory:
current_price = inventory.current_price
else:
# Если инвентаря нет, создаем его
inventory = AssetInventory(
room_id=room.id,
asset_id=asset_id,
total_quantity=asset_config['total_quantity'],
remaining_quantity=asset_config['total_quantity'],
current_price=asset_config['base_price'],
base_price=asset_config['base_price']
)
db.session.add(inventory)
db.session.commit()
current_price = asset_config['base_price']
# Для безлимитных активов - берем из game_state
else:
game_state = room.game_state
if game_state:
try:
market_data = json.loads(game_state.market_data)
assets = market_data.get('assets', {})
if asset_id in assets:
current_price = assets[asset_id].get('price', asset_config['base_price'])
except:
current_price = asset_config['base_price']
else:
current_price = asset_config['base_price']
# Если цену не нашли, используем цену покупки
if current_price is None:
current_price = asset_to_sell.get('purchase_price', asset_config['base_price'])
# Рассчитываем выручку
revenue = current_price * quantity
# Комиссия за продажу
transaction_fee = 0.01 # 1% по умолчанию
if economy_config:
if isinstance(economy_config, dict):
transaction_fee = economy_config.get('TAX_SYSTEM', {}).get('transaction_fee', 0.01)
else:
transaction_fee = getattr(economy_config, 'TRANSACTION_FEE', 0.01)
fee = revenue * transaction_fee
revenue_after_fee = revenue - fee
# ВЫПОЛНЯЕМ ПРОДАЖУ
# 1. Обновляем инвентарь (для ограниченных активов)
if asset_config.get('total_quantity') is not None and inventory:
inventory.remaining_quantity += quantity
inventory.updated_at = datetime.utcnow()
# 2. Обновляем активы игрока
asset_to_sell['quantity'] -= quantity
if asset_to_sell['quantity'] <= 0:
player_assets.remove(asset_to_sell)
player.assets = json.dumps(player_assets)
# 3. Начисляем деньги (после вычета комиссии)
player.capital += revenue_after_fee
# 4. Сохраняем транзакцию
transaction = Transaction(
room_id=room.id,
player_id=player.id,
asset_id=asset_id,
transaction_type='sell',
quantity=quantity,
price=current_price,
total_amount=revenue_after_fee
)
db.session.add(transaction)
db.session.commit()
# 5. Отправляем WebSocket уведомление
try:
socketio.emit('player_action', {
'room': room.code,
'player_id': player.user_id,
'player_name': current_user.username,
'action': 'sell',
'asset_id': asset_id,
'asset_name': asset_config['name'],
'quantity': quantity,
'price': current_price,
'total': revenue_after_fee,
'fee': fee,
'timestamp': datetime.utcnow().isoformat()
}, room=room.code)
except Exception as e:
print(f"WebSocket error: {str(e)}")
# 6. Проверяем прибыль/убыток от сделки
purchase_price = asset_to_sell.get('purchase_price', current_price)
profit_loss = (current_price - purchase_price) * quantity
profit_loss_percent = ((current_price - purchase_price) / purchase_price) * 100 if purchase_price > 0 else 0
profit_loss_text = f"Прибыль: {format_currency_filter(profit_loss)} ({profit_loss_percent:+.1f}%)" if profit_loss >= 0 else f"Убыток: {format_currency_filter(abs(profit_loss))} ({profit_loss_percent:+.1f}%)"
profit_loss_color = 'var(--success-color)' if profit_loss >= 0 else 'var(--danger-color)'
return jsonify({
'success': True,
'new_capital': player.capital,
'asset_name': asset_config['name'],
'quantity': quantity,
'price': current_price,
'total': revenue_after_fee,
'fee': fee,
'revenue': revenue,
'profit_loss': profit_loss,
'profit_loss_percent': profit_loss_percent,
'profit_loss_text': profit_loss_text,
'profit_loss_color': profit_loss_color,
'message': f'✅ Продано {quantity} {asset_config["name"]} за {format_currency_filter(revenue_after_fee)}'
})
@app.route('/api/game/<room_code>/use_ability', methods=['POST'])
@login_required
def use_ability(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 not player.ability:
return jsonify({'success': False, 'error': 'У вас нет способности'}), 400
# Здесь должна быть логика применения способности
# Пока просто возвращаем успех
return jsonify({
'success': True,
'ability_name': get_ability_name(player.ability),
'message': 'Способность использована'
})
@app.route('/api/game/<room_code>/end_action_phase', methods=['POST'])
@login_required
def end_action_phase(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({'success': False, 'error': 'Только администратор может завершать фазы'}), 403
if not room.game_state:
return jsonify({'success': False, 'error': 'Состояние игры не найдено'}), 404
if room.game_state.phase != 'action':
return jsonify(
{'success': False, 'error': f'Сейчас не фаза действий. Текущая фаза: {room.game_state.phase}'}), 400
# Загружаем баланс для получения длительности фазы
try:
balance = BalanceConfig.load_balance_mode(room.balance_mode)
game_config = balance.get_game_config()
phase_duration = game_config.get('PHASE_DURATIONS', {}).get('market', 30)
except Exception as e:
print(f"Error loading balance: {str(e)}")
phase_duration = 30
# Устанавливаем время окончания фазы рынка
phase_end_time = datetime.utcnow() + timedelta(seconds=phase_duration)
# Переходим к следующей фазе
game_state = room.game_state
game_state.phase = 'market'
game_state.phase_end = phase_end_time
game_state.updated_at = datetime.utcnow()
db.session.commit()
# Проверяем что phase_end установлен
print(f"⏰ Phase changed to market for room {room.code}")
print(f"⏰ Phase end time: {game_state.phase_end}")
print(f"⏰ ISO format: {game_state.phase_end.isoformat()}")
print(f"⏰ Seconds remaining: {(game_state.phase_end - datetime.utcnow()).total_seconds()}")
# Отправляем WebSocket событие
try:
socketio.emit('game_phase_changed', {
'room': room.code,
'phase': 'market',
'phase_end': phase_end_time.isoformat() + 'Z',
'month': room.current_month,
'total_months': room.total_months,
'message': 'Фаза действий завершена, начинается реакция рынка'
}, room=room.code)
print(f"📤 Phase changed event sent to room {room.code}")
except Exception as e:
print(f"❌ WebSocket error: {str(e)}")
# Запускаем расчет рынка в фоновом потоке с контекстом приложения
import threading
thread = threading.Thread(target=calculate_and_update_market, args=(room.id,))
thread.daemon = True
thread.start()
print(f"🔄 Market calculation thread started for room {room.code}")
return jsonify({'success': True})
def calculate_and_update_market(room_id):
"""Расчет реакции рынка и запуск следующей фазы"""
import time
import random
from flask import current_app
# Создаем контекст приложения для работы с БД в фоновом потоке
with app.app_context():
try:
# Ждем 5 секунд перед расчетом
time.sleep(5)
# Получаем комнату
room = GameRoom.query.get(room_id)
if not room or room.status != 'playing':
print(f"Room {room_id} not found or not playing")
return
print(
f"Calculating market for room {room_id}, current phase: {room.game_state.phase if room.game_state else 'None'}")
# Загружаем баланс комнаты
try:
balance = BalanceConfig.load_balance_mode(room.balance_mode)
assets_config = balance.get_assets_config()
economy_config = balance.get_economy_config()
game_config = balance.get_game_config()
events_config = balance.get_events_config()
except Exception as e:
print(f"Error loading balance mode: {str(e)}")
from game_balance.test_balance.standard_mode import StandardBalance
balance = StandardBalance()
assets_config = balance.get_assets_config()
economy_config = balance.get_economy_config()
game_config = balance.get_game_config()
events_config = balance.get_events_config()
game_state = room.game_state
if not game_state:
print(f"Game state not found for room {room_id}")
return
if game_state.phase != 'market':
print(f"Wrong phase for market calculation: {game_state.phase}")
return
# Получаем игроков
players = GamePlayer.query.filter_by(room_id=room_id).all()
# Получаем текущие данные рынка
try:
market_data = json.loads(game_state.market_data) if game_state.market_data else {}
except:
market_data = {}
assets = market_data.get('assets', {})
# Получаем инвентарь для ограниченных активов
inventories = {}
inventory_list = AssetInventory.query.filter_by(room_id=room_id).all()
for inv in inventory_list:
inventories[inv.asset_id] = inv
# Рассчитываем спрос по каждому активу
for asset_id, asset_data in assets.items():
if asset_id not in assets_config:
continue
# Получаем конфигурацию актива
asset_config = assets_config.get(asset_id, {})
# Сохраняем предыдущую цену для расчета изменения
asset_data['previous_price'] = asset_data.get('price', asset_config.get('base_price', 1000))
# Считаем общее количество активов у игроков
total_held = 0
for player in players:
try:
player_assets = json.loads(player.assets) if player.assets else []
for pa in player_assets:
if pa.get('id') == asset_id:
total_held += pa.get('quantity', 0)
except:
continue
# Получаем инвентарь для этого актива
inventory = inventories.get(asset_id)
# Коэффициент спроса
if inventory:
total_supply = inventory.total_quantity
if total_supply and total_supply > 0:
demand_factor = total_held / total_supply
else:
demand_factor = 0
else:
# Для безлимитных активов
demand_factor = total_held / (len(players) * 100) if players else 0
demand_factor = min(demand_factor, 1.0) # Ограничиваем
# Рассчитываем новую цену
try:
new_price = AssetsConfig.calculate_price(
asset_config,
demand_factor,
room.current_month,
len(players)
)
except Exception as e:
print(f"Error calculating price for {asset_id}: {str(e)}")
new_price = asset_config.get('base_price', 1000)
# Обновляем цену в активе
asset_data['price'] = new_price
# Обновляем цену в инвентаре
if inventory:
inventory.current_price = new_price
inventory.updated_at = datetime.utcnow()
# Применяем корреляции
try:
correlations = economy_config.get('ASSET_CORRELATIONS', {})
for corr_key, corr_value in correlations.items():
if ':' in corr_key:
asset1, asset2 = corr_key.split(':', 1)
if asset1 in assets and asset2 in assets:
price2 = assets[asset2]['price']
base2 = assets_config.get(asset2, {}).get('base_price', price2)
if base2 > 0:
change2 = (price2 / base2) - 1
assets[asset1]['price'] *= (1 + corr_value * change2)
except Exception as e:
print(f"Error applying correlations: {str(e)}")
# Обновляем данные рынка
market_data['assets'] = assets
market_data['last_update'] = datetime.utcnow().isoformat()
game_state.market_data = json.dumps(market_data)
db.session.commit()
print(f"Market updated for room {room_id}")
# Отправляем обновление через WebSocket
try:
socketio.emit('market_updated', {
'room': room.code,
'assets': assets,
'timestamp': datetime.utcnow().isoformat()
}, room=room.code)
print(f"Market updated event sent for room {room_id}")
except Exception as e:
print(f"WebSocket error: {str(e)}")
# Ждем 10 секунд перед переходом к событиям
time.sleep(10)
# Переходим к фазе событий
room = GameRoom.query.get(room_id)
if room and room.game_state and room.game_state.phase == 'market':
game_state = room.game_state
game_state.phase = 'event'
# Получаем длительность фазы из конфига
phase_duration = game_config.get('PHASE_DURATIONS', {}).get('event', 30)
game_state.phase_end = datetime.utcnow() + timedelta(seconds=phase_duration)
db.session.commit()
print(f"Phase changed to event for room {room_id}")
try:
socketio.emit('game_phase_changed', {
'room': room.code,
'phase': 'event',
'phase_end': game_state.phase_end.isoformat(),
'message': 'Реакция рынка завершена, начинаются случайные события'
}, room=room.code)
except Exception as e:
print(f"WebSocket error: {str(e)}")
# Запускаем генерацию событий
socketio.start_background_task(generate_random_event, room_id)
except Exception as e:
print(f"Error in calculate_and_update_market: {str(e)}")
import traceback
traceback.print_exc()
db.session.rollback()
def generate_random_event(room_id):
"""Генерация случайного события"""
import random
import time
with app.app_context():
try:
# Небольшая задержка
time.sleep(2)
room = GameRoom.query.get(room_id)
if not room or room.status != 'playing' or room.game_state.phase != 'event':
print(f"Room {room_id} not in event phase")
return
print(f"Generating random event for room {room_id}")
# Загружаем баланс
try:
balance = BalanceConfig.load_balance_mode(room.balance_mode)
events_config = balance.get_events_config()
game_config = balance.get_game_config()
except Exception as e:
print(f"Error loading events config: {str(e)}")
from game_balance.test_balance.standard_mode import StandardBalance
balance = StandardBalance()
events_config = balance.get_events_config()
game_config = balance.get_game_config()
# Выбираем случайное событие
events = events_config.get('EVENTS', {})
crises = events_config.get('CRISES', {})
# 20% шанс на кризис
if random.random() < 0.2 and crises:
event_id = random.choice(list(crises.keys()))
event_data = crises[event_id].copy()
event_data['is_crisis'] = True
else:
event_id = random.choice(list(events.keys()))
event_data = events[event_id].copy()
event_data['is_crisis'] = False
print(f"Selected event: {event_data.get('name')}")
# Получаем текущие данные рынка
game_state = room.game_state
market_data = json.loads(game_state.market_data) if game_state.market_data else {}
assets = market_data.get('assets', {})
# Применяем эффекты события
effects = event_data.get('effects', {})
affected_assets = []
for target, effect in effects.items():
if target == 'ALL':
# Применяем ко всем активам
for asset_id, asset_data in assets.items():
if 'price_change' in effect:
old_price = asset_data['price']
asset_data['price'] *= (1 + effect['price_change'])
asset_data['previous_price'] = old_price
affected_assets.append(asset_id)
elif target in assets:
# Применяем к конкретному активу
if 'price_change' in effect:
old_price = assets[target]['price']
assets[target]['price'] *= (1 + effect['price_change'])
assets[target]['previous_price'] = old_price
affected_assets.append(target)
elif target == 'stocks':
# Применяем ко всем акциям
for asset_id, asset_data in assets.items():
if asset_data.get('category') == 'stocks':
if 'price_change' in effect:
old_price = asset_data['price']
asset_data['price'] *= (1 + effect['price_change'])
asset_data['previous_price'] = old_price
affected_assets.append(asset_id)
elif target == 'real_estate':
# Применяем ко всей недвижимости
for asset_id, asset_data in assets.items():
if asset_data.get('category') == 'real_estate':
if 'price_change' in effect:
old_price = asset_data['price']
asset_data['price'] *= (1 + effect['price_change'])
asset_data['previous_price'] = old_price
affected_assets.append(asset_id)
# Обновляем данные рынка
market_data['assets'] = assets
market_data['last_event'] = {
'id': event_id,
'name': event_data.get('name'),
'description': event_data.get('description'),
'type': event_data.get('type'),
'is_crisis': event_data.get('is_crisis', False),
'effects': effects,
'timestamp': datetime.utcnow().isoformat()
}
game_state.market_data = json.dumps(market_data)
# Сохраняем событие в историю
events_history = json.loads(game_state.events) if game_state.events else []
events_history.append({
'id': event_id,
'name': event_data.get('name'),
'description': event_data.get('description'),
'type': event_data.get('type'),
'is_crisis': event_data.get('is_crisis', False),
'month': room.current_month,
'timestamp': datetime.utcnow().isoformat()
})
# Ограничиваем историю последними 20 событиями
if len(events_history) > 20:
events_history = events_history[-20:]
game_state.events = json.dumps(events_history)
db.session.commit()
print(f"Event applied for room {room_id}")
# Отправляем событие через WebSocket
try:
socketio.emit('game_event', {
'room': room.code,
'id': event_id,
'name': event_data.get('name'),
'description': event_data.get('description'),
'type': event_data.get('type'),
'is_crisis': event_data.get('is_crisis', False),
'effects': effects,
'affected_assets': affected_assets,
'timestamp': datetime.utcnow().isoformat()
}, room=room.code)
print(f"Game event sent for room {room_id}")
except Exception as e:
print(f"WebSocket error: {str(e)}")
# Ждем длительность фазы событий
phase_duration = game_config.get('PHASE_DURATIONS', {}).get('event', 30)
time.sleep(phase_duration - 5) # Ждем почти всю фазу
# Переходим к фазе результатов
room = GameRoom.query.get(room_id)
if room and room.game_state and room.game_state.phase == 'event':
game_state = room.game_state
game_state.phase = 'results'
# НАЧИСЛЯЕМ ДОХОД ОТ АКТИВОВ В НАЧАЛЕ ФАЗЫ РЕЗУЛЬТАТОВ
calculate_player_income(room_id)
phase_duration = game_config.get('PHASE_DURATIONS', {}).get('results', 45)
game_state.phase_end = datetime.utcnow() + timedelta(seconds=phase_duration)
db.session.commit()
print(f"Phase changed to results for room {room_id}")
try:
socketio.emit('game_phase_changed', {
'room': room.code,
'phase': 'results',
'phase_end': game_state.phase_end.isoformat(),
'message': 'Случайные события завершены, подводятся итоги месяца'
}, room=room.code)
except Exception as e:
print(f"WebSocket error: {str(e)}")
# Запускаем подведение итогов
socketio.start_background_task(end_month, room_id)
except Exception as e:
print(f"Error generating random event: {str(e)}")
import traceback
traceback.print_exc()
db.session.rollback()
def end_month(room_id):
"""Завершение месяца и переход к следующему"""
import time
with app.app_context():
try:
# Ждем почти всю фазу результатов
time.sleep(40)
room = GameRoom.query.get(room_id)
if not room or room.status != 'playing' or room.game_state.phase != 'results':
print(f"Room {room_id} not in results phase")
return
print(f"Ending month {room.current_month} for room {room_id}")
# ========== 1. СНАЧАЛА НАЧИСЛЯЕМ ДОХОД ЗА ТЕКУЩИЙ МЕСЯЦ ==========
print(f"💰 Calculating income for month {room.current_month}...")
calculate_player_income(room_id)
# Получаем всех игроков для обновления
players = GamePlayer.query.filter_by(room_id=room_id).all()
# ========== 2. ФОРМИРУЕМ ЛИДЕРБОРД С АКТУАЛЬНЫМИ ДАННЫМИ ==========
leaderboard_data = []
for player in players:
user = User.query.get(player.user_id)
profit = player.capital - room.start_capital
leaderboard_data.append({
'user_id': player.user_id,
'username': user.username if user else 'Игрок',
'capital': player.capital,
'ability': player.ability,
'profit': profit,
'profit_percent': (profit / room.start_capital) * 100 if room.start_capital > 0 else 0
})
# Сортируем по капиталу
leaderboard_data.sort(key=lambda x: x['capital'], reverse=True)
# ========== 3. ОТПРАВЛЯЕМ ОБНОВЛЕННЫЙ ЛИДЕРБОРД ==========
try:
socketio.emit('leaderboard_updated', {
'room': room.code,
'players': leaderboard_data,
'month': room.current_month
}, room=room.code)
print(f"📊 Leaderboard updated for month {room.current_month}")
except Exception as e:
print(f"WebSocket error: {str(e)}")
# ========== 4. ПРОВЕРЯЕМ ЗАВЕРШЕНИЕ ИГРЫ ==========
if room.current_month >= room.total_months:
# Игра завершена
room.status = 'finished'
room.game_state.phase = 'finished'
room.game_state.phase_end = None
# Определяем победителя
winner = leaderboard_data[0] if leaderboard_data else None
winner_name = winner['username'] if winner else 'Не определен'
winner_capital = winner['capital'] if winner else 0
db.session.commit()
try:
socketio.emit('game_ended', {
'room': room.code,
'winner': winner_name,
'winner_capital': winner_capital,
'leaderboard': leaderboard_data,
'message': f'🏆 Игра завершена! Победитель: {winner_name} с капиталом {format_currency_filter(winner_capital)}'
}, room=room.code)
except Exception as e:
print(f"WebSocket error: {str(e)}")
print(f"Game ended for room {room_id}, winner: {winner_name}")
return
# ========== 5. УВЕЛИЧИВАЕМ МЕСЯЦ ТОЛЬКО ЕСЛИ ИГРА НЕ ЗАВЕРШЕНА ==========
old_month = room.current_month
room.current_month += 1
print(f"📅 Month incremented from {old_month} to {room.current_month}")
# ========== 6. НАЧИНАЕМ НОВЫЙ МЕСЯЦ ==========
game_state = room.game_state
game_state.phase = 'action'
# Получаем длительность фазы из конфига
try:
balance = BalanceConfig.load_balance_mode(room.balance_mode)
game_config = balance.get_game_config()
phase_duration = game_config.get('PHASE_DURATIONS', {}).get('action', 120)
except:
phase_duration = 120
game_state.phase_end = datetime.utcnow() + timedelta(seconds=phase_duration)
game_state.updated_at = datetime.utcnow()
db.session.commit()
print(f"✅ New month {room.current_month} started for room {room_id}")
# ========== 7. ОТПРАВЛЯЕМ ВСЕ ОБНОВЛЕНИЯ ==========
try:
# Отправляем обновление месяца и фазы
socketio.emit('game_phase_changed', {
'room': room.code,
'phase': 'action',
'phase_end': game_state.phase_end.isoformat(),
'month': room.current_month,
'total_months': room.total_months,
'message': f'📅 Месяц {room.current_month}/{room.total_months} начался!'
}, room=room.code)
# Отправляем отдельное событие обновления месяца
socketio.emit('month_updated', {
'room': room.code,
'month': room.current_month,
'total_months': room.total_months,
'message': f'Месяц {room.current_month}/{room.total_months}'
}, room=room.code)
# Отправляем обновление капиталов всех игроков
for player in players:
socketio.emit('player_capital_updated', {
'room': room.code,
'user_id': player.user_id,
'capital': player.capital,
'month': room.current_month,
'profit': player.capital - room.start_capital
}, room=room.code)
print(f"📨 Month update events sent for room {room_id}, new month: {room.current_month}")
except Exception as e:
print(f"WebSocket error: {str(e)}")
except Exception as e:
print(f"Error ending month: {str(e)}")
import traceback
traceback.print_exc()
db.session.rollback()
@app.route('/api/game/<room_code>/leaderboard')
@login_required
def get_game_leaderboard(room_code):
"""Получение таблицы лидеров"""
room = GameRoom.query.filter_by(code=room_code).first_or_404()
# Проверяем, что игрок в комнате
GamePlayer.query.filter_by(
user_id=current_user.id,
room_id=room.id
).first_or_404()
# Получаем всех игроков
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 'Игрок',
'capital': player.capital,
'ability': player.ability,
'is_admin': player.is_admin
})
return jsonify({
'success': True,
'players': players_data
})
# WebSocket обработчики для игры
@socketio.on('join_game_room')
def handle_join_game_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 game room {room_code}')
emit('player_joined_game', {
'user_id': current_user.id,
'username': current_user.username,
'timestamp': datetime.utcnow().isoformat()
}, room=room_code, include_self=False)
@socketio.on('leave_game_room')
def handle_leave_game_room(data):
"""Выход из игровой комнаты"""
room_code = data.get('room')
reason = data.get('reason', 'left')
if room_code and current_user.is_authenticated:
leave_room(room_code)
logger.info(f'User {current_user.username} left game room {room_code} (reason: {reason})')
emit('player_left_game', {
'user_id': current_user.id,
'username': current_user.username,
'reason': reason,
'timestamp': datetime.utcnow().isoformat()
}, room=room_code, include_self=False)
@socketio.on('game_chat_message')
def handle_game_chat_message(data):
"""Обработка сообщений в игровом чате"""
room_code = data.get('room')
message = data.get('message', '').strip()
if message and room_code and current_user.is_authenticated:
emit('game_chat_message', {
'user_id': current_user.id,
'username': current_user.username,
'message': message,
'timestamp': datetime.utcnow().isoformat()
}, room=room_code)
@socketio.on('player_bought_asset')
def handle_player_bought_asset(data):
"""Игрок купил актив"""
room_code = data.get('room')
if room_code:
emit('player_action', {
'player_id': current_user.id,
'player_name': current_user.username,
'action': 'buy',
'asset_id': data.get('asset_id'),
'quantity': data.get('quantity'),
'price': data.get('price'),
'timestamp': datetime.utcnow().isoformat()
}, room=room_code, include_self=False)
@socketio.on('player_used_ability')
def handle_player_used_ability(data):
"""Игрок использовал способность"""
room_code = data.get('room')
if room_code:
emit('player_action', {
'player_id': current_user.id,
'player_name': current_user.username,
'action': 'ability',
'ability': data.get('ability'),
'timestamp': datetime.utcnow().isoformat()
}, room=room_code, include_self=False)
# --- ТОЧКА ВХОДА ---
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)