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

17
README.md Normal file
View File

@@ -0,0 +1,17 @@
# 1. Удалите старую базу данных
rm app.db
# 2. Создайте новую базу данных
flask init-db
# 3. Создайте тестовых пользователей
flask create-test-users
# 4. Создайте демо-комнаты
flask create-demo-rooms
# 5. Валидация баланса
flask validate-balance
# 6. Запустите сервер
python app.py

728
app.py
View File

@@ -10,6 +10,14 @@ import random
import logging import logging
from werkzeug.security import generate_password_hash, check_password_hash 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) logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -40,6 +48,8 @@ class GameRoom(db.Model):
start_capital = db.Column(db.Integer, default=100000) start_capital = db.Column(db.Integer, default=100000)
settings = db.Column(db.Text, default='{}') # JSON с настройками settings = db.Column(db.Text, default='{}') # JSON с настройками
balance_mode = db.Column(db.String(20), default='standard')
# Связи - убираем lazy='dynamic' для players # Связи - убираем lazy='dynamic' для players
players = db.relationship('GamePlayer', backref='room', lazy='select') # Изменено players = db.relationship('GamePlayer', backref='room', lazy='select') # Изменено
game_state = db.relationship('GameState', backref='room', uselist=False) 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) 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(): def generate_room_code():
@@ -400,6 +446,7 @@ def create_room():
room_name = request.form.get('name', 'Новая комната').strip() room_name = request.form.get('name', 'Новая комната').strip()
total_months = int(request.form.get('total_months', 12)) total_months = int(request.form.get('total_months', 12))
start_capital = int(request.form.get('start_capital', 100000)) 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_loans = request.form.get('allow_loans') == 'on'
allow_black_market = request.form.get('allow_black_market') == 'on' allow_black_market = request.form.get('allow_black_market') == 'on'
private_room = request.form.get('private_room') == 'on' private_room = request.form.get('private_room') == 'on'
@@ -408,14 +455,13 @@ def create_room():
if not room_name: if not room_name:
return jsonify({'error': 'Введите название комнаты'}), 400 return jsonify({'error': 'Введите название комнаты'}), 400
# Генерируем уникальный код комнаты # Загружаем баланс
import string balance = BalanceConfig.load_balance_mode(balance_mode)
import random players_config = balance.get_players_config()
assets_config = balance.get_assets_config()
def generate_room_code(): economy_config = balance.get_economy_config()
chars = string.ascii_uppercase + string.digits
return ''.join(random.choice(chars) for _ in range(6))
# Генерируем код комнаты
room_code = generate_room_code() room_code = generate_room_code()
# Создаем комнату # Создаем комнату
@@ -425,6 +471,7 @@ def create_room():
creator_id=current_user.id, creator_id=current_user.id,
total_months=total_months, total_months=total_months,
start_capital=start_capital, start_capital=start_capital,
balance_mode=balance_mode,
settings=json.dumps({ settings=json.dumps({
'allow_loans': allow_loans, 'allow_loans': allow_loans,
'allow_black_market': allow_black_market, 'allow_black_market': allow_black_market,
@@ -435,41 +482,57 @@ def create_room():
db.session.add(room) db.session.add(room)
db.session.commit() db.session.commit()
# Добавляем создателя как игрока-администратора # Добавляем создателя как игрока
abilities = list(players_config.get('ABILITIES', PlayersConfig.ABILITIES).keys())
player = GamePlayer( player = GamePlayer(
user_id=current_user.id, user_id=current_user.id,
room_id=room.id, room_id=room.id,
is_admin=True, is_admin=True,
is_ready=True, is_ready=True,
capital=start_capital, capital=start_capital,
ability=random.choice([ ability=random.choice(abilities) if abilities else None
'crisis_investor', 'lobbyist', 'predictor',
'golden_pillow', 'shadow_accountant', 'credit_magnate'
])
) )
db.session.add(player) db.session.add(player)
db.session.commit() 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( game_state = GameState(
room_id=room.id, room_id=room.id,
market_data=json.dumps({ phase='waiting', # Не 'action', а 'waiting' до начала игры
'initialized': True, phase_end=None, # Без времени до начала игры
'assets': { market_data=json.dumps(market_data)
'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.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() db.session.commit()
# Отправляем успешный ответ
return jsonify({ return jsonify({
'success': True, 'success': True,
'room_code': room.code, 'room_code': room.code,
@@ -580,24 +643,46 @@ def start_room_game(room_code):
# Меняем статус комнаты # Меняем статус комнаты
room.status = 'playing' 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: 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( game_state = GameState(
room_id=room.id, room_id=room.id,
phase='action', phase='action',
phase_end=datetime.utcnow() + timedelta(seconds=120), # 2 минуты на действия phase_end=datetime.utcnow() + timedelta(seconds=phase_duration),
market_data=json.dumps({ market_data=json.dumps({
'initialized': True, 'initialized': True,
'assets': { 'assets': assets_config,
'stock_gazprom': {'price': 1000, 'volatility': 0.2}, 'balance_mode': room.balance_mode,
'real_estate': {'price': 3000000, 'volatility': 0.1}, 'last_update': datetime.utcnow().isoformat(),
'bitcoin': {'price': 1850000, 'volatility': 0.3}, 'correlations': economy_config.get('ASSET_CORRELATIONS', {})
'oil': {'price': 5000, 'volatility': 0.25}
},
'last_update': datetime.utcnow().isoformat()
}) })
) )
db.session.add(game_state) 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() db.session.commit()
@@ -606,7 +691,7 @@ def start_room_game(room_code):
'room': room.code, 'room': room.code,
'message': 'Игра началась!', 'message': 'Игра началась!',
'phase': 'action', '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) }, room=room.code)
return jsonify({'success': True}) return jsonify({'success': True})
@@ -981,15 +1066,131 @@ def exit_lobby(room_code):
@app.route('/api/game/<room_code>/assets') @app.route('/api/game/<room_code>/assets')
@login_required @login_required
def get_assets(room_code): def get_game_assets(room_code):
"""Получение текущих цен и доступности активов"""
room = GameRoom.query.filter_by(code=room_code).first_or_404() 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 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']) @app.route('/api/game/<room_code>/action', methods=['POST'])
@@ -1433,6 +1634,35 @@ def reset_db():
print('\n🎉 База данных успешно сброшена и заполнена тестовыми данными!') 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 from datetime import datetime
@@ -1506,7 +1736,7 @@ def utility_processor():
# Логика игры # Логика игры
@app.route('/game/<room_code>') @app.route('/game/<room_code>')
@login_required @login_required
def game(room_code): def game_page(room_code):
"""Страница игры""" """Страница игры"""
room = GameRoom.query.filter_by(code=room_code).first_or_404() room = GameRoom.query.filter_by(code=room_code).first_or_404()
@@ -1525,17 +1755,22 @@ def game(room_code):
game_state = room.game_state game_state = room.game_state
if not 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( game_state = GameState(
room_id=room.id, room_id=room.id,
phase='action',
phase_end=datetime.utcnow() + timedelta(seconds=phase_duration),
market_data=json.dumps({ market_data=json.dumps({
'initialized': True, 'initialized': True,
'assets': { 'assets': assets_config,
'stock_gazprom': {'price': 1000, 'volatility': 0.2}, 'balance_mode': room.balance_mode,
'real_estate': {'price': 3000000, 'volatility': 0.1}, 'last_update': datetime.utcnow().isoformat(),
'bitcoin': {'price': 1850000, 'volatility': 0.3}, 'correlations': economy_config.get('ASSET_CORRELATIONS', {})
'oil': {'price': 5000, 'volatility': 0.25}
},
'last_update': datetime.utcnow().isoformat()
}) })
) )
db.session.add(game_state) db.session.add(game_state)
@@ -1546,10 +1781,19 @@ def game(room_code):
# Словарь названий активов для шаблона # Словарь названий активов для шаблона
asset_names = { asset_names = {
'gov_bonds': 'Гособлигации',
'stock_gazprom': 'Акции Газпрома', 'stock_gazprom': 'Акции Газпрома',
'real_estate': 'Недвижимость', 'stock_sberbank': 'Акции Сбербанка',
'stock_yandex': 'Акции Яндекса',
'apartment_small': 'Небольшая квартира',
'apartment_elite': 'Элитная квартира',
'bitcoin': 'Биткоин', 'bitcoin': 'Биткоин',
'oil': 'Нефть' 'oil': 'Нефть Brent',
'natural_gas': 'Природный газ',
'coffee_shop': 'Кофейня',
'it_startup': 'IT-стартап',
'shopping_mall': 'Торговый центр',
'oil_field': 'Нефтяное месторождение'
} }
return render_template('game.html', return render_template('game.html',
@@ -1563,7 +1807,7 @@ def game(room_code):
# API эндпоинты для игры # API эндпоинты для игры
@app.route('/api/game/<room_code>/state') @app.route('/api/game/<room_code>/state')
@login_required @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() room = GameRoom.query.filter_by(code=room_code).first_or_404()
@@ -1576,7 +1820,7 @@ def get_game_state(room_code):
return jsonify({ return jsonify({
'success': True, '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, 'phase_end': game_state.phase_end.isoformat() if game_state and game_state.phase_end else None,
'current_month': room.current_month, 'current_month': room.current_month,
'total_months': room.total_months, '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']) @app.route('/api/game/<room_code>/buy', methods=['POST'])
@login_required @login_required
def buy_asset(room_code): def buy_asset(room_code):
"""Покупка актива""" """Покупка актива"""
room = GameRoom.query.filter_by(code=room_code).first_or_404() room = GameRoom.query.filter_by(code=room_code).first_or_404()
# Проверяем, что игрок в комнате
player = GamePlayer.query.filter_by( player = GamePlayer.query.filter_by(
user_id=current_user.id, user_id=current_user.id,
room_id=room.id room_id=room.id
@@ -1622,42 +1843,164 @@ def buy_asset(room_code):
# Проверяем, что сейчас фаза действий # Проверяем, что сейчас фаза действий
if room.game_state and room.game_state.phase != 'action': 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 data = request.json
asset_id = data.get('asset_id') asset_id = data.get('asset_id')
quantity = data.get('quantity', 1) quantity = float(data.get('quantity', 1))
price = data.get('price')
if not asset_id or not price: if not asset_id or quantity <= 0:
return jsonify({'success': False, 'error': 'Не указан актив или цена'}), 400 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: 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']
# Добавляем новый актив if current_quantity + quantity > asset_config['max_per_player']:
assets.append({ 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, 'id': asset_id,
'quantity': quantity, 'quantity': quantity,
'purchase_price': price, 'purchase_price': current_price,
'timestamp': datetime.utcnow().isoformat() 'purchase_date': datetime.utcnow().isoformat()
}) })
# Обновляем капитал и активы игрока player.assets = json.dumps(player_assets)
# 3. Списываем деньги
player.capital -= total_cost 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() 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({ return jsonify({
'success': True, 'success': True,
'new_capital': player.capital, '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() room = GameRoom.query.filter_by(code=room_code).first_or_404()
# Проверяем, что игрок в комнате
player = GamePlayer.query.filter_by( player = GamePlayer.query.filter_by(
user_id=current_user.id, user_id=current_user.id,
room_id=room.id room_id=room.id
@@ -1674,55 +2018,132 @@ def sell_asset(room_code):
# Проверяем, что сейчас фаза действий # Проверяем, что сейчас фаза действий
if room.game_state and room.game_state.phase != 'action': 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 data = request.json
asset_id = data.get('asset_id') asset_id = data.get('asset_id')
quantity = data.get('quantity', 1) quantity = float(data.get('quantity', 1))
if not asset_id: if not asset_id or quantity <= 0:
return jsonify({'success': False, 'error': 'Не указан актив'}), 400 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 asset_to_sell = None
for asset in assets: for asset in player_assets:
if asset['id'] == asset_id and asset['quantity'] >= quantity: if asset['id'] == asset_id:
asset_to_sell = asset asset_to_sell = asset
break break
if not asset_to_sell: if not asset_to_sell:
return jsonify({'success': False, 'error': 'Недостаточно активов для продажи'}), 400 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
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 game_state = room.game_state
if not game_state: if game_state:
return jsonify({'success': False, 'error': 'Состояние игры не найдено'}), 404
market_data = json.loads(game_state.market_data) market_data = json.loads(game_state.market_data)
current_price = market_data.get('assets', {}).get(asset_id, {}).get('price', asset_to_sell['purchase_price']) 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 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 asset_to_sell['quantity'] -= quantity
if asset_to_sell['quantity'] <= 0: if asset_to_sell['quantity'] <= 0:
assets.remove(asset_to_sell) player_assets.remove(asset_to_sell)
# Обновляем капитал player.assets = json.dumps(player_assets)
player.capital += revenue
player.assets = json.dumps(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() 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({ return jsonify({
'success': True, 'success': True,
'new_capital': player.capital, 'new_capital': player.capital,
'revenue': revenue, 'asset_name': asset_config['name'],
'message': f'Продано {quantity} {asset_id}' '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): def calculate_and_update_market(room_id):
"""Расчет реакции рынка (в фоновом потоке)""" """Расчет реакции рынка с использованием баланса"""
import time import time
import random
# Ждем 5 секунд перед расчетом
time.sleep(5) time.sleep(5)
room = GameRoom.query.get(room_id) room = GameRoom.query.get(room_id)
if not room or room.status != 'playing': if not room or room.status != 'playing':
return 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 game_state = room.game_state
if not game_state or game_state.phase != 'market': if not game_state or game_state.phase != 'market':
return return
# Получаем всех игроков # Получаем игроков
players = GamePlayer.query.filter_by(room_id=room_id).all() players = GamePlayer.query.filter_by(room_id=room_id).all()
# Получаем текущие данные рынка # Получаем текущие данные рынка
market_data = json.loads(game_state.market_data) if game_state.market_data else {} market_data = json.loads(game_state.market_data) if game_state.market_data else {}
assets = market_data.get('assets', {}) assets = market_data.get('assets', {})
# Базовые цены для расчета перегрева # Рассчитываем спрос по каждому активу
base_prices = { for asset_id, asset_data in assets.items():
'stock_gazprom': 1000, if asset_id not in assets_config:
'real_estate': 3000000, continue
'bitcoin': 1850000,
'oil': 5000
}
# Считаем спрос по каждому активу # Считаем общее количество активов у игроков
for asset_id in assets.keys(): total_held = 0
demand = 0
for player in players: for player in players:
player_assets = json.loads(player.assets) if player.assets else [] player_assets = json.loads(player.assets) if player.assets else []
for player_asset in player_assets: for pa in player_assets:
if player_asset.get('id') == asset_id: if pa.get('id') == asset_id:
demand += player_asset.get('quantity', 0) total_held += pa.get('quantity', 0)
# Формула влияния спроса # Коэффициент спроса
total_players = len(players) total_supply = asset_data.get('total_quantity')
if total_players > 0 and asset_id in assets: if total_supply is not None:
volatility = assets[asset_id].get('volatility', 0.2) demand_factor = total_held / total_supply if total_supply > 0 else 0
price_change = volatility * (demand / (total_players * 10)) else:
new_price = assets[asset_id]['price'] * (1 + price_change) demand_factor = total_held / (len(players) * 100) if players else 0 # Для безлимитных
# Эффект перегрева demand_factor = min(demand_factor, 1.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)
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['assets'] = assets
market_data['last_update'] = datetime.utcnow().isoformat() market_data['last_update'] = datetime.utcnow().isoformat()
game_state.market_data = json.dumps(market_data) game_state.market_data = json.dumps(market_data)
db.session.commit() db.session.commit()
# Отправляем обновление через WebSocket # Отправляем обновление
socketio.emit('market_updated', { socketio.emit('market_updated', {
'room': room.code, 'room': room.code,
'assets': assets, 'assets': assets,
'timestamp': datetime.utcnow().isoformat() 'timestamp': datetime.utcnow().isoformat()
}, room=room.code) }, room=room.code)
# Через 25 секунд переходим к событиям # Переходим к следующей фазе
time.sleep(25) time.sleep(25)
# Проверяем, что мы все еще в фазе рынка
room = GameRoom.query.get(room_id) room = GameRoom.query.get(room_id)
if room and room.game_state and room.game_state.phase == 'market': if room and room.game_state and room.game_state.phase == 'market':
game_state = room.game_state game_state = room.game_state
game_state.phase = 'event' 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() db.session.commit()

15
game_balance/__init__.py Normal file
View File

@@ -0,0 +1,15 @@
from game_balance.config import BalanceConfig
from game_balance.assets_config import AssetsConfig
from game_balance.players_config import PlayersConfig
from game_balance.game_config import GameConfig
from game_balance.economy_config import EconomyConfig
from game_balance.events_config import EventsConfig
__all__ = [
'BalanceConfig',
'AssetsConfig',
'PlayersConfig',
'GameConfig',
'EconomyConfig',
'EventsConfig'
]

View File

@@ -0,0 +1,252 @@
"""
НАСТРОЙКИ АКТИВОВ
"""
class AssetsConfig:
# Категории активов
ASSET_CATEGORIES = {
'bonds': {'name': 'Облигации', 'color': '#4CAF50', 'icon': '🏦'},
'stocks': {'name': 'Акции', 'color': '#2196F3', 'icon': '📈'},
'real_estate': {'name': 'Недвижимость', 'color': '#FF9800', 'icon': '🏠'},
'crypto': {'name': 'Криптовалюта', 'color': '#9C27B0', 'icon': '💰'},
'commodities': {'name': 'Сырье', 'color': '#795548', 'icon': ''},
'business': {'name': 'Бизнес', 'color': '#F44336', 'icon': '🏢'},
'unique': {'name': 'Уникальные', 'color': '#FFD700', 'icon': '🏆'},
}
# Список всех активов
ASSETS = {
# ОБЛИГАЦИИ
'gov_bonds': {
'name': 'Государственные облигации',
'category': 'bonds',
'base_price': 10000,
'volatility': 0.05,
'income_per_month': 0.01,
'risk_level': 1,
'liquidity': 10,
'total_quantity': None,
'max_per_player': None,
'min_purchase': 1,
'description': 'Самый надежный актив. Защита от кризисов.'
},
# АКЦИИ
'stock_gazprom': {
'name': 'Акции Газпрома',
'category': 'stocks',
'base_price': 1000,
'volatility': 0.15,
'income_per_month': 0.03,
'risk_level': 3,
'liquidity': 8,
'total_quantity': 10000,
'max_per_player': 2000,
'min_purchase': 10,
'description': 'Голубые фишки. Стабильные дивиденды.'
},
'stock_sberbank': {
'name': 'Акции Сбербанка',
'category': 'stocks',
'base_price': 300,
'volatility': 0.18,
'income_per_month': 0.04,
'risk_level': 4,
'liquidity': 9,
'total_quantity': 15000,
'max_per_player': 3000,
'min_purchase': 10,
'description': 'Крупнейший банк. Высокая ликвидность.'
},
'stock_yandex': {
'name': 'Акции Яндекса',
'category': 'stocks',
'base_price': 3500,
'volatility': 0.25,
'income_per_month': 0.00,
'risk_level': 6,
'liquidity': 7,
'total_quantity': 5000,
'max_per_player': 1000,
'min_purchase': 1,
'description': 'IT-гигант. Высокий рост, высокая волатильность.'
},
# НЕДВИЖИМОСТЬ
'apartment_small': {
'name': 'Небольшая квартира',
'category': 'real_estate',
'base_price': 5000000,
'volatility': 0.10,
'income_per_month': 0.02,
'risk_level': 2,
'liquidity': 4,
'total_quantity': 12,
'max_per_player': 3,
'min_purchase': 1,
'description': 'Стабильный доход от аренды.'
},
'apartment_elite': {
'name': 'Элитная квартира',
'category': 'real_estate',
'base_price': 15000000,
'volatility': 0.08,
'income_per_month': 0.03,
'risk_level': 2,
'liquidity': 2,
'total_quantity': 3,
'max_per_player': 1,
'min_purchase': 1,
'description': 'Статусный актив. Низкая волатильность.'
},
# КРИПТОВАЛЮТА
'bitcoin': {
'name': 'Биткоин',
'category': 'crypto',
'base_price': 2000000,
'volatility': 0.35,
'income_per_month': 0.00,
'risk_level': 9,
'liquidity': 7,
'total_quantity': None,
'max_per_player': None,
'min_purchase': 0.001,
'description': 'Высокорисковый актив. Только спекуляции.'
},
# СЫРЬЕ
'oil': {
'name': 'Нефть Brent',
'category': 'commodities',
'base_price': 5500,
'volatility': 0.25,
'income_per_month': 0.00,
'risk_level': 6,
'liquidity': 6,
'total_quantity': 200,
'max_per_player': 40,
'min_purchase': 10,
'description': 'Зависит от политики. Высокая волатильность.'
},
'natural_gas': {
'name': 'Природный газ',
'category': 'commodities',
'base_price': 3000,
'volatility': 0.20,
'income_per_month': 0.00,
'risk_level': 5,
'liquidity': 5,
'total_quantity': 300,
'max_per_player': 50,
'min_purchase': 100,
'description': 'Сезонный актив. Пик зимой.'
},
# БИЗНЕС
'coffee_shop': {
'name': 'Кофейня',
'category': 'business',
'base_price': 2000000,
'volatility': 0.12,
'income_per_month': 0.05,
'risk_level': 5,
'liquidity': 3,
'total_quantity': 8,
'max_per_player': 2,
'min_purchase': 1,
'description': 'Пассивный бизнес. Средний риск.'
},
'it_startup': {
'name': 'IT-стартап',
'category': 'business',
'base_price': 1000000,
'volatility': 0.40,
'income_per_month': 0.00,
'risk_level': 10,
'liquidity': 1,
'total_quantity': 4,
'max_per_player': 1,
'min_purchase': 1,
'description': 'Венчурные инвестиции. Может взлететь или провалиться.'
},
# УНИКАЛЬНЫЕ
'shopping_mall': {
'name': 'Торговый центр',
'category': 'unique',
'base_price': 50000000,
'volatility': 0.10,
'income_per_month': 0.06,
'risk_level': 4,
'liquidity': 1,
'total_quantity': 1,
'max_per_player': 1,
'min_purchase': 1,
'description': 'Ключевой актив. Контроль над малым бизнесом.',
'special_rules': {
'can_be_fractional': True,
'min_fraction': 0.1,
'control_bonus': 0.1
}
},
'oil_field': {
'name': 'Нефтяное месторождение',
'category': 'unique',
'base_price': 100000000,
'volatility': 0.20,
'income_per_month': 0.08,
'risk_level': 7,
'liquidity': 1,
'total_quantity': 1,
'max_per_player': 1,
'min_purchase': 1,
'description': 'Стратегический актив. Контроль над ценами на нефть.',
'special_rules': {
'affects_other_assets': ['oil'],
'price_influence': 0.15
}
}
}
@staticmethod
def calculate_price(asset_data, demand_factor, month, total_players):
"""Расчет текущей цены актива"""
base_price = asset_data['base_price']
volatility = asset_data['volatility']
# Базовое изменение от спроса
price = base_price * (1 + demand_factor * volatility)
# Инфляция (0.5% в месяц)
price *= (1.005 ** month)
# Ограничения
price = max(price, base_price * 0.1) # Не ниже 10% от базы
price = min(price, base_price * 5) # Не выше 500% от базы
# Округление
if price < 1000:
return round(price, 2)
elif price < 1000000:
return round(price, 0)
else:
return round(price, -3)
@staticmethod
def calculate_income(asset_data, player_share, total_owners):
"""Расчет доходности с учетом баланса"""
base_income = asset_data['income_per_month']
if base_income <= 0:
return 0
# Штраф за концентрацию (если много владельцев у одного игрока)
if total_owners > 0:
concentration_penalty = (player_share ** 2) * 0.2
income = base_income * (1 - concentration_penalty)
else:
income = base_income
return max(income, 0.001) # Минимум 0.1%

34
game_balance/config.py Normal file
View File

@@ -0,0 +1,34 @@
"""
КОНФИГУРАЦИЯ БАЛАНСА ИГРЫ "КАПИТАЛ & РЫНОК"
"""
class BalanceConfig:
# Текущий режим баланса
BALANCE_MODE = "standard" # standard, easy, hard, tournament
@classmethod
def load_balance_mode(cls, mode):
"""Загрузка определенного режима баланса"""
if mode == "easy":
from game_balance.test_balance.easy_mode import EasyBalance
return EasyBalance()
elif mode == "hard":
from game_balance.test_balance.hard_mode import HardBalance
return HardBalance()
elif mode == "tournament":
from game_balance.test_balance.tournament import TournamentBalance
return TournamentBalance()
else:
from game_balance.test_balance.standard_mode import StandardBalance
return StandardBalance()
@classmethod
def get_available_modes(cls):
"""Возвращает список доступных режимов"""
return {
'standard': 'Стандартный',
'easy': 'Легкий (новички)',
'hard': 'Сложный (эксперты)',
'tournament': 'Турнирный'
}

View File

@@ -0,0 +1,74 @@
"""
ЭКОНОМИЧЕСКИЕ ПАРАМЕТРЫ
"""
class EconomyConfig:
# Налоговая система
TAX_SYSTEM = {
'income_tax': [
{'threshold': 0, 'rate': 0.00},
{'threshold': 50000, 'rate': 0.10},
{'threshold': 200000, 'rate': 0.15},
{'threshold': 500000, 'rate': 0.20},
{'threshold': 1000000, 'rate': 0.25}
],
'wealth_tax': {
'threshold': 5000000,
'rate': 0.01
},
'monopoly_tax': {
'threshold': 0.4,
'rate': 0.03
},
'transaction_fee': 0.01,
'auction_fee': 0.02,
}
# Кредитная система
LOAN_SYSTEM = {
'max_loan_multiplier': 10.0,
'interest_rates': {
'standard': 0.05,
'crisis': 0.10,
'black_market': 0.15,
},
'repayment_periods': [3, 6, 12, 24],
'late_fee': 0.02,
'default_threshold': 3,
}
# Макроэкономика
MACROECONOMICS = {
'base_inflation': 0.005,
'inflation_multipliers': {
'crisis': 1.5,
'boom': 0.7,
'default': 2.0,
},
'central_bank_rate': 0.04,
}
# Корреляции активов - ИСПРАВЛЕНО: ключи-строки вместо кортежей
ASSET_CORRELATIONS = {
'stock_gazprom:oil': 0.6,
'stock_sberbank:stock_gazprom': 0.4,
'oil:natural_gas': 0.7,
'apartment_small:apartment_elite': 0.5,
'gold:bitcoin': -0.3,
'gov_bonds:stock_gazprom': -0.2,
}
@staticmethod
def get_correlation(asset1, asset2):
"""Получить корреляцию между активами"""
# Пробуем прямой ключ
key1 = f"{asset1}:{asset2}"
if key1 in EconomyConfig.ASSET_CORRELATIONS:
return EconomyConfig.ASSET_CORRELATIONS[key1]
# Пробуем обратный ключ
key2 = f"{asset2}:{asset1}"
if key2 in EconomyConfig.ASSET_CORRELATIONS:
return EconomyConfig.ASSET_CORRELATIONS[key2]
return 0 # Нет корреляции

View File

@@ -0,0 +1,102 @@
"""
СОБЫТИЯ И КРИЗИСЫ
"""
class EventsConfig:
# Типы событий
EVENT_TYPES = {
'economic': {'name': 'Экономические', 'frequency': 0.4, 'color': '#FF9800'},
'political': {'name': 'Политические', 'frequency': 0.3, 'color': '#F44336'},
'natural': {'name': 'Природные', 'frequency': 0.15, 'color': '#4CAF50'},
'technological': {'name': 'Технологические', 'frequency': 0.15, 'color': '#2196F3'}
}
# События
EVENTS = {
'oil_boom': {
'name': 'Бум нефти',
'type': 'economic',
'probability': 0.08,
'effects': {
'oil': {'price_change': 0.30, 'duration': 2},
'stock_gazprom': {'price_change': 0.15, 'duration': 2},
},
'description': 'Цены на нефть взлетели на 30%'
},
'financial_crisis': {
'name': 'Финансовый кризис',
'type': 'economic',
'probability': 0.10,
'effects': {
'ALL': {'price_change': -0.20, 'duration': 2},
'gov_bonds': {'price_change': 0.05, 'duration': 2},
},
'description': 'Кризис ликвидности на рынках'
},
'sanctions': {
'name': 'Международные санкции',
'type': 'political',
'probability': 0.07,
'effects': {
'stock_gazprom': {'price_change': -0.35, 'duration': 3},
'stock_sberbank': {'price_change': -0.25, 'duration': 3},
},
'description': 'Новые санкции против российских компаний'
},
'tech_revolution': {
'name': 'Технологическая революция',
'type': 'technological',
'probability': 0.05,
'effects': {
'it_startup': {'price_change': 0.50, 'duration': 3},
'stock_yandex': {'price_change': 0.25, 'duration': 2},
'bitcoin': {'price_change': 0.20, 'duration': 1},
},
'description': 'Прорыв в IT-технологиях'
},
'elections': {
'name': 'Президентские выборы',
'type': 'political',
'probability': 0.06,
'effects': {
'ALL': {'price_change': -0.10, 'duration': 1},
'gov_bonds': {'price_change': 0.05, 'duration': 1},
},
'description': 'Неопределенность перед выборами'
},
'earthquake': {
'name': 'Землетрясение',
'type': 'natural',
'probability': 0.03,
'effects': {
'real_estate': {'price_change': -0.25, 'duration': 3},
},
'description': 'Разрушена инфраструктура в ключевом регионе'
}
}
# Кризисы
CRISES = {
'hyperinflation': {
'name': 'Гиперинфляция',
'probability': 0.02,
'conditions': ['month > 6'],
'effects': {
'ALL': {'price_change': 0.50, 'duration': 3},
'cash': {'value_change': -0.30, 'duration': 3},
},
'description': 'Гиперинфляция! Цены растут на 50% в месяц'
},
'market_crash': {
'name': 'Обвал рынка',
'probability': 0.025,
'conditions': ['month > 4'],
'effects': {
'stocks': {'price_change': -0.60, 'duration': 4},
'real_estate': {'price_change': -0.40, 'duration': 6},
'crypto': {'price_change': -0.80, 'duration': 2},
},
'description': 'Лопнул финансовый пузырь. Рынки рухнули'
}
}

View File

@@ -0,0 +1,43 @@
"""
НАСТРОЙКИ ИГРОВОГО ПРОЦЕССА
"""
class GameConfig:
# Основные параметры
GAME_NAME = "Капитал & Рынок"
VERSION = "1.0.0"
DEFAULT_TOTAL_MONTHS = 12
MIN_TOTAL_MONTHS = 6
MAX_TOTAL_MONTHS = 24
# Тайминги фаз (секунды)
PHASE_DURATIONS = {
'action': 120,
'market': 30,
'event': 30,
'results': 45
}
# Режимы скорости
SPEED_MODES = {
'slow': 2.0,
'normal': 1.0,
'fast': 0.5,
'blitz': 0.25
}
# Настройки комнат
MAX_PLAYERS_PER_ROOM = 10
MIN_PLAYERS_TO_START = 2
MIN_PLAYERS_TO_CONTINUE = 2
# Ограничения
MAX_TRANSACTIONS_PER_PHASE = 20
MIN_BID_AMOUNT = 1000
# Настройки интерфейса
DEFAULT_LANGUAGE = 'ru'
CURRENCY_SYMBOL = ''
CURRENCY_CODE = 'RUB'

View File

@@ -0,0 +1,158 @@
"""
НАСТРОЙКИ ИГРОКОВ
"""
class PlayersConfig:
# Стартовые условия
STARTING_CAPITAL = 100000
TARGET_CAPITAL = 50000000
# Способности игроков
ABILITIES = {
'crisis_investor': {
'name': 'Кризисный инвестор',
'description': '+20% к доходу при падении рынка более 10%',
'cooldown': 3,
'effect': {
'type': 'crisis_bonus',
'value': 0.2,
'condition': 'market_drop > 0.1'
}
},
'lobbyist': {
'name': 'Лоббист',
'description': 'Может временно снизить налоги на 5%',
'cooldown': 2,
'effect': {
'type': 'tax_reduction',
'value': 0.05,
'duration': 1
}
},
'predictor': {
'name': 'Предсказатель',
'description': 'Видит следующее случайное событие за 1 месяц',
'cooldown': 4,
'effect': {
'type': 'event_preview',
'lookahead': 1
}
},
'golden_pillow': {
'name': 'Золотая подушка',
'description': 'Может защитить 20% капитала от любых потерь',
'cooldown': 6,
'effect': {
'type': 'capital_protection',
'percentage': 0.2,
'duration': 1
}
},
'shadow_accountant': {
'name': 'Теневая бухгалтерия',
'description': 'Раз в игру уменьшить налоги на 50%',
'cooldown': None,
'effect': {
'type': 'tax_evasion',
'value': 0.5
}
},
'credit_magnate': {
'name': 'Кредитный магнат',
'description': 'Может давать кредиты другим игрокам под свой процент',
'cooldown': 1,
'effect': {
'type': 'lend_money',
'max_amount_multiplier': 0.5
}
},
'bear_raid': {
'name': 'Медвежий набег',
'description': 'Вызвать искусственное падение цены актива на 15%',
'cooldown': 4,
'effect': {
'type': 'price_manipulation',
'direction': 'down',
'value': 0.15
}
},
'fake_news': {
'name': 'Фейковые новости',
'description': 'Подменить одно случайное событие',
'cooldown': 6,
'effect': {
'type': 'event_manipulation',
'scope': 'next_event'
}
},
'dividend_king': {
'name': 'Король дивидендов',
'description': '+10% дохода от всех дивидендных активов',
'cooldown': 0,
'effect': {
'type': 'dividend_bonus',
'value': 0.1
}
},
'raider_capture': {
'name': 'Рейдерский захват',
'description': 'Попытаться отобрать 5% капитала у лидера',
'cooldown': 3,
'effect': {
'type': 'capital_raid',
'percentage': 0.05,
'success_chance': 0.6,
'penalty': 0.1
}
},
'mafia_connections': {
'name': 'Мафиозные связи',
'description': 'Отменить одно негативное событие для себя',
'cooldown': 5,
'effect': {
'type': 'event_block',
'condition': 'negative_event'
}
},
'economic_advisor': {
'name': 'Экономический советник',
'description': 'Раз в 3 месяца изменить налоговую ставку для всех',
'cooldown': 3,
'effect': {
'type': 'tax_policy',
'change_range': (-0.05, 0.05)
}
},
'currency_speculator': {
'name': 'Валютный спекулянт',
'description': 'На 1 месяц отвязать рубль от нефти',
'cooldown': 4,
'effect': {
'type': 'correlation_break',
'assets': ['oil', 'stock_gazprom'],
'duration': 1
}
}
}
# Ограничения по владению
MAX_ASSETS_PER_TYPE = {
'bonds': None,
'stocks': 0.4,
'real_estate': 0.5,
'crypto': None,
'commodities': 0.3,
'business': 0.4,
'unique': 1.0,
}
# Лимиты по месяцам
PURCHASE_LIMITS_BY_MONTH = {
1: 0.1,
2: 0.2,
3: 0.4,
4: 0.6,
5: 0.8,
6: 1.0,
}

View File

@@ -0,0 +1,11 @@
from game_balance.test_balance.easy_mode import EasyBalance
from game_balance.test_balance.hard_mode import HardBalance
from game_balance.test_balance.tournament import TournamentBalance
from game_balance.test_balance.standard_mode import StandardBalance
__all__ = [
'EasyBalance',
'HardBalance',
'TournamentBalance',
'StandardBalance'
]

View File

@@ -0,0 +1,73 @@
"""
ЛЕГКИЙ РЕЖИМ БАЛАНСА
Для новичков
"""
from game_balance.test_balance.standard_mode import StandardBalance
class EasyBalance(StandardBalance):
"""Облегченный режим"""
@classmethod
def get_assets_config(cls):
config = super().get_assets_config()
# Делаем активы дешевле и стабильнее
for asset_id, asset_data in config.items():
if asset_data.get('base_price'):
asset_data['base_price'] *= 0.5
if asset_data.get('volatility'):
asset_data['volatility'] *= 0.7
if asset_data.get('income_per_month'):
asset_data['income_per_month'] *= 1.3
return config
@classmethod
def get_players_config(cls):
config = super().get_players_config()
config['STARTING_CAPITAL'] = 200000
config['TARGET_CAPITAL'] = 25000000
# Более мягкие ограничения
config['MAX_ASSETS_PER_TYPE'] = {
'bonds': None,
'stocks': 0.6,
'real_estate': 0.7,
'crypto': None,
'commodities': 0.5,
'business': 0.6,
'unique': 1.0,
}
# Быстрое снятие лимитов
config['PURCHASE_LIMITS_BY_MONTH'] = {
1: 0.3,
2: 0.6,
3: 1.0,
}
return config
@classmethod
def get_economy_config(cls):
config = super().get_economy_config()
# Меньше налогов
config['TAX_SYSTEM']['income_tax'] = [
{'threshold': 0, 'rate': 0.00},
{'threshold': 100000, 'rate': 0.05},
{'threshold': 500000, 'rate': 0.10},
{'threshold': 2000000, 'rate': 0.15},
{'threshold': 5000000, 'rate': 0.20},
]
config['TAX_SYSTEM']['wealth_tax']['threshold'] = 10000000
# Ниже ставки по кредитам
config['LOAN_SYSTEM']['interest_rates']['standard'] = 0.03
# Меньше инфляции
config['MACROECONOMICS']['base_inflation'] = 0.002
return config

View File

@@ -0,0 +1,73 @@
"""
ЛЕГКИЙ РЕЖИМ БАЛАНСА
Для новичков
"""
from game_balance.test_balance.standard_mode import StandardBalance
class HardBalance(StandardBalance):
"""Облегченный режим"""
@classmethod
def get_assets_config(cls):
config = super().get_assets_config()
# Делаем активы дешевле и стабильнее
for asset_id, asset_data in config.items():
if asset_data.get('base_price'):
asset_data['base_price'] *= 0.5
if asset_data.get('volatility'):
asset_data['volatility'] *= 0.7
if asset_data.get('income_per_month'):
asset_data['income_per_month'] *= 1.3
return config
@classmethod
def get_players_config(cls):
config = super().get_players_config()
config['STARTING_CAPITAL'] = 200000
config['TARGET_CAPITAL'] = 25000000
# Более мягкие ограничения
config['MAX_ASSETS_PER_TYPE'] = {
'bonds': None,
'stocks': 0.6,
'real_estate': 0.7,
'crypto': None,
'commodities': 0.5,
'business': 0.6,
'unique': 1.0,
}
# Быстрое снятие лимитов
config['PURCHASE_LIMITS_BY_MONTH'] = {
1: 0.3,
2: 0.6,
3: 1.0,
}
return config
@classmethod
def get_economy_config(cls):
config = super().get_economy_config()
# Меньше налогов
config['TAX_SYSTEM']['income_tax'] = [
{'threshold': 0, 'rate': 0.00},
{'threshold': 100000, 'rate': 0.05},
{'threshold': 500000, 'rate': 0.10},
{'threshold': 2000000, 'rate': 0.15},
{'threshold': 5000000, 'rate': 0.20},
]
config['TAX_SYSTEM']['wealth_tax']['threshold'] = 10000000
# Ниже ставки по кредитам
config['LOAN_SYSTEM']['interest_rates']['standard'] = 0.03
# Меньше инфляции
config['MACROECONOMICS']['base_inflation'] = 0.002
return config

View File

@@ -0,0 +1,61 @@
"""
СТАНДАРТНЫЙ РЕЖИМ БАЛАНСА
Используется по умолчанию
"""
from game_balance.assets_config import AssetsConfig
from game_balance.players_config import PlayersConfig
from game_balance.economy_config import EconomyConfig
from game_balance.events_config import EventsConfig
from game_balance.game_config import GameConfig
class StandardBalance:
"""Стандартный сбалансированный режим"""
@classmethod
def get_assets_config(cls):
"""Возвращает конфигурацию активов"""
return AssetsConfig.ASSETS.copy()
@classmethod
def get_players_config(cls):
"""Возвращает конфигурацию игроков"""
return {
'STARTING_CAPITAL': PlayersConfig.STARTING_CAPITAL,
'TARGET_CAPITAL': PlayersConfig.TARGET_CAPITAL,
'MAX_ASSETS_PER_TYPE': PlayersConfig.MAX_ASSETS_PER_TYPE.copy(),
'PURCHASE_LIMITS_BY_MONTH': PlayersConfig.PURCHASE_LIMITS_BY_MONTH.copy(),
'ABILITIES': PlayersConfig.ABILITIES.copy()
}
@classmethod
def get_economy_config(cls):
"""Возвращает экономическую конфигурацию"""
return {
'TAX_SYSTEM': EconomyConfig.TAX_SYSTEM.copy(),
'LOAN_SYSTEM': EconomyConfig.LOAN_SYSTEM.copy(),
'MACROECONOMICS': EconomyConfig.MACROECONOMICS.copy(),
'ASSET_CORRELATIONS': EconomyConfig.ASSET_CORRELATIONS.copy()
}
@classmethod
def get_events_config(cls):
"""Возвращает конфигурацию событий"""
return {
'EVENT_TYPES': EventsConfig.EVENT_TYPES.copy(),
'EVENTS': EventsConfig.EVENTS.copy(),
'CRISES': EventsConfig.CRISES.copy()
}
@classmethod
def get_game_config(cls):
"""Возвращает игровую конфигурацию"""
return {
'PHASE_DURATIONS': GameConfig.PHASE_DURATIONS.copy(),
'SPEED_MODES': GameConfig.SPEED_MODES.copy(),
'MAX_PLAYERS_PER_ROOM': GameConfig.MAX_PLAYERS_PER_ROOM,
'MIN_PLAYERS_TO_START': GameConfig.MIN_PLAYERS_TO_START,
'MIN_PLAYERS_TO_CONTINUE': GameConfig.MIN_PLAYERS_TO_CONTINUE,
'MAX_TRANSACTIONS_PER_PHASE': GameConfig.MAX_TRANSACTIONS_PER_PHASE,
'MIN_BID_AMOUNT': GameConfig.MIN_BID_AMOUNT
}

View File

@@ -0,0 +1,83 @@
"""
ТУРНИРНЫЙ РЕЖИМ БАЛАНСА
Для соревнований
"""
from game_balance.test_balance.standard_mode import StandardBalance
class TournamentBalance(StandardBalance):
"""Турнирный режим"""
@classmethod
def get_assets_config(cls):
config = super().get_assets_config()
# Балансировка цен
price_adjustments = {
'gov_bonds': 1.0,
'stock_gazprom': 0.8,
'stock_sberbank': 0.8,
'apartment_small': 0.7,
'apartment_elite': 0.6,
'bitcoin': 1.2,
'oil': 1.0,
'coffee_shop': 0.8,
'it_startup': 1.0,
'shopping_mall': 0.5,
'oil_field': 0.4,
}
for asset_id, multiplier in price_adjustments.items():
if asset_id in config and config[asset_id].get('base_price'):
config[asset_id]['base_price'] *= multiplier
return config
@classmethod
def get_players_config(cls):
config = super().get_players_config()
config['STARTING_CAPITAL'] = 500000
config['TARGET_CAPITAL'] = 100000000
# Жесткие ограничения против монополий
config['MAX_ASSETS_PER_TYPE'] = {
'bonds': 0.5,
'stocks': 0.3,
'real_estate': 0.4,
'crypto': 0.5,
'commodities': 0.3,
'business': 0.4,
'unique': 0.5,
}
# Быстрый старт
config['PURCHASE_LIMITS_BY_MONTH'] = {
1: 0.5,
2: 1.0,
}
return config
@classmethod
def get_economy_config(cls):
config = super().get_economy_config()
# Высокие налоги
config['TAX_SYSTEM']['income_tax'] = [
{'threshold': 0, 'rate': 0.00},
{'threshold': 100000, 'rate': 0.15},
{'threshold': 500000, 'rate': 0.25},
{'threshold': 2000000, 'rate': 0.35},
{'threshold': 5000000, 'rate': 0.45},
]
config['TAX_SYSTEM']['wealth_tax']['rate'] = 0.02
config['TAX_SYSTEM']['transaction_fee'] = 0.02
# Высокие ставки
config['LOAN_SYSTEM']['interest_rates']['standard'] = 0.08
# Высокая инфляция
config['MACROECONOMICS']['base_inflation'] = 0.01
return config

124
game_balance/validator.py Normal file
View File

@@ -0,0 +1,124 @@
"""
ВАЛИДАТОР БАЛАНСА
Проверяет корректность настроек
"""
class BalanceValidator:
@staticmethod
def validate_assets(assets):
"""Проверка конфигурации активов"""
errors = []
if not isinstance(assets, dict):
errors.append("Assets config must be a dictionary")
return errors
for asset_id, asset_data in assets.items():
if not isinstance(asset_data, dict):
errors.append(f"Asset {asset_id}: data must be a dictionary")
continue
# Проверка обязательных полей
required_fields = ['name', 'category', 'base_price', 'volatility']
for field in required_fields:
if field not in asset_data:
errors.append(f"Asset {asset_id}: missing required field '{field}'")
# Проверка цены
price = asset_data.get('base_price', 0)
if not isinstance(price, (int, float)) or price <= 0:
errors.append(f"Asset {asset_id}: base_price must be positive number")
# Проверка волатильности
volatility = asset_data.get('volatility', 0)
if not isinstance(volatility, (int, float)) or volatility < 0 or volatility > 1:
errors.append(f"Asset {asset_id}: volatility must be between 0 and 1")
# Проверка доходности
income = asset_data.get('income_per_month', 0)
if not isinstance(income, (int, float)) or income < 0 or income > 0.5:
errors.append(f"Asset {asset_id}: income_per_month must be between 0 and 0.5")
# Проверка количества
total = asset_data.get('total_quantity')
if total is not None:
if not isinstance(total, (int, float)) or total <= 0:
errors.append(f"Asset {asset_id}: total_quantity must be positive or None")
return errors
@staticmethod
def validate_economy(economy_config):
"""Проверка экономической конфигурации"""
errors = []
if not isinstance(economy_config, dict):
errors.append("Economy config must be a dictionary")
return errors
# Проверка налогов
tax_system = economy_config.get('TAX_SYSTEM', {})
income_tax = tax_system.get('income_tax', [])
if not isinstance(income_tax, list):
errors.append("income_tax must be a list")
else:
last_threshold = -1
for i, bracket in enumerate(income_tax):
if not isinstance(bracket, dict):
errors.append(f"Tax bracket {i}: must be a dictionary")
continue
threshold = bracket.get('threshold', 0)
if threshold <= last_threshold:
errors.append(f"Tax brackets must be in ascending order at index {i}")
last_threshold = threshold
# Проверка кредитов
loan_system = economy_config.get('LOAN_SYSTEM', {})
max_multiplier = loan_system.get('max_loan_multiplier', 0)
if not isinstance(max_multiplier, (int, float)) or max_multiplier <= 0:
errors.append("max_loan_multiplier must be positive")
# Проверка корреляций
correlations = economy_config.get('ASSET_CORRELATIONS', {})
if not isinstance(correlations, dict):
errors.append("ASSET_CORRELATIONS must be a dictionary")
else:
for key, value in correlations.items():
if not isinstance(key, str):
errors.append(f"Correlation key must be string, got {type(key)}")
if not isinstance(value, (int, float)) or value < -1 or value > 1:
errors.append(f"Correlation value must be between -1 and 1, got {value}")
return errors
@staticmethod
def validate_balance_mode(balance):
"""Полная проверка режима баланса"""
all_errors = []
# Проверяем наличие необходимых методов
required_methods = ['get_assets_config', 'get_players_config', 'get_economy_config']
for method in required_methods:
if not hasattr(balance, method):
all_errors.append(f"Balance mode missing required method: {method}")
return all_errors
try:
# Проверка активов
assets = balance.get_assets_config()
all_errors.extend(BalanceValidator.validate_assets(assets))
# Проверка экономики
economy = balance.get_economy_config()
all_errors.extend(BalanceValidator.validate_economy(economy))
# Проверка конфигурации игроков
players = balance.get_players_config()
if not isinstance(players, dict):
all_errors.append("Players config must be a dictionary")
except Exception as e:
all_errors.append(f"Error validating balance: {str(e)}")
return all_errors

View File

@@ -362,6 +362,57 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Модальное окно продажи актива -->
<div id="sell-asset-modal" class="modal-backdrop">
<div class="modal">
<h3 id="sell-asset-title">Продажа актива</h3>
<div style="margin: 20px 0;">
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span>Текущая цена:</span>
<strong id="sell-asset-price">0 ₽</strong>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span>У вас в наличии:</span>
<strong id="sell-player-quantity">0 шт.</strong>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
<span>Комиссия:</span>
<strong id="sell-fee">1%</strong>
</div>
<div class="input-group">
<label for="sell-quantity">Количество для продажи</label>
<input type="number" id="sell-quantity" min="1" value="1" style="width: 100%;">
</div>
<div style="margin-top: 15px; padding: 15px; background-color: #f8f9fa; border-radius: var(--border-radius);">
<div style="display: flex; justify-content: space-between;">
<span>Выручка:</span>
<strong id="sell-revenue">0 ₽</strong>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 5px;">
<span>Комиссия:</span>
<strong id="sell-fee-amount">0 ₽</strong>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 5px;">
<span>К получению:</span>
<strong id="sell-total">0 ₽</strong>
</div>
</div>
</div>
<div class="flex gap-2">
<button onclick="confirmSellAsset()" class="button warning" style="flex: 1;">
Продать
</button>
<button onclick="cancelSellAsset()" class="button secondary" style="flex: 1;">
Отмена
</button>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
@@ -374,6 +425,7 @@ let currentPhase = '{{ game_state.phase }}';
let phaseEndTime = '{{ game_state.phase_end }}'; let phaseEndTime = '{{ game_state.phase_end }}';
let selectedAsset = null; let selectedAsset = null;
let gameTimer = null; let gameTimer = null;
let selectedAssetForSell = null;
// Инициализация при загрузке // Инициализация при загрузке
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
@@ -643,7 +695,8 @@ function loadAssets() {
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success && data.assets) { if (data.success && data.assets) {
displayAssets(data.assets); window.allAssetsData = data.assets;
displayAssets(data.assets, data.categories);
} }
}) })
.catch(error => { .catch(error => {
@@ -652,37 +705,194 @@ function loadAssets() {
}); });
} }
function displayAssets(assets) { function displayAssets(assets, categories) {
const assetsList = document.getElementById('assets-list'); const assetsList = document.getElementById('assets-list');
if (!assetsList) return; if (!assetsList) return;
assetsList.innerHTML = ''; assetsList.innerHTML = '';
// Сохраняем категории глобально для использования в других функциях
window.assetCategories = categories || {
'bonds': {'name': 'Облигации', 'icon': '🏦'},
'stocks': {'name': 'Акции', 'icon': '📈'},
'real_estate': {'name': 'Недвижимость', 'icon': '🏠'},
'crypto': {'name': 'Криптовалюта', 'icon': '💰'},
'commodities': {'name': 'Сырьевые товары', 'icon': '⛽'},
'business': {'name': 'Бизнес', 'icon': '🏢'},
'unique': {'name': 'Уникальные активы', 'icon': '🏆'}
};
// Группируем активы по категориям
const groupedAssets = {};
Object.entries(assets).forEach(([assetId, assetData]) => { Object.entries(assets).forEach(([assetId, assetData]) => {
const category = assetData.category || 'other';
if (!groupedAssets[category]) {
groupedAssets[category] = [];
}
groupedAssets[category].push({ id: assetId, ...assetData });
});
// Порядок категорий для отображения
const categoryOrder = ['bonds', 'stocks', 'real_estate', 'business', 'commodities', 'crypto', 'unique'];
// Отображаем активы по категориям в заданном порядке
categoryOrder.forEach(categoryKey => {
if (groupedAssets[categoryKey] && groupedAssets[categoryKey].length > 0) {
const category = window.assetCategories[categoryKey] || { name: categoryKey, icon: '📊' };
// Заголовок категории
const categoryHeader = document.createElement('div');
categoryHeader.className = 'category-header';
categoryHeader.innerHTML = `
<span style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.3rem;">${category.icon || '📊'}</span>
<span style="font-weight: 600; color: var(--primary-color);">${category.name || categoryKey}</span>
</span>
`;
assetsList.appendChild(categoryHeader);
// Сортируем активы внутри категории по цене (от дешевых к дорогим)
groupedAssets[categoryKey].sort((a, b) => a.price - b.price);
// Активы категории
groupedAssets[categoryKey].forEach(asset => {
const assetElement = createAssetElement(asset);
assetsList.appendChild(assetElement);
});
}
});
// Показываем оставшиеся категории, которые не вошли в порядок
Object.keys(groupedAssets).forEach(categoryKey => {
if (!categoryOrder.includes(categoryKey) && groupedAssets[categoryKey].length > 0) {
const category = window.assetCategories[categoryKey] || { name: categoryKey, icon: '📊' };
const categoryHeader = document.createElement('div');
categoryHeader.className = 'category-header';
categoryHeader.innerHTML = `
<span style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.3rem;">${category.icon || '📊'}</span>
<span style="font-weight: 600; color: var(--primary-color);">${category.name || categoryKey}</span>
</span>
`;
assetsList.appendChild(categoryHeader);
groupedAssets[categoryKey].forEach(asset => {
const assetElement = createAssetElement(asset);
assetsList.appendChild(assetElement);
});
}
});
}
function createAssetElement(asset) {
const assetElement = document.createElement('div'); const assetElement = document.createElement('div');
assetElement.className = 'asset-item'; assetElement.className = 'asset-item';
const isAvailable = asset.available === null || asset.available > 0;
const canAfford = window.playerCapital >= asset.price * asset.min_purchase;
const canBuyMore = !asset.max_per_player || asset.player_quantity < asset.max_per_player;
let availabilityText = '';
if (asset.available !== null) {
const percentage = ((asset.available / asset.total) * 100).toFixed(0);
availabilityText = `<div style="font-size: 0.8rem; color: var(--light-text); margin-top: 3px;">
Доступно: <span style="font-weight: 600; color: ${percentage > 30 ? 'var(--success-color)' : 'var(--warning-color)'}">
${asset.available} / ${asset.total}
</span> (${percentage}%)
</div>`;
}
let playerQuantityText = '';
if (asset.player_quantity > 0) {
playerQuantityText = `<div style="font-size: 0.8rem; color: var(--success-color); margin-top: 3px;">
У вас: <span style="font-weight: 600;">${asset.player_quantity} шт.</span>
</div>`;
}
let limitText = '';
if (asset.max_per_player) {
limitText = `<div style="font-size: 0.8rem; color: var(--light-text); margin-top: 3px;">
Лимит: ${asset.player_quantity} / ${asset.max_per_player}
</div>`;
}
const volatilityLevel = getVolatilityLevel(asset.volatility);
const volatilityColors = {
'low': '#4CAF50',
'medium': '#FF9800',
'high': '#F44336'
};
assetElement.innerHTML = ` assetElement.innerHTML = `
<div class="asset-info"> <div class="asset-info" style="flex: 1;">
<div class="asset-name"> <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 5px;">
${getAssetName(assetId)} <span style="font-size: 1.2rem;">${getAssetIcon(asset.category)}</span>
<span class="asset-name" style="font-size: 1.1rem; font-weight: 600;">${asset.name}</span>
</div> </div>
<div class="asset-meta"> <div style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 5px;">
Волатильность: ${(assetData.volatility * 100).toFixed(1)}% <span style="font-size: 0.8rem; padding: 3px 8px; border-radius: 12px; background-color: ${volatilityColors[volatilityLevel]}20; color: ${volatilityColors[volatilityLevel]}; font-weight: 600;">
⚡ Волатильность: ${(asset.volatility * 100).toFixed(0)}%
</span>
${asset.income_per_month > 0 ?
`<span style="font-size: 0.8rem; padding: 3px 8px; border-radius: 12px; background-color: var(--success-color)20; color: var(--success-color); font-weight: 600;">
💰 Доход: +${(asset.income_per_month * 100).toFixed(1)}%/мес
</span>` : ''
}
</div>
${availabilityText}
${playerQuantityText}
${limitText}
<div style="font-size: 0.85rem; color: var(--light-text); margin-top: 8px; padding-top: 8px; border-top: 1px solid #eee;">
${asset.description || 'Нет описания'}
</div> </div>
</div> </div>
<div style="text-align: right;"> <div style="text-align: right; min-width: 120px;">
<div class="asset-price"> <div style="font-size: 1.3rem; font-weight: 700; color: var(--primary-color); margin-bottom: 10px;">
${formatCurrency(assetData.price)} ${formatCurrency(asset.price)}
</div>
<div style="display: flex; flex-direction: column; gap: 5px;">
${asset.player_quantity > 0 ?
`<button onclick="showSellModal('${asset.id}', ${asset.price}, '${asset.name}', ${asset.player_quantity})"
class="button danger" style="padding: 8px 12px; font-size: 0.9rem; width: 100%;">
📉 Продать
</button>` : ''
}
${isAvailable && canAfford && canBuyMore ?
`<button onclick="showBuyAssetModal('${asset.id}', ${asset.price}, '${asset.name}', ${asset.min_purchase || 1}, ${asset.available || 'null'})"
class="button success" style="padding: 8px 12px; font-size: 0.9rem; width: 100%;">
📈 Купить
</button>` :
`<button disabled class="button" style="padding: 8px 12px; font-size: 0.9rem; width: 100%; opacity: 0.5; cursor: not-allowed;">
${!isAvailable ? 'Нет в наличии' : !canAfford ? 'Не хватает средств' : 'Лимит исчерпан'}
</button>`
}
</div> </div>
<button onclick="showBuyAssetModal('${assetId}', ${assetData.price})"
class="button" style="margin-top: 5px; padding: 5px 10px; font-size: 0.9rem;">
Купить
</button>
</div> </div>
`; `;
assetsList.appendChild(assetElement); return assetElement;
}); }
function getAssetIcon(category) {
const icons = {
'bonds': '🏦',
'stocks': '📈',
'real_estate': '🏠',
'crypto': '💰',
'commodities': '⛽',
'business': '🏢',
'unique': '🏆',
'other': '📊'
};
return icons[category] || '📊';
}
function getVolatilityLevel(volatility) {
if (volatility < 0.15) return 'low';
if (volatility < 0.25) return 'medium';
return 'high';
} }
function getAssetName(assetId) { function getAssetName(assetId) {
@@ -695,18 +905,40 @@ function getAssetName(assetId) {
return names[assetId] || assetId; return names[assetId] || assetId;
} }
function showBuyAssetModal(assetId, price) { function showBuyAssetModal(assetId, price, assetName, minQuantity, available) {
selectedAsset = { id: assetId, price: price }; selectedAsset = {
id: assetId,
price: price,
name: assetName,
minQuantity: minQuantity,
available: available
};
document.getElementById('buy-asset-title').textContent = `Покупка ${getAssetName(assetId)}`; document.getElementById('buy-asset-title').textContent = `Покупка: ${assetName}`;
document.getElementById('asset-price').textContent = formatCurrency(price); document.getElementById('asset-price').textContent = formatCurrency(price);
document.getElementById('player-capital-display').textContent = formatCurrency(playerCapital); document.getElementById('player-capital-display').textContent = formatCurrency(playerCapital);
const maxQuantity = Math.floor(playerCapital / price); const maxByCapital = Math.floor(playerCapital / price);
document.getElementById('max-quantity').textContent = `${maxQuantity} шт.`; let maxQuantity = maxByCapital;
// Учитываем доступность
if (available !== null && available !== undefined) {
maxQuantity = Math.min(maxQuantity, available);
}
// Учитываем лимит на игрока
const assetData = allAssetsData?.[assetId];
if (assetData?.max_per_player) {
const currentQuantity = assetData.player_quantity || 0;
const remainingLimit = assetData.max_per_player - currentQuantity;
maxQuantity = Math.min(maxQuantity, remainingLimit);
}
document.getElementById('max-quantity').textContent = `${maxQuantity} шт.`;
document.getElementById('buy-quantity').max = maxQuantity; document.getElementById('buy-quantity').max = maxQuantity;
document.getElementById('buy-quantity').value = 1; document.getElementById('buy-quantity').value = Math.min(minQuantity, maxQuantity);
document.getElementById('buy-quantity').min = minQuantity;
document.getElementById('buy-quantity').step = minQuantity;
updateBuyCalculations(); updateBuyCalculations();
@@ -846,6 +1078,102 @@ function useAbility() {
}); });
} }
//продажа активов
function showSellModal(assetId, price, assetName, maxQuantity) {
selectedAssetForSell = {
id: assetId,
price: price,
name: assetName,
maxQuantity: maxQuantity
};
document.getElementById('sell-asset-title').textContent = `Продажа: ${assetName}`;
document.getElementById('sell-asset-price').textContent = formatCurrency(price);
document.getElementById('sell-player-quantity').textContent = `${maxQuantity} шт.`;
const feeRate = 0.01; // 1% комиссия
document.getElementById('sell-fee').textContent = `${feeRate * 100}%`;
document.getElementById('sell-quantity').max = maxQuantity;
document.getElementById('sell-quantity').value = 1;
document.getElementById('sell-quantity').min = 1;
updateSellCalculations();
document.getElementById('sell-asset-modal').classList.add('active');
}
function updateSellCalculations() {
if (!selectedAssetForSell) return;
const quantity = parseInt(document.getElementById('sell-quantity').value) || 1;
const price = selectedAssetForSell.price;
const feeRate = 0.01;
const revenue = quantity * price;
const fee = revenue * feeRate;
const total = revenue - fee;
document.getElementById('sell-revenue').textContent = formatCurrency(revenue);
document.getElementById('sell-fee-amount').textContent = formatCurrency(fee);
document.getElementById('sell-total').textContent = formatCurrency(total);
}
function cancelSellAsset() {
selectedAssetForSell = null;
document.getElementById('sell-asset-modal').classList.remove('active');
}
function confirmSellAsset() {
if (!selectedAssetForSell) return;
const quantity = parseInt(document.getElementById('sell-quantity').value) || 1;
if (quantity > selectedAssetForSell.maxQuantity) {
showNotification(`У вас только ${selectedAssetForSell.maxQuantity} шт.`, 'error');
return;
}
fetch(`/api/game/${roomCode}/sell`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
},
body: JSON.stringify({
asset_id: selectedAssetForSell.id,
quantity: quantity
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(data.message, 'success');
cancelSellAsset();
// Обновляем капитал
playerCapital = data.new_capital;
updateCapitalProgress();
// Перезагружаем активы
loadAssets();
// Обновляем отображение капитала
document.getElementById('player-capital-display').textContent =
formatCurrency(playerCapital);
} else {
showNotification('❌ ' + data.error, 'error');
}
})
.catch(error => {
console.error('Error selling asset:', error);
showNotification('❌ Ошибка при продаже', 'error');
});
}
// Добавляем обработчик input для поля количества
document.getElementById('sell-quantity')?.addEventListener('input', updateSellCalculations);
// Завершение фазы действий // Завершение фазы действий
function endActionPhase() { function endActionPhase() {
if (!confirm('Завершить ваши действия и перейти к следующей фазе?')) { if (!confirm('Завершить ваши действия и перейти к следующей фазе?')) {
@@ -1270,6 +1598,112 @@ document.addEventListener('keydown', function(event) {
font-size: 0.9rem; font-size: 0.9rem;
padding: 10px 5px; padding: 10px 5px;
} }
}
/* Стили для категорий активов */
.category-header {
margin: 25px 0 15px 0;
padding-bottom: 10px;
border-bottom: 3px solid var(--primary-color);
position: relative;
}
.category-header:first-of-type {
margin-top: 5px;
}
.category-header span {
background-color: var(--primary-color);
color: white;
padding: 8px 16px;
border-radius: 20px;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 1rem;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.asset-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px;
margin-bottom: 10px;
background-color: white;
border-radius: var(--border-radius);
border: 1px solid #eee;
transition: all 0.3s ease;
}
.asset-item:hover {
transform: translateX(5px);
box-shadow: var(--box-shadow);
border-color: var(--primary-color);
}
.asset-info {
flex: 1;
padding-right: 20px;
}
.asset-name {
color: var(--text-color);
font-size: 1.1rem;
}
.asset-price {
color: var(--primary-color);
font-size: 1.3rem;
font-weight: 700;
}
.volatility-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 16px;
font-size: 0.8rem;
font-weight: 600;
}
.volatility-low {
background-color: #e8f5e9;
color: #388e3c;
}
.volatility-medium {
background-color: #fff3e0;
color: #ef6c00;
}
.volatility-high {
background-color: #ffebee;
color: #d32f2f;
}
.income-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 16px;
font-size: 0.8rem;
font-weight: 600;
background-color: #e8f5e9;
color: var(--success-color);
}
@media (max-width: 768px) {
.asset-item {
flex-direction: column;
gap: 15px;
}
.asset-info {
padding-right: 0;
width: 100%;
}
.asset-item > div:last-child {
width: 100%;
}
} }
</style> </style>
{% endblock %} {% endblock %}

View File

@@ -226,6 +226,19 @@
</select> </select>
</div> </div>
<div class="input-group">
<label for="balance-mode">Режим баланса</label>
<select id="balance-mode" name="balance_mode">
<option value="standard">⚖️ Стандартный (сбалансированный)</option>
<option value="easy">🟢 Легкий (для новичков)</option>
<option value="hard">🔴 Сложный (для экспертов)</option>
<option value="tournament">🏆 Турнирный (соревновательный)</option>
</select>
<small style="color: var(--light-text);">
Влияет на цены, налоги, доступность активов
</small>
</div>
<div class="input-group"> <div class="input-group">
<label for="start-capital">Стартовый капитал</label> <label for="start-capital">Стартовый капитал</label>
<input type="number" id="start-capital" name="start_capital" <input type="number" id="start-capital" name="start_capital"