Buy Sell
This commit is contained in:
17
README.md
Normal file
17
README.md
Normal 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
|
||||||
736
app.py
736
app.py
@@ -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({
|
||||||
'id': asset_id,
|
'success': False,
|
||||||
'quantity': quantity,
|
'error': f'Максимум {asset_config["max_per_player"]} {asset_config["name"]} на игрока'
|
||||||
'purchase_price': price,
|
}), 400
|
||||||
'timestamp': datetime.utcnow().isoformat()
|
|
||||||
})
|
|
||||||
|
|
||||||
# Обновляем капитал и активы игрока
|
# Проверяем месячные лимиты
|
||||||
|
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.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:
|
||||||
game_state = room.game_state
|
return jsonify({
|
||||||
if not game_state:
|
'success': False,
|
||||||
return jsonify({'success': False, 'error': 'Состояние игры не найдено'}), 404
|
'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
|
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
15
game_balance/__init__.py
Normal 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'
|
||||||
|
]
|
||||||
252
game_balance/assets_config.py
Normal file
252
game_balance/assets_config.py
Normal 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
34
game_balance/config.py
Normal 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': 'Турнирный'
|
||||||
|
}
|
||||||
74
game_balance/economy_config.py
Normal file
74
game_balance/economy_config.py
Normal 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 # Нет корреляции
|
||||||
102
game_balance/events_config.py
Normal file
102
game_balance/events_config.py
Normal 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': 'Лопнул финансовый пузырь. Рынки рухнули'
|
||||||
|
}
|
||||||
|
}
|
||||||
43
game_balance/game_config.py
Normal file
43
game_balance/game_config.py
Normal 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'
|
||||||
158
game_balance/players_config.py
Normal file
158
game_balance/players_config.py
Normal 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,
|
||||||
|
}
|
||||||
11
game_balance/test_balance/__init__.py
Normal file
11
game_balance/test_balance/__init__.py
Normal 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'
|
||||||
|
]
|
||||||
73
game_balance/test_balance/easy_mode.py
Normal file
73
game_balance/test_balance/easy_mode.py
Normal 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
|
||||||
73
game_balance/test_balance/hard_mode.py
Normal file
73
game_balance/test_balance/hard_mode.py
Normal 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
|
||||||
61
game_balance/test_balance/standard_mode.py
Normal file
61
game_balance/test_balance/standard_mode.py
Normal 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
|
||||||
|
}
|
||||||
83
game_balance/test_balance/tournament.py
Normal file
83
game_balance/test_balance/tournament.py
Normal 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
124
game_balance/validator.py
Normal 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
|
||||||
@@ -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 = '';
|
||||||
|
|
||||||
Object.entries(assets).forEach(([assetId, assetData]) => {
|
// Сохраняем категории глобально для использования в других функциях
|
||||||
const assetElement = document.createElement('div');
|
window.assetCategories = categories || {
|
||||||
assetElement.className = 'asset-item';
|
'bonds': {'name': 'Облигации', 'icon': '🏦'},
|
||||||
assetElement.innerHTML = `
|
'stocks': {'name': 'Акции', 'icon': '📈'},
|
||||||
<div class="asset-info">
|
'real_estate': {'name': 'Недвижимость', 'icon': '🏠'},
|
||||||
<div class="asset-name">
|
'crypto': {'name': 'Криптовалюта', 'icon': '💰'},
|
||||||
${getAssetName(assetId)}
|
'commodities': {'name': 'Сырьевые товары', 'icon': '⛽'},
|
||||||
</div>
|
'business': {'name': 'Бизнес', 'icon': '🏢'},
|
||||||
<div class="asset-meta">
|
'unique': {'name': 'Уникальные активы', 'icon': '🏆'}
|
||||||
Волатильность: ${(assetData.volatility * 100).toFixed(1)}%
|
};
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: right;">
|
|
||||||
<div class="asset-price">
|
|
||||||
${formatCurrency(assetData.price)}
|
|
||||||
</div>
|
|
||||||
<button onclick="showBuyAssetModal('${assetId}', ${assetData.price})"
|
|
||||||
class="button" style="margin-top: 5px; padding: 5px 10px; font-size: 0.9rem;">
|
|
||||||
Купить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
assetsList.appendChild(assetElement);
|
// Группируем активы по категориям
|
||||||
|
const groupedAssets = {};
|
||||||
|
|
||||||
|
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');
|
||||||
|
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 = `
|
||||||
|
<div class="asset-info" style="flex: 1;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 5px;">
|
||||||
|
<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 style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 5px;">
|
||||||
|
<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 style="text-align: right; min-width: 120px;">
|
||||||
|
<div style="font-size: 1.3rem; font-weight: 700; color: var(--primary-color); margin-bottom: 10px;">
|
||||||
|
${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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 %}
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user