From 3bf1598927baa059e22552428976fc4f1b654cca Mon Sep 17 00:00:00 2001 From: Kavalar Date: Mon, 20 Apr 2026 14:39:59 +0300 Subject: [PATCH] 20.04.26 --- README.md | 5 +- app.py | 1033 +++++++++-- game_balance/game_config.py | 6 +- templates/game.html | 3449 ++++++++++++++++++++++++----------- templates/lobby.html | 17 +- 5 files changed, 3235 insertions(+), 1275 deletions(-) diff --git a/README.md b/README.md index f5b1b58..f39fddb 100644 --- a/README.md +++ b/README.md @@ -13,5 +13,8 @@ flask create-demo-rooms # 5. Валидация баланса flask validate-balance -# 6. Запустите сервер +# 6. Проверка прибыльности +flask test-income + +# 7. Запустите сервер python app.py \ No newline at end of file diff --git a/app.py b/app.py index e431116..3e4318b 100644 --- a/app.py +++ b/app.py @@ -66,9 +66,12 @@ class GamePlayer(db.Model): # Игровые данные 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='[]') # JSON с активами - position = db.Column(db.Integer, default=0) # Позиция в рейтинге + 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): @@ -108,11 +111,11 @@ 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 с историей + 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) @@ -472,6 +475,7 @@ def create_room(): 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, @@ -507,12 +511,13 @@ def create_room(): game_state = GameState( room_id=room.id, - phase='waiting', # Не 'action', а 'waiting' до начала игры - phase_end=None, # Без времени до начала игры + phase=None, # Не 'waiting', а None + phase_end=None, market_data=json.dumps(market_data) ) db.session.add(game_state) + db.session.commit() # ========== ДОБАВЛЯЕМ ИНВЕНТАРЬ ЗДЕСЬ ========== # Создаем инвентарь для ограниченных активов @@ -633,7 +638,7 @@ def start_room_game(room_code): return jsonify({'error': 'Только администратор может начать игру'}), 403 if room.status != 'waiting': - return jsonify({'error': 'Игра уже начата или завершена'}), 400 + return jsonify({'error': f'Игра уже начата или завершена. Текущий статус: {room.status}'}), 400 # Проверяем минимальное количество игроков player_count = GamePlayer.query.filter_by(room_id=room.id).count() @@ -642,14 +647,15 @@ def start_room_game(room_code): # Меняем статус комнаты 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: - # Если баланс не загружается, используем значение по умолчанию + except Exception as e: + print(f"Error loading balance: {str(e)}") phase_duration = 120 # Создаем или обновляем состояние игры @@ -659,16 +665,41 @@ def start_room_game(room_code): balance = BalanceConfig.load_balance_mode(room.balance_mode) assets_config = balance.get_assets_config() economy_config = balance.get_economy_config() - except: + 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': {}} + 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=datetime.utcnow() + timedelta(seconds=phase_duration), + phase_end=phase_end_time, market_data=json.dumps({ 'initialized': True, 'assets': assets_config, @@ -678,21 +709,41 @@ def start_room_game(room_code): }) ) 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 - socketio.emit('game_started', { - 'room': room.code, - 'message': 'Игра началась!', - 'phase': 'action', - 'phase_end': room.game_state.phase_end.isoformat() if room.game_state and room.game_state.phase_end else None - }, room=room.code) + 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}) @@ -1154,7 +1205,8 @@ def get_game_assets(room_code): return jsonify({ 'success': True, 'assets': result, - 'categories': categories + 'categories': categories, + 'player_capital': player.capital # Добавляем капитал игрока }) @@ -1259,7 +1311,10 @@ def format_currency_filter(value): """Форматирует число как валюту""" if value is None: return "0 ₽" - return f"{value:,.0f} ₽".replace(",", " ") + try: + return f"{value:,.0f} ₽".replace(",", " ") + except: + return f"{int(value)} ₽" # --- WEBSOCKET ОБРАБОТЧИКИ --- @@ -1663,6 +1718,20 @@ def validate_balance_command(): 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 @@ -1724,6 +1793,116 @@ def get_ability_description(ability_code): } 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(): @@ -1754,28 +1933,38 @@ def game_page(room_code): # Получаем состояние игры game_state = room.game_state 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) + flash('Состояние игры не найдено', 'error') + return redirect(url_for('lobby', room_code=room_code)) - 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) + # Если фаза не установлена, устанавливаем 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() @@ -1807,7 +1996,7 @@ def game_page(room_code): # API эндпоинты для игры @app.route('/api/game//state') @login_required -def get_game_state_api(room_code): # Переименовано, чтобы не конфликтовать +def get_game_state_api(room_code): """Получение текущего состояния игры""" room = GameRoom.query.filter_by(code=room_code).first_or_404() @@ -1818,10 +2007,24 @@ def get_game_state_api(room_code): # Переименовано, чтобы н 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 if game_state else 'waiting', - 'phase_end': game_state.phase_end.isoformat() if game_state and game_state.phase_end else None, + 'phase': game_state.phase, + 'phase_end': phase_end_iso, 'current_month': room.current_month, 'total_months': room.total_months, 'player_capital': player.capital, @@ -1899,7 +2102,7 @@ def buy_asset(room_code): # Проверяем достаточно ли средств if player.capital < total_cost: - return jsonify({'success': False, 'error': f'Недостаточно средств. Нужно: {format_currency(total_cost)}'}), 400 + return jsonify({'success': False, 'error': f'Недостаточно средств. Нужно: {format_currency_filter(total_cost)}'}), 400 # Проверяем лимиты на владение if asset_config.get('max_per_player'): @@ -1916,14 +2119,16 @@ def buy_asset(room_code): '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) - max_allowed = asset_config.get('max_per_player', float('inf')) - if max_allowed != float('inf'): + # Проверяем только если есть лимит на игрока + 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, @@ -2000,7 +2205,7 @@ def buy_asset(room_code): 'price': current_price, 'total': total_cost, 'remaining': inventory.remaining_quantity if inventory else None, - 'message': f'Куплено {quantity} {asset_config["name"]} за {format_currency(total_cost)}' + 'message': f'Куплено {quantity} {asset_config["name"]} за {format_currency_filter(total_cost)}' }) @@ -2028,8 +2233,16 @@ def sell_asset(room_code): return jsonify({'success': False, 'error': 'Не указан актив или количество'}), 400 # Загружаем баланс комнаты - balance = BalanceConfig.load_balance_mode(room.balance_mode) - assets_config = balance.get_assets_config() + 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) @@ -2057,9 +2270,10 @@ def sell_asset(room_code): # Получаем текущую цену 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 @@ -2067,25 +2281,49 @@ def sell_asset(room_code): 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 game_state = room.game_state if game_state: - 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']) + 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['purchase_price'] + current_price = asset_to_sell.get('purchase_price', asset_config['base_price']) # Рассчитываем выручку revenue = current_price * quantity # Комиссия за продажу - economy_config = balance.get_economy_config() - transaction_fee = economy_config.get('TAX_SYSTEM', {}).get('transaction_fee', 0.01) + 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 @@ -2103,7 +2341,7 @@ def sell_asset(room_code): player.assets = json.dumps(player_assets) - # 3. Начисляем деньги + # 3. Начисляем деньги (после вычета комиссии) player.capital += revenue_after_fee # 4. Сохраняем транзакцию @@ -2121,19 +2359,30 @@ def sell_asset(room_code): db.session.commit() # 5. Отправляем WebSocket уведомление - 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) + 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, @@ -2143,7 +2392,12 @@ def sell_asset(room_code): 'price': current_price, 'total': revenue_after_fee, 'fee': fee, - 'message': f'Продано {quantity} {asset_config["name"]} за {format_currency(revenue_after_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)}' }) @@ -2186,137 +2440,576 @@ def end_action_phase(room_code): if not player: return jsonify({'success': False, 'error': 'Только администратор может завершать фазы'}), 403 - if not room.game_state or room.game_state.phase != 'action': - return jsonify({'success': False, 'error': 'Сейчас не фаза действий'}), 400 + 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 = datetime.utcnow() + timedelta(seconds=30) # 30 секунд на реакцию рынка + game_state.phase_end = phase_end_time + game_state.updated_at = datetime.utcnow() db.session.commit() - # Отправляем WebSocket событие - socketio.emit('game_phase_changed', { - 'room': room.code, - 'phase': 'market', - 'phase_end': game_state.phase_end.isoformat(), - 'message': 'Фаза действий завершена, начинается реакция рынка' - }, room=room.code) + # Проверяем что 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()}") - # Запускаем расчет рынка - socketio.start_background_task(calculate_and_update_market, room.id) + # Отправляем 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 - time.sleep(5) + # Создаем контекст приложения для работы с БД в фоновом потоке + with app.app_context(): + try: + # Ждем 5 секунд перед расчетом + time.sleep(5) - room = GameRoom.query.get(room_id) - if not room or room.status != 'playing': - return + # Получаем комнату + room = GameRoom.query.get(room_id) + if not room or room.status != 'playing': + print(f"Room {room_id} not found or not playing") + return - # Загружаем баланс комнаты - balance = BalanceConfig.load_balance_mode(room.balance_mode) - assets_config = balance.get_assets_config() - economy_config = balance.get_economy_config() + print( + f"Calculating market for room {room_id}, current phase: {room.game_state.phase if room.game_state else 'None'}") - game_state = room.game_state - if not game_state or game_state.phase != 'market': - return + # Загружаем баланс комнаты + 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() - # Получаем игроков - players = GamePlayer.query.filter_by(room_id=room_id).all() + game_state = room.game_state + if not game_state: + print(f"Game state not found for room {room_id}") + return - # Получаем текущие данные рынка - market_data = json.loads(game_state.market_data) if game_state.market_data else {} - assets = market_data.get('assets', {}) + if game_state.phase != 'market': + print(f"Wrong phase for market calculation: {game_state.phase}") + return - # Рассчитываем спрос по каждому активу - for asset_id, asset_data in assets.items(): - if asset_id not in assets_config: - continue + # Получаем игроков + players = GamePlayer.query.filter_by(room_id=room_id).all() - # Считаем общее количество активов у игроков - total_held = 0 - for player in players: - 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) + # Получаем текущие данные рынка + try: + market_data = json.loads(game_state.market_data) if game_state.market_data else {} + except: + market_data = {} - # Коэффициент спроса - total_supply = asset_data.get('total_quantity') - if total_supply is not None: - demand_factor = total_held / total_supply if total_supply > 0 else 0 - else: - demand_factor = total_held / (len(players) * 100) if players else 0 # Для безлимитных + assets = market_data.get('assets', {}) - demand_factor = min(demand_factor, 1.0) # Ограничиваем + # Получаем инвентарь для ограниченных активов + inventories = {} + inventory_list = AssetInventory.query.filter_by(room_id=room_id).all() + for inv in inventory_list: + inventories[inv.asset_id] = inv - # Рассчитываем новую цену - new_price = AssetsConfig.calculate_price( - asset_data, - demand_factor, - room.current_month, - len(players) - ) + # Рассчитываем спрос по каждому активу + for asset_id, asset_data in assets.items(): + if asset_id not in assets_config: + continue - asset_data['price'] = new_price + # Получаем конфигурацию актива + asset_config = assets_config.get(asset_id, {}) - # Применяем корреляции - ИСПРАВЛЕНО - correlations = economy_config.get('ASSET_CORRELATIONS', {}) - for corr_key, corr_value in correlations.items(): - # Разбираем ключ вида "asset1:asset2" - 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) - change2 = (price2 / base2) - 1 if base2 > 0 else 0 - assets[asset1]['price'] *= (1 + corr_value * change2) + # Сохраняем предыдущую цену для расчета изменения + asset_data['previous_price'] = asset_data.get('price', asset_config.get('base_price', 1000)) - # Обновляем данные рынка - market_data['assets'] = assets - market_data['last_update'] = datetime.utcnow().isoformat() - game_state.market_data = json.dumps(market_data) + # Считаем общее количество активов у игроков + 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 - db.session.commit() + # Получаем инвентарь для этого актива + inventory = inventories.get(asset_id) - # Отправляем обновление - socketio.emit('market_updated', { - 'room': room.code, - 'assets': assets, - 'timestamp': datetime.utcnow().isoformat() - }, room=room.code) + # Коэффициент спроса + 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 - # Переходим к следующей фазе - time.sleep(25) + demand_factor = min(demand_factor, 1.0) # Ограничиваем - 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' + # Рассчитываем новую цену + 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) - # Получаем длительность фазы из конфига - game_config = balance.get_game_config() - phase_duration = game_config.get('PHASE_DURATIONS', {}).get('event', 30) + # Обновляем цену в активе + asset_data['price'] = new_price - game_state.phase_end = datetime.utcnow() + timedelta(seconds=phase_duration) + # Обновляем цену в инвентаре + if inventory: + inventory.current_price = new_price + inventory.updated_at = datetime.utcnow() - db.session.commit() + # Применяем корреляции + 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)}") - socketio.emit('game_phase_changed', { - 'room': room.code, - 'phase': 'event', - 'phase_end': game_state.phase_end.isoformat(), - 'message': 'Реакция рынка завершена, начинаются случайные события' - }, room=room.code) + # Обновляем данные рынка + 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//leaderboard') diff --git a/game_balance/game_config.py b/game_balance/game_config.py index 05065c6..18abd7d 100644 --- a/game_balance/game_config.py +++ b/game_balance/game_config.py @@ -15,9 +15,9 @@ class GameConfig: # Тайминги фаз (секунды) PHASE_DURATIONS = { 'action': 120, - 'market': 30, - 'event': 30, - 'results': 45 + 'market': 20, + 'event': 20, + 'results': 25 } # Режимы скорости diff --git a/templates/game.html b/templates/game.html index 5d3a33e..0111993 100644 --- a/templates/game.html +++ b/templates/game.html @@ -4,11 +4,9 @@ {% block content %}
- +
- - ← - +
🎮 {{ room.name }} @@ -16,9 +14,9 @@
- {{ game_state.phase|capitalize }}: 2:00 + Фаза действий
-
+
Месяц {{ room.current_month }}/{{ room.total_months }}
@@ -49,7 +47,7 @@
- +
@@ -83,6 +81,30 @@
+
+

💼 Инвестиционные возможности

+

+ У вас есть {{ player.capital|format_currency }} для инвестиций. +

+ + + + + +
+
+
Всего активов
+
0
+
+
+
Диверсификация
+
0
+
+
+

💼 Инвестиционные возможности

@@ -91,19 +113,11 @@ Выберите активы для покупки или продажи.

- -
- -
- Загрузка активов... -
-
- - + - -
+ +
@@ -131,32 +146,32 @@

📈 Ваши активы

{% if player.assets and player.assets != '[]' %} - {% set assets = player.assets|from_json %} - {% for asset in assets %} -
-
-
- {{ asset_names.get(asset.id, asset.id)|capitalize }} -
-
- {{ asset.quantity }} шт. • - Куплено за: {{ asset.purchase_price|format_currency }} -
-
-
- -
+ {% set assets = player.assets|from_json %} + {% for asset in assets %} +
+
+
+ {{ asset_names.get(asset.id, asset.id)|capitalize }} +
+
+ {{ asset.quantity }} шт. • + Куплено за: {{ asset.purchase_price|format_currency }}
- {% endfor %} - {% else %} -
-
📭
-

У вас пока нет активов

-

Купите активы в списке выше

+
+ +
+
+ {% endfor %} + {% else %} +
+
📭
+

У вас пока нет активов

+

Купите активы в списке выше

+
{% endif %}
@@ -169,7 +184,7 @@

Цены активов после действий игроков

- +
@@ -197,7 +212,7 @@

События, которые повлияли на рынок в этом месяце

- +
@@ -231,7 +246,7 @@

🏆 Итоги месяца {{ room.current_month }}

- +
@@ -242,14 +257,14 @@

📊 Лидерборд

- +
Загрузка лидерборда...
- + @@ -320,7 +335,7 @@ + + + {% endblock %} {% block scripts %} {% endblock %} \ No newline at end of file diff --git a/templates/lobby.html b/templates/lobby.html index 904f1db..72f32f6 100644 --- a/templates/lobby.html +++ b/templates/lobby.html @@ -612,8 +612,8 @@ return; } - const readyPlayers = {{ players|selectattr('is_ready')|list|length }}; - const totalPlayers = {{ players|length }}; + const readyPlayers = document.querySelectorAll('.player-ready.ready').length; + const totalPlayers = document.querySelectorAll('.player-item').length; if (totalPlayers < 2) { showNotification('❌ Нужно минимум 2 игрока для начала игры', 'error'); @@ -626,6 +626,8 @@ } } + showNotification('🔄 Запуск игры...', 'info'); + fetch('/room/{{ room.code }}/start', { method: 'POST', headers: { @@ -633,7 +635,12 @@ 'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}' } }) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + return response.json().then(err => Promise.reject(err)); + } + return response.json(); + }) .then(data => { if (data.success) { showNotification('✅ Игра начинается!', 'success'); @@ -647,8 +654,8 @@ } }) .catch(error => { - console.error('Error:', error); - showNotification('❌ Ошибка при запуске игры', 'error'); + console.error('Error starting game:', error); + showNotification('❌ Ошибка при запуске игры: ' + (error.error || error.message || 'Неизвестная ошибка'), 'error'); }); }