Files
cm/app.py
2026-02-03 00:16:30 +03:00

1419 lines
51 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import json
from datetime import datetime, timedelta
from flask import Flask, render_template, request, jsonify, session, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from flask_socketio import SocketIO, emit, join_room, leave_room, send
from config import config
import random
import logging
from werkzeug.security import generate_password_hash, check_password_hash
# Настройка логирования
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# Инициализация Flask
app = Flask(__name__)
app.config.from_object(config)
# Инициализация расширений
db = SQLAlchemy(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
socketio = SocketIO(app, cors_allowed_origins="*")
# --- МОДЕЛИ БАЗЫ ДАННЫХ ---
class GameRoom(db.Model):
__tablename__ = 'game_rooms'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
code = db.Column(db.String(10), unique=True, nullable=False)
creator_id = db.Column(db.Integer, db.ForeignKey('users.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
status = db.Column(db.String(20), default='waiting') # waiting, playing, finished
current_month = db.Column(db.Integer, default=1)
total_months = db.Column(db.Integer, default=12)
start_capital = db.Column(db.Integer, default=100000)
settings = db.Column(db.Text, default='{}') # JSON с настройками
# Связи - убираем lazy='dynamic' для players
players = db.relationship('GamePlayer', backref='room', lazy='select') # Изменено
game_state = db.relationship('GameState', backref='room', uselist=False)
class GamePlayer(db.Model):
__tablename__ = 'game_players'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
room_id = db.Column(db.Integer, db.ForeignKey('game_rooms.id'))
joined_at = db.Column(db.DateTime, default=datetime.utcnow)
is_ready = db.Column(db.Boolean, default=False)
is_admin = db.Column(db.Boolean, default=False)
# Игровые данные
capital = db.Column(db.Float, default=100000)
ability = db.Column(db.String(50))
assets = db.Column(db.Text, default='[]') # JSON с активами
position = db.Column(db.Integer, default=0) # Позиция в рейтинге
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(256))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
# Статистика
total_games = db.Column(db.Integer, default=0)
games_won = db.Column(db.Integer, default=0)
total_earnings = db.Column(db.Float, default=0)
is_active = db.Column(db.Boolean, default=True)
# Связи
game_players = db.relationship('GamePlayer', backref='user', lazy='dynamic')
rooms_created = db.relationship('GameRoom', backref='creator', lazy='dynamic')
def set_password(self, password):
"""Установка хеша пароля"""
self.password_hash = generate_password_hash(password)
def check_password(self, password):
"""Проверка пароля"""
if not self.password_hash:
return False
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
class GameState(db.Model):
__tablename__ = 'game_states'
id = db.Column(db.Integer, primary_key=True)
room_id = db.Column(db.Integer, db.ForeignKey('game_rooms.id'), unique=True)
phase = db.Column(db.String(20), default='action') # action, market, event, results
phase_end = db.Column(db.DateTime)
market_data = db.Column(db.Text, default='{}') # JSON с данными рынка
events = db.Column(db.Text, default='[]') # JSON с событиями
history = db.Column(db.Text, default='[]') # JSON с историей
updated_at = db.Column(db.DateTime, default=datetime.utcnow)
# --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
def generate_room_code():
"""Генерация уникального кода комнаты"""
import string
characters = string.ascii_uppercase + string.digits
return ''.join(random.choice(characters) for _ in range(6))
def calculate_market_changes(room_id):
"""Расчет изменений на рынке (по формулам из концепции)"""
room = GameRoom.query.get(room_id)
players = GamePlayer.query.filter_by(room_id=room_id).all()
# Пример расчета (упрощенный)
market_data = json.loads(room.game_state.market_data) if room.game_state else {}
# Активы и их базовые цены
assets = {
'stock_gazprom': {'price': 1000, 'volatility': 0.2},
'real_estate': {'price': 3000000, 'volatility': 0.1},
'bitcoin': {'price': 1850000, 'volatility': 0.3},
'oil': {'price': 5000, 'volatility': 0.25}
}
# Считаем спрос по каждому активу
for asset_id, asset_data in assets.items():
demand = 0
for player in players:
player_assets = json.loads(player.assets)
for player_asset in player_assets:
if player_asset.get('id') == asset_id:
demand += player_asset.get('quantity', 0)
# Формула влияния спроса
total_players = len(players)
if total_players > 0:
price_change = asset_data['volatility'] * (demand / (total_players * 10))
new_price = asset_data['price'] * (1 + price_change)
# Эффект перегрева
if new_price > asset_data['price'] * (1 + 2 * asset_data['volatility']):
new_price *= random.uniform(0.7, 0.9)
assets[asset_id]['price'] = new_price
# Случайные события
events = [
('boom_oil', 'Бум нефти', {'oil': 1.3}, 'positive'),
('cyber_attack', 'Кибератака', {'bitcoin': 0.5}, 'negative'),
('elections', 'Выборы президента', {'all': 0.95}, 'neutral'),
('sanctions', 'Санкции', {'stock_gazprom': 0.65, 'oil': 0.8}, 'negative')
]
event_name, event_description, event_effects, event_type = random.choice(events)
# Применяем эффекты события
for asset_id, multiplier in event_effects.items():
if asset_id == 'all':
for key in assets.keys():
assets[key]['price'] *= multiplier
elif asset_id in assets:
assets[asset_id]['price'] *= multiplier
return {
'assets': assets,
'event': {
'name': event_name,
'description': event_description,
'type': event_type,
'effects': event_effects
},
'timestamp': datetime.utcnow().isoformat()
}
# --- FLASK-LOGIN ---
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# --- РОУТЫ ---
# Главная страница теперь будет index.html с описанием
@app.route('/')
def index():
return render_template('index.html')
# Старый маршрут index теперь перенаправляет на главную
@app.route('/home')
def home():
return redirect(url_for('index'))
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('rooms'))
if request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
remember = request.form.get('remember', False)
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
if not user.is_active:
flash('Аккаунт заблокирован', 'error')
else:
login_user(user, remember=remember)
user.last_seen = datetime.utcnow()
db.session.commit()
flash(f'Добро пожаловать, {username}!', 'success')
# Редирект на следующую страницу или rooms
next_page = request.args.get('next')
return redirect(next_page or url_for('rooms'))
else:
flash('Неверное имя пользователя или пароль', 'error')
return render_template('login.html')
@app.route('/quick_login/<username>')
def quick_login(username):
"""Быстрый вход для тестирования (только для разработки!)"""
user = User.query.filter_by(username=username).first()
if user:
login_user(user)
flash(f'Быстрый вход как {username}', 'info')
return redirect(url_for('rooms'))
# Если пользователя нет, создаем его
user = User(
username=username,
email=f'{username}@test.com',
is_active=True
)
user.set_password('test123')
db.session.add(user)
db.session.commit()
login_user(user)
flash(f'Создан и вошли как {username}', 'success')
return redirect(url_for('rooms'))
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('rooms'))
if request.method == 'POST':
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '')
password2 = request.form.get('password2', '')
# Простая валидация
errors = []
if not username:
errors.append('Введите имя пользователя')
elif len(username) < 3:
errors.append('Имя пользователя должно быть не менее 3 символов')
elif User.query.filter_by(username=username).first():
errors.append('Имя пользователя уже занято')
if not email:
errors.append('Введите email')
elif '@' not in email:
errors.append('Введите корректный email')
elif User.query.filter_by(email=email).first():
errors.append('Email уже используется')
if not password:
errors.append('Введите пароль')
elif len(password) < 4:
errors.append('Пароль должен быть не менее 4 символов')
elif password != password2:
errors.append('Пароли не совпадают')
if errors:
for error in errors:
flash(error, 'error')
return render_template('register.html')
try:
# Создаем пользователя
user = User(
username=username,
email=email
)
user.set_password(password)
db.session.add(user)
db.session.commit()
login_user(user, remember=True)
flash('Регистрация успешна! Добро пожаловать!', 'success')
return redirect(url_for('rooms'))
except Exception as e:
db.session.rollback()
flash(f'Ошибка при регистрации: {str(e)}', 'error')
return render_template('register.html')
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('index'))
@app.route('/rooms')
@login_required
def rooms():
"""Страница списка комнат"""
try:
# Получаем все активные комнаты
all_rooms = GameRoom.query.filter(
GameRoom.status.in_(['waiting', 'playing'])
).order_by(GameRoom.created_at.desc()).all()
# Для каждой комнаты получаем количество игроков
rooms_with_counts = []
for room in all_rooms:
# Получаем количество игроков
player_count = GamePlayer.query.filter_by(room_id=room.id).count()
# Получаем создателя
creator = User.query.get(room.creator_id) if room.creator_id else None
# Добавляем комнату с дополнительной информацией
rooms_with_counts.append({
'id': room.id,
'name': room.name,
'code': room.code,
'status': room.status,
'creator': creator,
'creator_id': room.creator_id,
'current_month': room.current_month,
'total_months': room.total_months,
'start_capital': room.start_capital,
'settings': room.settings,
'player_count': player_count,
'players': [] # Пустой список, так как не загружаем всех игроков
})
# Комнаты текущего пользователя
user_rooms = []
user_room_ids = GamePlayer.query.filter_by(
user_id=current_user.id
).with_entities(GamePlayer.room_id).all()
user_room_ids = [rid for (rid,) in user_room_ids]
for room_data in rooms_with_counts:
if room_data['id'] in user_room_ids:
user_rooms.append(room_data)
print(f"Found {len(rooms_with_counts)} rooms, user in {len(user_rooms)} rooms")
return render_template('rooms.html',
rooms=rooms_with_counts,
user_rooms=user_rooms,
config=app.config)
except Exception as e:
print(f"Error in rooms route: {str(e)}")
import traceback
traceback.print_exc()
db.session.rollback()
flash(f'Ошибка при загрузке комнат: {str(e)}', 'error')
return render_template('rooms.html',
rooms=[],
user_rooms=[],
config=app.config)
@app.route('/room/create', methods=['POST'])
@login_required
def create_room():
"""Создание новой комнаты"""
try:
# Получаем данные из формы
room_name = request.form.get('name', 'Новая комната').strip()
total_months = int(request.form.get('total_months', 12))
start_capital = int(request.form.get('start_capital', 100000))
allow_loans = request.form.get('allow_loans') == 'on'
allow_black_market = request.form.get('allow_black_market') == 'on'
private_room = request.form.get('private_room') == 'on'
# Валидация
if not room_name:
return jsonify({'error': 'Введите название комнаты'}), 400
# Генерируем уникальный код комнаты
import string
import random
def generate_room_code():
chars = string.ascii_uppercase + string.digits
return ''.join(random.choice(chars) for _ in range(6))
room_code = generate_room_code()
# Создаем комнату
room = GameRoom(
name=room_name,
code=room_code,
creator_id=current_user.id,
total_months=total_months,
start_capital=start_capital,
settings=json.dumps({
'allow_loans': allow_loans,
'allow_black_market': allow_black_market,
'private_room': private_room
})
)
db.session.add(room)
db.session.commit()
# Добавляем создателя как игрока-администратора
player = GamePlayer(
user_id=current_user.id,
room_id=room.id,
is_admin=True,
is_ready=True,
capital=start_capital,
ability=random.choice([
'crisis_investor', 'lobbyist', 'predictor',
'golden_pillow', 'shadow_accountant', 'credit_magnate'
])
)
db.session.add(player)
db.session.commit()
# Создаем начальное состояние игры
game_state = GameState(
room_id=room.id,
market_data=json.dumps({
'initialized': True,
'assets': {
'stock_gazprom': {'price': 1000, 'volatility': 0.2},
'real_estate': {'price': 3000000, 'volatility': 0.1},
'bitcoin': {'price': 1850000, 'volatility': 0.3},
'oil': {'price': 5000, 'volatility': 0.25}
},
'last_update': datetime.utcnow().isoformat()
})
)
db.session.add(game_state)
db.session.commit()
# Отправляем успешный ответ
return jsonify({
'success': True,
'room_code': room.code,
'redirect': url_for('lobby', room_code=room.code)
})
except Exception as e:
db.session.rollback()
print(f"Error creating room: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({'error': f'Ошибка при создании комнаты: {str(e)}'}), 500
@app.route('/room/<room_code>')
@login_required
def lobby(room_code):
"""Лобби комнаты"""
room = GameRoom.query.filter_by(code=room_code).first_or_404()
# Получаем всех игроков комнаты
players = GamePlayer.query.filter_by(room_id=room.id).all()
# Получаем информацию о пользователях для игроков
players_with_users = []
for player in players:
user = User.query.get(player.user_id)
player.user = user # Добавляем объект пользователя к игроку
players_with_users.append(player)
# Проверяем, есть ли текущий пользователь в комнате
current_player = GamePlayer.query.filter_by(
user_id=current_user.id,
room_id=room.id
).first()
# Если игрока нет и комната в ожидании, добавляем его
if not current_player and room.status == 'waiting':
# Проверяем, есть ли место в комнате
if len(players) >= app.config['MAX_PLAYERS_PER_ROOM']:
flash('Комната заполнена', 'error')
return redirect(url_for('rooms'))
# Случайная способность
import random
abilities = [
'crisis_investor', 'lobbyist', 'predictor',
'golden_pillow', 'shadow_accountant', 'credit_magnate',
'bear_raid', 'fake_news', 'dividend_king',
'raider_capture', 'mafia_connections', 'economic_advisor',
'currency_speculator'
]
current_player = GamePlayer(
user_id=current_user.id,
room_id=room.id,
is_admin=False,
is_ready=False,
capital=room.start_capital,
ability=random.choice(abilities)
)
db.session.add(current_player)
db.session.commit()
players_with_users.append(current_player)
flash(f'Вы присоединились к комнате "{room.name}"', 'success')
elif not current_player:
flash('Вы не можете присоединиться к этой комнате', 'error')
return redirect(url_for('rooms'))
# Получаем создателя комнаты
creator = User.query.get(room.creator_id) if room.creator_id else None
return render_template('lobby.html',
room=room,
players=players_with_users,
current_player=current_player,
creator=creator,
config=app.config)
@app.route('/room/<room_code>/start', methods=['POST'])
@login_required
def start_room_game(room_code):
"""Начало игры в комнате"""
room = GameRoom.query.filter_by(code=room_code).first_or_404()
# Проверяем права администратора
player = GamePlayer.query.filter_by(
user_id=current_user.id,
room_id=room.id,
is_admin=True
).first()
if not player:
return jsonify({'error': 'Только администратор может начать игру'}), 403
if room.status != 'waiting':
return jsonify({'error': 'Игра уже начата или завершена'}), 400
# Проверяем минимальное количество игроков
player_count = GamePlayer.query.filter_by(room_id=room.id).count()
if player_count < 2:
return jsonify({'error': 'Нужно минимум 2 игрока для начала игры'}), 400
# Меняем статус комнаты
room.status = 'playing'
db.session.commit()
# Отправляем событие через WebSocket
socketio.emit('game_started', {
'room': room.code,
'message': 'Игра началась!'
}, room=room.code)
return jsonify({'success': True})
@app.route('/room/<room_code>/ready', methods=['POST'])
@login_required
def toggle_player_ready(room_code):
"""Изменение статуса готовности игрока"""
room = GameRoom.query.filter_by(code=room_code).first_or_404()
player = GamePlayer.query.filter_by(
user_id=current_user.id,
room_id=room.id
).first_or_404()
data = request.get_json()
is_ready = data.get('ready', not player.is_ready)
player.is_ready = is_ready
db.session.commit()
# Отправляем событие через WebSocket
socketio.emit('player_ready_changed', {
'user_id': current_user.id,
'username': current_user.username,
'is_ready': is_ready
}, room=room.code)
return jsonify({'success': True, 'is_ready': is_ready})
@app.route('/api/room/<room_code>/status')
@login_required
def get_room_status(room_code):
"""Получение статуса комнаты"""
room = GameRoom.query.filter_by(code=room_code).first_or_404()
player_count = GamePlayer.query.filter_by(room_id=room.id).count()
ready_count = GamePlayer.query.filter_by(room_id=room.id, is_ready=True).count()
return jsonify({
'status': room.status,
'player_count': player_count,
'ready_count': ready_count,
'current_month': room.current_month
})
# @app.route('/game/<room_code>')
# @login_required
# def game(room_code):
# room = GameRoom.query.filter_by(code=room_code).first_or_404()
#
# # Проверяем, что игрок в комнате и игра идет
# player = GamePlayer.query.filter_by(
# user_id=current_user.id,
# room_id=room.id
# ).first_or_404()
#
# if room.status != 'playing':
# return redirect(url_for('lobby', room_code=room_code))
#
# # Получаем данные игры
# game_state = room.game_state
# market_data = json.loads(game_state.market_data) if game_state else {}
#
# # Получаем всех игроков для лидерборда
# players = GamePlayer.query.filter_by(room_id=room.id).order_by(GamePlayer.capital.desc()).all()
#
# return render_template('game.html',
# room=room,
# player=player,
# players=players,
# game_state=game_state,
# market_data=market_data)
@app.route('/room/<room_code>/update', methods=['POST'])
@login_required
def update_room_settings(room_code):
"""Обновление настроек комнаты"""
room = GameRoom.query.filter_by(code=room_code).first_or_404()
# Проверяем права администратора
player = GamePlayer.query.filter_by(
user_id=current_user.id,
room_id=room.id,
is_admin=True
).first()
if not player:
return jsonify({'error': 'Только администратор может изменять настройки'}), 403
if room.status != 'waiting':
return jsonify({'error': 'Нельзя изменять настройки во время игры'}), 400
try:
room.name = request.form.get('name', room.name)
room.total_months = int(request.form.get('total_months', room.total_months))
room.start_capital = int(request.form.get('start_capital', room.start_capital))
# Обновляем настройки
settings = json.loads(room.settings) if room.settings else {}
settings['allow_loans'] = request.form.get('allow_loans') == 'on'
settings['allow_black_market'] = request.form.get('allow_black_market') == 'on'
room.settings = json.dumps(settings)
db.session.commit()
socketio.emit('room_updated', {
'room': room.code,
'name': room.name
}, room=room.code)
return jsonify({'success': True})
except Exception as e:
db.session.rollback()
return jsonify({'error': f'Ошибка при обновлении настроек: {str(e)}'}), 500
@app.route('/room/<room_code>/kick/<int:user_id>', methods=['POST'])
@login_required
def kick_player(room_code, user_id):
"""Выгнать игрока из комнаты"""
room = GameRoom.query.filter_by(code=room_code).first_or_404()
# Проверяем права администратора
admin_player = GamePlayer.query.filter_by(
user_id=current_user.id,
room_id=room.id,
is_admin=True
).first()
if not admin_player:
return jsonify({'error': 'Только администратор может выгонять игроков'}), 403
# Нельзя выгнать себя или другого администратора
if user_id == current_user.id:
return jsonify({'error': 'Нельзя выгнать самого себя'}), 400
player_to_kick = GamePlayer.query.filter_by(
user_id=user_id,
room_id=room.id
).first()
if not player_to_kick:
return jsonify({'error': 'Игрок не найден в комнате'}), 404
if player_to_kick.is_admin:
return jsonify({'error': 'Нельзя выгнать другого администратора'}), 400
# Удаляем игрока
db.session.delete(player_to_kick)
db.session.commit()
socketio.emit('player_kicked', {
'user_id': user_id,
'username': User.query.get(user_id).username if User.query.get(user_id) else 'Игрок'
}, room=room.code)
return jsonify({'success': True})
@app.route('/room/<room_code>/kick_all', methods=['POST'])
@login_required
def kick_all_players(room_code):
"""Выгнать всех игроков из комнаты"""
room = GameRoom.query.filter_by(code=room_code).first_or_404()
# Проверяем права администратора
admin_player = GamePlayer.query.filter_by(
user_id=current_user.id,
room_id=room.id,
is_admin=True
).first()
if not admin_player:
return jsonify({'error': 'Только администратор может выгонять игроков'}), 403
# Удаляем всех игроков кроме администратора
GamePlayer.query.filter_by(room_id=room.id).filter(
GamePlayer.user_id != current_user.id
).delete(synchronize_session=False)
db.session.commit()
socketio.emit('all_players_kicked', {
'room': room.code
}, room=room.code)
return jsonify({'success': True})
@app.route('/room/<room_code>/reset', methods=['POST'])
@login_required
def reset_room(room_code):
"""Сбросить комнату к начальному состоянию"""
room = GameRoom.query.filter_by(code=room_code).first_or_404()
# Проверяем права администратора
admin_player = GamePlayer.query.filter_by(
user_id=current_user.id,
room_id=room.id,
is_admin=True
).first()
if not admin_player:
return jsonify({'error': 'Только администратор может сбрасывать комнату'}), 403
# Сбрасываем статус комнаты
room.status = 'waiting'
room.current_month = 1
# Сбрасываем всех игроков
GamePlayer.query.filter_by(room_id=room.id).update({
'is_ready': False,
'capital': room.start_capital,
'assets': '[]',
'position': 0
})
# Сбрасываем состояние игры
game_state = GameState.query.filter_by(room_id=room.id).first()
if game_state:
game_state.phase = 'action'
game_state.phase_end = None
game_state.market_data = json.dumps({
'initialized': True,
'assets': {
'stock_gazprom': {'price': 1000, 'volatility': 0.2},
'real_estate': {'price': 3000000, 'volatility': 0.1},
'bitcoin': {'price': 1850000, 'volatility': 0.3},
'oil': {'price': 5000, 'volatility': 0.25}
},
'last_update': datetime.utcnow().isoformat()
})
game_state.events = '[]'
game_state.history = '[]'
db.session.commit()
socketio.emit('room_reset', {
'room': room.code
}, room=room.code)
return jsonify({'success': True})
@app.route('/api/room/<room_code>/players')
@login_required
def get_room_players(room_code):
"""Получение списка игроков комнаты без перезагрузки страницы"""
room = GameRoom.query.filter_by(code=room_code).first_or_404()
# Проверяем, состоит ли пользователь в комнате
current_player = GamePlayer.query.filter_by(
user_id=current_user.id,
room_id=room.id
).first()
if not current_player:
return jsonify({'error': 'Вы не в этой комнате'}), 403
# Получаем всех игроков
players = GamePlayer.query.filter_by(room_id=room.id).all()
players_data = []
for player in players:
user = User.query.get(player.user_id)
players_data.append({
'user_id': player.user_id,
'username': user.username if user else 'Игрок',
'is_admin': player.is_admin,
'is_ready': player.is_ready,
'capital': player.capital,
'ability_name': get_ability_name(player.ability),
'ability_description': get_ability_description(player.ability)
})
return jsonify({
'success': True,
'players': players_data,
'room_status': room.status
})
@app.route('/room/<room_code>/delete', methods=['POST'])
@login_required
def delete_room(room_code):
"""Удалить комнату"""
room = GameRoom.query.filter_by(code=room_code).first_or_404()
# Проверяем права администратора
admin_player = GamePlayer.query.filter_by(
user_id=current_user.id,
room_id=room.id,
is_admin=True
).first()
if not admin_player:
return jsonify({'error': 'Только администратор может удалить комнату'}), 403
# Удаляем комнату и все связанные данные
GamePlayer.query.filter_by(room_id=room.id).delete()
GameState.query.filter_by(room_id=room.id).delete()
db.session.delete(room)
db.session.commit()
return jsonify({'success': True})
@app.route('/api/game/<room_code>/assets')
@login_required
def get_assets(room_code):
room = GameRoom.query.filter_by(code=room_code).first_or_404()
game_state = room.game_state
if not game_state:
return jsonify({'error': 'Game state not found'}), 404
market_data = json.loads(game_state.market_data)
return jsonify(market_data)
@app.route('/api/game/<room_code>/action', methods=['POST'])
@login_required
def perform_action(room_code):
"""Выполнение действия игрока"""
room = GameRoom.query.filter_by(code=room_code).first_or_404()
# Проверяем, что сейчас фаза действий
if room.game_state.phase != 'action':
return jsonify({'error': 'Not action phase'}), 400
data = request.json
action_type = data.get('type')
player = GamePlayer.query.filter_by(
user_id=current_user.id,
room_id=room.id
).first_or_404()
# Обработка разных типов действий
if action_type == 'buy_asset':
asset_id = data.get('asset_id')
quantity = data.get('quantity', 1)
# Здесь должна быть логика покупки актива
# ...
# Обновляем капитал игрока
player.capital -= data.get('cost', 0)
# Сохраняем активы
assets = json.loads(player.assets)
assets.append({
'id': asset_id,
'quantity': quantity,
'purchase_price': data.get('price'),
'timestamp': datetime.utcnow().isoformat()
})
player.assets = json.dumps(assets)
db.session.commit()
# Отправляем обновление через WebSocket
socketio.emit('player_action', {
'player_id': current_user.id,
'player_name': current_user.username,
'action': action_type,
'asset_id': asset_id,
'quantity': quantity
}, room=room.code)
return jsonify({'success': True, 'new_capital': player.capital})
return jsonify({'error': 'Unknown action type'}), 400
@app.template_filter('from_json')
def from_json_filter(value):
"""Преобразует JSON строку в объект"""
if isinstance(value, str):
return json.loads(value)
return value
@app.template_filter('format_currency')
def format_currency_filter(value):
"""Форматирует число как валюту"""
if value is None:
return "0 ₽"
return f"{value:,.0f}".replace(",", " ")
# --- WEBSOCKET ОБРАБОТЧИКИ ---
@socketio.on('connect')
def handle_connect():
if current_user.is_authenticated:
logger.info(f'User {current_user.username} connected')
emit('connected', {'user_id': current_user.id, 'username': current_user.username})
@socketio.on('join_room')
def handle_join_room(data):
room_code = data.get('room')
if room_code and current_user.is_authenticated:
join_room(room_code)
logger.info(f'User {current_user.username} joined room {room_code}')
# Проверяем, было ли уже уведомление о входе
session_key = f'has_notified_join_{room_code}'
if not session.get(session_key):
# Отправляем уведомление другим игрокам
emit('player_joined', {
'user_id': current_user.id,
'username': current_user.username,
'timestamp': datetime.utcnow().isoformat(),
'is_reconnect': False
}, room=room_code, include_self=False)
# Отмечаем в сессии, что уведомление отправлено
session[session_key] = True
else:
# Это реконнект, отправляем тихое обновление
emit('player_reconnected', {
'user_id': current_user.id,
'username': current_user.username
}, room=room_code, include_self=False)
@socketio.on('leave_room')
def handle_leave_room(data):
room_code = data.get('room')
if room_code:
leave_room(room_code)
logger.info(f'User {current_user.username} left room {room_code}')
emit('player_left', {
'user_id': current_user.id,
'username': current_user.username
}, room=room_code)
@socketio.on('player_ready')
def handle_player_ready(data):
room_code = data.get('room')
is_ready = data.get('ready', True)
room = GameRoom.query.filter_by(code=room_code).first()
if room:
player = GamePlayer.query.filter_by(
user_id=current_user.id,
room_id=room.id
).first()
if player:
player.is_ready = is_ready
db.session.commit()
emit('player_ready_changed', {
'user_id': current_user.id,
'username': current_user.username,
'is_ready': is_ready
}, room=room_code)
@socketio.on('chat_message')
def handle_chat_message(data):
room_code = data.get('room')
message = data.get('message', '').strip()
if message and room_code:
emit('chat_message', {
'user_id': current_user.id,
'username': current_user.username,
'message': message,
'timestamp': datetime.utcnow().isoformat()
}, room=room_code)
@socketio.on('join_global_room')
def handle_join_global_room():
"""Присоединение к глобальной комнате для обновлений"""
join_room('global_updates')
print(f'User {current_user.username if current_user else "Guest"} joined global room')
@socketio.on('user_online')
def handle_user_online(data):
"""Обработка статуса онлайн пользователя"""
if current_user.is_authenticated:
current_user.last_seen = datetime.utcnow()
db.session.commit()
print(f'User {current_user.username} is online')
# Добавим обработчик для обновления комнат
@socketio.on('get_rooms')
def handle_get_rooms():
"""Отправка списка комнат"""
rooms = GameRoom.query.filter(GameRoom.status != 'finished').all()
rooms_data = []
for room in rooms:
players_count = GamePlayer.query.filter_by(room_id=room.id).count()
rooms_data.append({
'id': room.id,
'name': room.name,
'code': room.code,
'status': room.status,
'players': players_count,
'max_players': app.config['MAX_PLAYERS_PER_ROOM'],
'month': room.current_month,
'total_months': room.total_months
})
emit('rooms_list', {'rooms': rooms_data}, room=request.sid)
# --- КОМАНДЫ УПРАВЛЕНИЯ ---
@app.cli.command('init-db')
def init_db_command():
"""Инициализация базы данных"""
db.create_all()
print('База данных инициализирована.')
@app.cli.command('create-test-data')
def create_test_data():
"""Создание тестовых данных"""
from werkzeug.security import generate_password_hash
# Создаем тестового пользователя
test_user = User(
username='test',
email='test@example.com',
password_hash=generate_password_hash('test123')
)
db.session.add(test_user)
db.session.commit()
print('Тестовые данные созданы.')
@app.cli.command('create-test-users')
def create_test_users():
"""Создание тестовых пользователей"""
test_users = [
{'username': 'Игрок1', 'email': 'player1@test.com', 'password': '123456'},
{'username': 'Игрок2', 'email': 'player2@test.com', 'password': '123456'},
{'username': 'Инвестор', 'email': 'investor@test.com', 'password': '123456'},
{'username': 'Трейдер', 'email': 'trader@test.com', 'password': '123456'},
{'username': 'Банкир', 'email': 'banker@test.com', 'password': '123456'},
{'username': 'admin', 'email': 'admin@test.com', 'password': 'admin123'},
]
created_count = 0
for user_data in test_users:
# Проверяем, существует ли пользователь
existing_user = User.query.filter_by(username=user_data['username']).first()
if not existing_user:
user = User(
username=user_data['username'],
email=user_data['email'],
is_active=True
)
user.set_password(user_data['password'])
db.session.add(user)
created_count += 1
print(f'✓ Создан пользователь: {user_data["username"]}')
else:
print(f'⏭ Пользователь уже существует: {user_data["username"]}')
try:
db.session.commit()
print(f'\n✅ Создано {created_count} тестовых пользователей!')
print('\nТестовые учетные данные:')
print('------------------------')
for user_data in test_users:
print(f'Логин: {user_data["username"]}, Пароль: {user_data["password"]}')
except Exception as e:
db.session.rollback()
print(f'❌ Ошибка при создании пользователей: {str(e)}')
@app.cli.command('create-demo-rooms')
def create_demo_rooms():
"""Создание демонстрационных комнат"""
import random
demo_rooms = [
{
'name': '🤑 Быстрая игра для новичков',
'total_months': 6,
'start_capital': 50000,
'allow_loans': False,
'allow_black_market': False
},
{
'name': '📈 Турнир профессионалов',
'total_months': 12,
'start_capital': 200000,
'allow_loans': True,
'allow_black_market': True
},
{
'name': '👥 Игра с друзьями',
'total_months': 12,
'start_capital': 100000,
'allow_loans': True,
'allow_black_market': False
},
{
'name': '⚡ Экспресс-торги',
'total_months': 3,
'start_capital': 75000,
'allow_loans': False,
'allow_black_market': True
},
]
# Получаем случайного пользователя для создания комнат
users = User.query.all()
if not users:
print('❌ Нет пользователей. Сначала создайте тестовых пользователей.')
return
created_count = 0
for room_data in demo_rooms:
creator = random.choice(users)
# Генерируем уникальный код
import string
chars = string.ascii_uppercase + string.digits
room_code = ''.join(random.choice(chars) for _ in range(6))
# Проверяем, существует ли комната с таким кодом
while GameRoom.query.filter_by(code=room_code).first():
room_code = ''.join(random.choice(chars) for _ in range(6))
# Создаем комнату
room = GameRoom(
name=room_data['name'],
code=room_code,
creator_id=creator.id,
total_months=room_data['total_months'],
start_capital=room_data['start_capital'],
settings=json.dumps({
'allow_loans': room_data['allow_loans'],
'allow_black_market': room_data['allow_black_market'],
'private_room': False
})
)
db.session.add(room)
db.session.commit()
# Добавляем создателя в комнату
player = GamePlayer(
user_id=creator.id,
room_id=room.id,
is_admin=True,
is_ready=True,
capital=room_data['start_capital'],
ability=random.choice([
'crisis_investor', 'lobbyist', 'predictor',
'golden_pillow', 'shadow_accountant', 'credit_magnate'
])
)
db.session.add(player)
# Создаем начальное состояние игры
game_state = GameState(
room_id=room.id,
market_data=json.dumps({
'initialized': True,
'assets': {
'stock_gazprom': {'price': 1000, 'volatility': 0.2},
'real_estate': {'price': 3000000, 'volatility': 0.1},
'bitcoin': {'price': 1850000, 'volatility': 0.3},
'oil': {'price': 5000, 'volatility': 0.25}
},
'last_update': datetime.utcnow().isoformat()
})
)
db.session.add(game_state)
db.session.commit()
# Добавляем еще 2-3 случайных игроков в комнату
other_users = [u for u in users if u.id != creator.id]
if other_users:
for _ in range(random.randint(2, 3)):
if other_users:
random_user = random.choice(other_users)
other_users.remove(random_user)
player = GamePlayer(
user_id=random_user.id,
room_id=room.id,
is_admin=False,
is_ready=random.choice([True, False]),
capital=room_data['start_capital'],
ability=random.choice([
'crisis_investor', 'lobbyist', 'predictor',
'golden_pillow', 'shadow_accountant', 'credit_magnate'
])
)
db.session.add(player)
db.session.commit()
created_count += 1
print(f'✓ Создана демо-комната: {room_data["name"]} (код: {room_code})')
print(f'\n✅ Создано {created_count} демонстрационных комнат!')
@app.cli.command('reset-db')
def reset_db():
"""Полный сброс базы данных"""
confirmation = input('⚠️ Вы уверены, что хотите сбросить всю базу данных? (yes/no): ')
if confirmation.lower() != 'yes':
print('❌ Отменено')
return
# Удаляем все таблицы
db.drop_all()
print('🗑️ Таблицы удалены')
# Создаем заново
db.create_all()
print('✅ Таблицы созданы заново')
# Создаем тестовых пользователей
print('\n👥 Создание тестовых пользователей...')
ctx = app.test_request_context()
with ctx:
create_test_users.callback()
# Создаем демо-комнаты
print('\n🏢 Создание демонстрационных комнат...')
with ctx:
create_demo_rooms.callback()
print('\n🎉 База данных успешно сброшена и заполнена тестовыми данными!')
# --- ОБРАБОТЧИКИ ОШИБОК ---
from datetime import datetime
@app.errorhandler(404)
def not_found_error(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('500.html', now=datetime.now()), 500
@app.errorhandler(403)
def forbidden_error(error):
return render_template('403.html'), 403
# Создадим и 403 ошибку для полноты
@app.route('/403')
def forbidden_page():
return render_template('403.html'), 403
# Дополнительные функции
def get_ability_name(ability_code):
"""Получить название способности по коду"""
abilities = {
'crisis_investor': 'Кризисный инвестор',
'lobbyist': 'Лоббист',
'predictor': 'Предсказатель',
'golden_pillow': 'Золотая подушка',
'shadow_accountant': 'Теневая бухгалтерия',
'credit_magnate': 'Кредитный магнат',
'bear_raid': 'Медвежий набег',
'fake_news': 'Фейковые новости',
'dividend_king': 'Король дивидендов',
'raider_capture': 'Рейдерский захват',
'mafia_connections': 'Мафиозные связи',
'economic_advisor': 'Экономический советник',
'currency_speculator': 'Валютный спекулянт'
}
return abilities.get(ability_code, 'Неизвестная способность')
def get_ability_description(ability_code):
"""Получить описание способности по коду"""
descriptions = {
'crisis_investor': '+20% к доходу при падении рынка',
'lobbyist': 'Может временно менять правила налогообложения',
'predictor': 'Видит следующее случайное событие',
'golden_pillow': 'Может защитить 20% капитала от кризисов',
'shadow_accountant': 'Раз в игру уменьшить налоги на 50%',
'credit_magnate': 'Может давать кредиты другим игрокам',
'bear_raid': 'Вызвать искусственное падение цены актива на 15%',
'fake_news': 'Подменить случайное событие на выгодное',
'dividend_king': 'Получать +10% дохода от всех акций',
'raider_capture': 'Попытаться отобрать 5% капитала у лидера',
'mafia_connections': 'Отменить одно негативное событие',
'economic_advisor': 'Раз в 3 месяца изменить налоговую ставку',
'currency_speculator': 'Отвязать рубль от нефти на 1 месяц'
}
return descriptions.get(ability_code, 'Описание отсутствует')
# Добавим функции в контекст шаблонов
@app.context_processor
def utility_processor():
return dict(
get_ability_name=get_ability_name,
get_ability_description=get_ability_description
)
# --- ТОЧКА ВХОДА ---
if __name__ == '__main__':
# Создаем таблицы если их нет
with app.app_context():
db.create_all()
# Запускаем сервер
socketio.run(app,
host='0.0.0.0',
port=5000,
debug=True,
allow_unsafe_werkzeug=True)