This commit is contained in:
2026-02-11 17:44:26 +03:00
parent baeaea69f8
commit 1388a5fd6b
17 changed files with 2188 additions and 177 deletions

736
app.py
View File

@@ -10,6 +10,14 @@ 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__)
@@ -40,6 +48,8 @@ class GameRoom(db.Model):
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)
@@ -106,6 +116,42 @@ class GameState(db.Model):
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():
@@ -400,6 +446,7 @@ def create_room():
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'
@@ -408,14 +455,13 @@ def create_room():
if not room_name:
return jsonify({'error': 'Введите название комнаты'}), 400
# Генерируем уникальный код комнаты
import string
import random
def generate_room_code():
chars = string.ascii_uppercase + string.digits
return ''.join(random.choice(chars) for _ in range(6))
# Загружаем баланс
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()
# Создаем комнату
@@ -425,6 +471,7 @@ def create_room():
creator_id=current_user.id,
total_months=total_months,
start_capital=start_capital,
balance_mode=balance_mode,
settings=json.dumps({
'allow_loans': allow_loans,
'allow_black_market': allow_black_market,
@@ -435,41 +482,57 @@ def create_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([
'crisis_investor', 'lobbyist', 'predictor',
'golden_pillow', 'shadow_accountant', 'credit_magnate'
])
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,
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()
})
phase='waiting', # Не 'action', а 'waiting' до начала игры
phase_end=None, # Без времени до начала игры
market_data=json.dumps(market_data)
)
db.session.add(game_state)
# ========== ДОБАВЛЯЕМ ИНВЕНТАРЬ ЗДЕСЬ ==========
# Создаем инвентарь для ограниченных активов
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,
@@ -580,24 +643,46 @@ def start_room_game(room_code):
# Меняем статус комнаты
room.status = 'playing'
# Создаем начальное состояние игры если его нет
# Получаем длительность фазы из баланса
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
# Создаем или обновляем состояние игры
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:
from game_balance.assets_config import AssetsConfig
assets_config = AssetsConfig.ASSETS
economy_config = {'ASSET_CORRELATIONS': {}}
# Создаем новое состояние
game_state = GameState(
room_id=room.id,
phase='action',
phase_end=datetime.utcnow() + timedelta(seconds=120), # 2 минуты на действия
phase_end=datetime.utcnow() + timedelta(seconds=phase_duration),
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()
'assets': assets_config,
'balance_mode': room.balance_mode,
'last_update': datetime.utcnow().isoformat(),
'correlations': economy_config.get('ASSET_CORRELATIONS', {})
})
)
db.session.add(game_state)
else:
# Обновляем существующее состояние
room.game_state.phase = 'action'
room.game_state.phase_end = datetime.utcnow() + timedelta(seconds=phase_duration)
room.game_state.updated_at = datetime.utcnow()
db.session.commit()
@@ -606,7 +691,7 @@ def start_room_game(room_code):
'room': room.code,
'message': 'Игра началась!',
'phase': 'action',
'phase_end': room.game_state.phase_end.isoformat() if room.game_state else None
'phase_end': room.game_state.phase_end.isoformat() if room.game_state and room.game_state.phase_end else None
}, room=room.code)
return jsonify({'success': True})
@@ -981,15 +1066,131 @@ def exit_lobby(room_code):
@app.route('/api/game/<room_code>/assets')
@login_required
def get_assets(room_code):
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', {})
if not game_state:
return jsonify({'error': 'Game state not found'}), 404
# Получаем инвентарь для ограниченных активов
inventories = {}
inventory_list = AssetInventory.query.filter_by(room_id=room.id).all()
for inv in inventory_list:
inventories[inv.asset_id] = inv
market_data = json.loads(game_state.market_data)
return jsonify(market_data)
# Получаем активы игрока
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
})
@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'])
@@ -1433,6 +1634,35 @@ def reset_db():
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❌ Обнаружены ошибки в балансе")
# --- ОБРАБОТЧИКИ ОШИБОК ---
from datetime import datetime
@@ -1506,7 +1736,7 @@ def utility_processor():
# Логика игры
@app.route('/game/<room_code>')
@login_required
def game(room_code):
def game_page(room_code):
"""Страница игры"""
room = GameRoom.query.filter_by(code=room_code).first_or_404()
@@ -1525,17 +1755,22 @@ def game(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)
game_state = GameState(
room_id=room.id,
phase='action',
phase_end=datetime.utcnow() + timedelta(seconds=phase_duration),
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()
'assets': assets_config,
'balance_mode': room.balance_mode,
'last_update': datetime.utcnow().isoformat(),
'correlations': economy_config.get('ASSET_CORRELATIONS', {})
})
)
db.session.add(game_state)
@@ -1546,10 +1781,19 @@ def game(room_code):
# Словарь названий активов для шаблона
asset_names = {
'gov_bonds': 'Гособлигации',
'stock_gazprom': 'Акции Газпрома',
'real_estate': 'Недвижимость',
'stock_sberbank': 'Акции Сбербанка',
'stock_yandex': 'Акции Яндекса',
'apartment_small': 'Небольшая квартира',
'apartment_elite': 'Элитная квартира',
'bitcoin': 'Биткоин',
'oil': 'Нефть'
'oil': 'Нефть Brent',
'natural_gas': 'Природный газ',
'coffee_shop': 'Кофейня',
'it_startup': 'IT-стартап',
'shopping_mall': 'Торговый центр',
'oil_field': 'Нефтяное месторождение'
}
return render_template('game.html',
@@ -1563,7 +1807,7 @@ def game(room_code):
# API эндпоинты для игры
@app.route('/api/game/<room_code>/state')
@login_required
def get_game_state(room_code):
def get_game_state_api(room_code): # Переименовано, чтобы не конфликтовать
"""Получение текущего состояния игры"""
room = GameRoom.query.filter_by(code=room_code).first_or_404()
@@ -1576,7 +1820,7 @@ def get_game_state(room_code):
return jsonify({
'success': True,
'phase': game_state.phase if game_state else 'action',
'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,
'current_month': room.current_month,
'total_months': room.total_months,
@@ -1585,36 +1829,13 @@ def get_game_state(room_code):
})
@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()
# Проверяем, что игрок в комнате
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
market_data = json.loads(game_state.market_data)
return jsonify({
'success': True,
'assets': market_data.get('assets', {})
})
@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
@@ -1622,42 +1843,164 @@ def buy_asset(room_code):
# Проверяем, что сейчас фаза действий
if room.game_state and room.game_state.phase != 'action':
return jsonify({'success': False, 'error': 'Не фаза действий'}), 400
return jsonify({'success': False, 'error': 'Покупка доступна только в фазу действий'}), 400
data = request.json
asset_id = data.get('asset_id')
quantity = data.get('quantity', 1)
price = data.get('price')
quantity = float(data.get('quantity', 1))
if not asset_id or not price:
return jsonify({'success': False, 'error': 'Не указан актив или цена'}), 400
if not asset_id or quantity <= 0:
return jsonify({'success': False, 'error': 'Не указан актив или количество'}), 400
total_cost = price * quantity
# Загружаем баланс комнаты
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': 'Недостаточно средств'}), 400
return jsonify({'success': False, 'error': f'Недостаточно средств. Нужно: {format_currency(total_cost)}'}), 400
# Получаем текущие активы игрока
assets = json.loads(player.assets) if player.assets else []
# Проверяем лимиты на владение
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']
# Добавляем новый актив
assets.append({
'id': asset_id,
'quantity': quantity,
'purchase_price': price,
'timestamp': datetime.utcnow().isoformat()
})
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)
max_allowed = asset_config.get('max_per_player', float('inf'))
if max_allowed != float('inf'):
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
player.assets = json.dumps(assets)
# 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,
'message': f'Куплено {quantity} {asset_id}'
'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(total_cost)}'
})
@@ -1667,6 +2010,7 @@ 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
@@ -1674,55 +2018,132 @@ def sell_asset(room_code):
# Проверяем, что сейчас фаза действий
if room.game_state and room.game_state.phase != 'action':
return jsonify({'success': False, 'error': 'Не фаза действий'}), 400
return jsonify({'success': False, 'error': 'Продажа доступна только в фазу действий'}), 400
data = request.json
asset_id = data.get('asset_id')
quantity = data.get('quantity', 1)
quantity = float(data.get('quantity', 1))
if not asset_id:
return jsonify({'success': False, 'error': 'Не указан актив'}), 400
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
# Получаем текущие активы игрока
assets = json.loads(player.assets) if player.assets else []
player_assets = json.loads(player.assets) if player.assets else []
# Ищем актив для продажи
asset_to_sell = None
for asset in assets:
if asset['id'] == asset_id and asset['quantity'] >= quantity:
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': 'Недостаточно активов для продажи'}), 400
return jsonify({'success': False, 'error': f'У вас нет {asset_config["name"]}'}), 400
# Получаем текущую цену актива
game_state = room.game_state
if not game_state:
return jsonify({'success': False, 'error': 'Состояние игры не найдено'}), 404
if asset_to_sell['quantity'] < quantity:
return jsonify({
'success': False,
'error': f'У вас только {asset_to_sell["quantity"]} {asset_config["name"]}'
}), 400
market_data = json.loads(game_state.market_data)
current_price = market_data.get('assets', {}).get(asset_id, {}).get('price', asset_to_sell['purchase_price'])
# Получаем текущую цену
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
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'])
# Если цену не нашли, используем цену покупки
if current_price is None:
current_price = asset_to_sell['purchase_price']
# Рассчитываем выручку
revenue = current_price * quantity
# Обновляем активы игрока
# Комиссия за продажу
economy_config = balance.get_economy_config()
transaction_fee = economy_config.get('TAX_SYSTEM', {}).get('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:
assets.remove(asset_to_sell)
player_assets.remove(asset_to_sell)
# Обновляем капитал
player.capital += revenue
player.assets = json.dumps(assets)
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 уведомление
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)
return jsonify({
'success': True,
'new_capital': player.capital,
'revenue': revenue,
'message': f'Продано {quantity} {asset_id}'
'asset_name': asset_config['name'],
'quantity': quantity,
'price': current_price,
'total': revenue_after_fee,
'fee': fee,
'message': f'Продано {quantity} {asset_config["name"]} за {format_currency(revenue_after_fee)}'
})
@@ -1790,80 +2211,103 @@ def end_action_phase(room_code):
def calculate_and_update_market(room_id):
"""Расчет реакции рынка (в фоновом потоке)"""
"""Расчет реакции рынка с использованием баланса"""
import time
import random
# Ждем 5 секунд перед расчетом
time.sleep(5)
room = GameRoom.query.get(room_id)
if not room or room.status != 'playing':
return
# Загружаем баланс комнаты
balance = BalanceConfig.load_balance_mode(room.balance_mode)
assets_config = balance.get_assets_config()
economy_config = balance.get_economy_config()
game_state = room.game_state
if not game_state or game_state.phase != 'market':
return
# Получаем всех игроков
# Получаем игроков
players = GamePlayer.query.filter_by(room_id=room_id).all()
# Получаем текущие данные рынка
market_data = json.loads(game_state.market_data) if game_state.market_data else {}
assets = market_data.get('assets', {})
# Базовые цены для расчета перегрева
base_prices = {
'stock_gazprom': 1000,
'real_estate': 3000000,
'bitcoin': 1850000,
'oil': 5000
}
# Рассчитываем спрос по каждому активу
for asset_id, asset_data in assets.items():
if asset_id not in assets_config:
continue
# Считаем спрос по каждому активу
for asset_id in assets.keys():
demand = 0
# Считаем общее количество активов у игроков
total_held = 0
for player in players:
player_assets = json.loads(player.assets) if player.assets else []
for player_asset in player_assets:
if player_asset.get('id') == asset_id:
demand += player_asset.get('quantity', 0)
for pa in player_assets:
if pa.get('id') == asset_id:
total_held += pa.get('quantity', 0)
# Формула влияния спроса
total_players = len(players)
if total_players > 0 and asset_id in assets:
volatility = assets[asset_id].get('volatility', 0.2)
price_change = volatility * (demand / (total_players * 10))
new_price = assets[asset_id]['price'] * (1 + price_change)
# Коэффициент спроса
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 # Для безлимитных
# Эффект перегрева
if base_prices.get(asset_id) and new_price > base_prices[asset_id] * (1 + 2 * volatility):
new_price *= random.uniform(0.7, 0.9)
demand_factor = min(demand_factor, 1.0) # Ограничиваем
assets[asset_id]['price'] = new_price
# Рассчитываем новую цену
new_price = AssetsConfig.calculate_price(
asset_data,
demand_factor,
room.current_month,
len(players)
)
asset_data['price'] = new_price
# Применяем корреляции - ИСПРАВЛЕНО
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)
# Обновляем данные рынка
market_data['assets'] = assets
market_data['last_update'] = datetime.utcnow().isoformat()
game_state.market_data = json.dumps(market_data)
db.session.commit()
# Отправляем обновление через WebSocket
# Отправляем обновление
socketio.emit('market_updated', {
'room': room.code,
'assets': assets,
'timestamp': datetime.utcnow().isoformat()
}, room=room.code)
# Через 25 секунд переходим к событиям
# Переходим к следующей фазе
time.sleep(25)
# Проверяем, что мы все еще в фазе рынка
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'
game_state.phase_end = datetime.utcnow() + timedelta(seconds=30)
# Получаем длительность фазы из конфига
game_config = balance.get_game_config()
phase_duration = game_config.get('PHASE_DURATIONS', {}).get('event', 30)
game_state.phase_end = datetime.utcnow() + timedelta(seconds=phase_duration)
db.session.commit()