commit 038b307d701b84b338f86b0dc279752a926f44f5 Author: Kavalar Date: Mon Feb 2 19:18:25 2026 +0300 fix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88aca21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +.venv +app.db \ No newline at end of file diff --git a/__pycache__/app.cpython-310.pyc b/__pycache__/app.cpython-310.pyc new file mode 100644 index 0000000..0a0cd1e Binary files /dev/null and b/__pycache__/app.cpython-310.pyc differ diff --git a/__pycache__/config.cpython-310.pyc b/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..feb30ea Binary files /dev/null and b/__pycache__/config.cpython-310.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..a495a7a --- /dev/null +++ b/app.py @@ -0,0 +1,1368 @@ +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'' + + +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/') +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/') +@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//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//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//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/') +# @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//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//kick/', 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//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//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('/room//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//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//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: + join_room(room_code) + logger.info(f'User {current_user.username} joined room {room_code}') + + # Отправляем уведомление другим игрокам + emit('player_joined', { + 'user_id': current_user.id, + 'username': current_user.username, + 'timestamp': datetime.utcnow().isoformat() + }, 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) \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..480d751 --- /dev/null +++ b/config.py @@ -0,0 +1,35 @@ +import os +from datetime import timedelta + +basedir = os.path.abspath(os.path.dirname(__file__)) + + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production' + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ + 'sqlite:///' + os.path.join(basedir, 'app.db') + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # Настройки игры + STARTING_CAPITAL = 100000 + MAX_PLAYERS_PER_ROOM = 10 + DEFAULT_GAME_MONTHS = 12 + + # Тайминги (в секундах) + ACTION_PHASE_DURATION = 120 + MARKET_PHASE_DURATION = 30 + EVENT_PHASE_DURATION = 30 + RESULTS_PHASE_DURATION = 45 + + # Пути + UPLOAD_FOLDER = os.path.join(basedir, 'static/uploads') + MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB + + @staticmethod + def init_app(app): + # Создаем папки если их нет + if not os.path.exists(Config.UPLOAD_FOLDER): + os.makedirs(Config.UPLOAD_FOLDER) + + +config = Config() \ No newline at end of file diff --git a/logo.py b/logo.py new file mode 100644 index 0000000..3231fe7 --- /dev/null +++ b/logo.py @@ -0,0 +1,23 @@ +# create_logo.py +from PIL import Image, ImageDraw, ImageFont +import os + +# Создаем папку если её нет +os.makedirs('static/images', exist_ok=True) + +# Создаем изображение +img = Image.new('RGB', (200, 60), color='#0088cc') +draw = ImageDraw.Draw(img) + +# Рисуем текст (нужен шрифт, или используем стандартный) +try: + font = ImageFont.truetype("arial.ttf", 20) +except: + font = ImageFont.load_default() + +# Текст логотипа +draw.text((10, 20), "💰 Капитал & Рынок", fill="white", font=font) + +# Сохраняем +img.save('static/images/logo.png') +print("Логотип создан: static/images/logo.png") \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..5596b44 --- /dev/null +++ b/main.py @@ -0,0 +1,16 @@ +# This is a sample Python script. + +# Press Shift+F10 to execute it or replace it with your code. +# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. + + +def print_hi(name): + # Use a breakpoint in the code line below to debug your script. + print(f'Hi, {name}') # Press Ctrl+F8 to toggle the breakpoint. + + +# Press the green button in the gutter to run the script. +if __name__ == '__main__': + print_hi('PyCharm') + +# See PyCharm help at https://www.jetbrains.com/help/pycharm/ diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..2cb2870 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,890 @@ +:root { + --primary-color: #0088cc; /* Telegram blue */ + --secondary-color: #f0f2f5; + --accent-color: #34b7f1; + --danger-color: #e53935; + --success-color: #4caf50; + --warning-color: #ff9800; + --text-color: #333; + --light-text: #707579; + --border-radius: 10px; + --box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + --transition: all 0.3s ease; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +} + +body { + background-color: var(--secondary-color); + color: var(--text-color); + line-height: 1.6; + max-width: 100%; + overflow-x: hidden; + min-height: 100vh; +} + +/* Контейнер */ +.container { + width: 100%; + max-width: 500px; + margin: 0 auto; + padding: 15px; + min-height: calc(100vh - 60px); +} + +/* Шапка */ +.header { + background-color: var(--primary-color); + color: white; + display: flex; + align-items: center; + padding: 10px 15px; + box-shadow: var(--box-shadow); + position: sticky; + top: 0; + z-index: 100; + height: 60px; +} + +.back-button { + background: none; + border: none; + color: white; + font-size: 1.2rem; + margin-right: 10px; + cursor: pointer; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: var(--transition); +} + +.back-button:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.logo-container { + display: flex; + align-items: center; + justify-content: center; + flex-grow: 1; +} + +.logo { + height: 30px; + max-width: 150px; + object-fit: contain; +} + +.header-title { + flex-grow: 1; + text-align: center; + font-size: 1.2rem; + font-weight: 600; +} + +/* Карточки */ +.card { + background-color: white; + border-radius: var(--border-radius); + padding: 15px; + margin-bottom: 15px; + box-shadow: var(--box-shadow); + transition: var(--transition); +} + +.card:hover { + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); +} + +.card h2, .card h3 { + margin-bottom: 15px; + color: var(--text-color); + font-weight: 600; +} + +.card h2 { + font-size: 1.4rem; +} + +.card h3 { + font-size: 1.2rem; + border-bottom: 2px solid var(--secondary-color); + padding-bottom: 8px; +} + +/* Списки */ +.player-list, .room-list { + list-style: none; +} + +.player-item, .room-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #eee; + transition: var(--transition); + cursor: pointer; +} + +.player-item:hover, .room-item:hover { + background-color: #f9f9f9; + border-radius: var(--border-radius); + padding: 12px; + margin: 0 -15px; +} + +.player-item:last-child, .room-item:last-child { + border-bottom: none; +} + +.player-info, .room-info { + display: flex; + align-items: center; + flex: 1; +} + +.player-avatar, .room-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: var(--accent-color); + display: flex; + align-items: center; + justify-content: center; + color: white; + margin-right: 12px; + font-weight: bold; + font-size: 1.1rem; + flex-shrink: 0; +} + +.player-capital { + font-weight: bold; + color: var(--success-color); + font-size: 1.1rem; +} + +.player-ability { + background-color: var(--secondary-color); + padding: 4px 8px; + border-radius: 15px; + font-size: 0.85rem; + color: var(--light-text); + max-width: 120px; + text-align: center; +} + +/* Кнопки */ +.button { + background-color: var(--primary-color); + color: white; + border: none; + border-radius: var(--border-radius); + padding: 12px 20px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + width: 100%; + margin-top: 10px; + transition: var(--transition); + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.button:hover { + background-color: #0077b3; + transform: translateY(-2px); +} + +.button:active { + transform: translateY(0); +} + +.button.secondary { + background-color: white; + color: var(--primary-color); + border: 2px solid var(--primary-color); +} + +.button.secondary:hover { + background-color: var(--primary-color); + color: white; +} + +.button.danger { + background-color: var(--danger-color); +} + +.button.danger:hover { + background-color: #d32f2f; +} + +.button.success { + background-color: var(--success-color); +} + +.button.success:hover { + background-color: #388e3c; +} + +.button.warning { + background-color: var(--warning-color); +} + +.button.warning:hover { + background-color: #f57c00; +} + +.button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none !important; +} + +/* Табы */ +.tab-container { + display: flex; + margin-bottom: 15px; + background-color: white; + border-radius: var(--border-radius); + overflow: hidden; + box-shadow: var(--box-shadow); +} + +.tab { + flex: 1; + text-align: center; + padding: 12px; + background-color: white; + border-bottom: 3px solid transparent; + cursor: pointer; + transition: var(--transition); + font-weight: 500; +} + +.tab:hover { + background-color: #f5f5f5; +} + +.tab.active { + border-bottom: 3px solid var(--primary-color); + font-weight: 600; + color: var(--primary-color); + background-color: #f0f8ff; +} + +.tab-content { + display: none; + animation: fadeIn 0.3s ease; +} + +.tab-content.active { + display: block; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Активы */ +.asset-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #eee; + transition: var(--transition); +} + +.asset-item:hover { + background-color: #f9f9f9; + border-radius: var(--border-radius); + padding: 12px; + margin: 0 -15px; +} + +.asset-name { + font-weight: 600; + font-size: 1rem; +} + +.asset-price { + font-weight: bold; + color: var(--success-color); + font-size: 1.1rem; +} + +.asset-price.negative { + color: var(--danger-color); +} + +.asset-price.neutral { + color: var(--light-text); +} + +.asset-change { + font-size: 0.85rem; + color: var(--light-text); + margin-top: 2px; +} + +.asset-change.positive { + color: var(--success-color); +} + +.asset-change.negative { + color: var(--danger-color); +} + +/* Прогресс-бар */ +.progress-container { + margin: 20px 0; + background-color: white; + padding: 15px; + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); +} + +.progress-label { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + font-size: 0.95rem; +} + +.progress-bar { + height: 12px; + background-color: #e0e0e0; + border-radius: 6px; + overflow: hidden; + margin-bottom: 5px; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary-color), var(--accent-color)); + width: 0%; + transition: width 0.5s ease; + border-radius: 6px; +} + +/* Таймер */ +.timer { + text-align: center; + font-size: 1.5rem; + font-weight: bold; + margin: 20px 0; + color: var(--primary-color); + background-color: white; + padding: 15px; + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); +} + +.timer.warning { + color: var(--warning-color); + animation: pulse 1s infinite; +} + +.timer.danger { + color: var(--danger-color); + animation: pulse 0.5s infinite; +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.7; } + 100% { opacity: 1; } +} + +/* Новости и события */ +.news-item { + padding: 12px 0; + border-bottom: 1px solid #eee; + transition: var(--transition); +} + +.news-item:hover { + background-color: #f9f9f9; + border-radius: var(--border-radius); + padding: 12px; + margin: 0 -15px; +} + +.news-title { + font-weight: 600; + margin-bottom: 5px; + font-size: 1rem; +} + +.news-impact { + font-size: 0.85rem; + margin-bottom: 5px; +} + +.impact-positive { + color: var(--success-color); + font-weight: 600; +} + +.impact-negative { + color: var(--danger-color); + font-weight: 600; +} + +.impact-neutral { + color: var(--warning-color); + font-weight: 600; +} + +/* Способности */ +.ability-item { + padding: 12px; + margin-bottom: 10px; + background-color: #f5f5f5; + border-radius: var(--border-radius); + cursor: pointer; + transition: var(--transition); + border-left: 4px solid var(--primary-color); +} + +.ability-item:hover { + background-color: #e0e0e0; + transform: translateX(5px); +} + +.ability-item.disabled { + opacity: 0.5; + cursor: not-allowed; + border-left-color: var(--light-text); +} + +.ability-item.disabled:hover { + transform: none; + background-color: #f5f5f5; +} + +.ability-name { + font-weight: 600; + color: var(--primary-color); + font-size: 1rem; + margin-bottom: 5px; +} + +.ability-description { + font-size: 0.9rem; + color: var(--light-text); + line-height: 1.4; +} + +.ability-cooldown { + font-size: 0.8rem; + color: var(--danger-color); + margin-top: 5px; + font-weight: 500; +} + +/* Формы авторизации */ +.auth-form { + display: flex; + flex-direction: column; + gap: 15px; +} + +.input-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.input-group label { + font-weight: 600; + color: var(--light-text); + font-size: 0.95rem; +} + +.input-group input, +.input-group select, +.input-group textarea { + padding: 12px; + border: 1px solid #ddd; + border-radius: var(--border-radius); + font-size: 1rem; + transition: var(--transition); + background-color: white; +} + +.input-group input:focus, +.input-group select:focus, +.input-group textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(0, 136, 204, 0.1); +} + +.auth-links { + display: flex; + justify-content: space-between; + margin-top: 10px; + padding-top: 15px; + border-top: 1px solid #eee; +} + +.auth-links a { + color: var(--primary-color); + text-decoration: none; + font-weight: 500; + transition: var(--transition); +} + +.auth-links a:hover { + text-decoration: underline; + color: #0077b3; +} + +/* Комнаты */ +.room-status { + font-size: 0.8rem; + padding: 5px 10px; + border-radius: 15px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + white-space: nowrap; + margin-left: 10px; +} + +.room-status.waiting { + background-color: #e3f2fd; + color: #1976d2; +} + +.room-status.playing { + background-color: #e8f5e9; + color: #388e3c; +} + +.room-status.full { + background-color: #ffebee; + color: #d32f2f; +} + +.room-status.finished { + background-color: #f5f5f5; + color: #757575; +} + +.room-meta { + font-size: 0.85rem; + color: var(--light-text); + margin-top: 3px; +} + +.search-bar { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +.search-bar input { + flex: 1; + padding: 12px; + border: 1px solid #ddd; + border-radius: var(--border-radius); + font-size: 1rem; + transition: var(--transition); +} + +.search-bar input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(0, 136, 204, 0.1); +} + +.search-bar button { + padding: 0 20px; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: var(--border-radius); + cursor: pointer; + font-size: 1.1rem; + transition: var(--transition); + min-width: 50px; +} + +.search-bar button:hover { + background-color: #0077b3; +} + +/* Всплывающие сообщения */ +.flash-messages { + position: fixed; + top: 70px; + left: 50%; + transform: translateX(-50%); + z-index: 1000; + width: 90%; + max-width: 500px; +} + +.flash-message { + background-color: white; + color: var(--text-color); + padding: 15px; + margin-bottom: 10px; + border-radius: var(--border-radius); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + border-left: 4px solid var(--primary-color); + animation: slideDown 0.3s ease; +} + +.flash-message.success { + border-left-color: var(--success-color); +} + +.flash-message.error { + border-left-color: var(--danger-color); +} + +.flash-message.warning { + border-left-color: var(--warning-color); +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Скроллбар */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: var(--primary-color); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #0077b3; +} + +/* Адаптивные стили */ +@media (max-width: 480px) { + .container { + padding: 10px; + } + + .header { + padding: 10px; + font-size: 1rem; + } + + .logo { + height: 25px; + max-width: 120px; + } + + .card { + padding: 12px; + } + + .tab { + padding: 10px; + font-size: 0.9rem; + } + + .button { + padding: 10px 15px; + font-size: 0.95rem; + } + + .player-avatar, .room-avatar { + width: 35px; + height: 35px; + font-size: 1rem; + } + + .timer { + font-size: 1.3rem; + padding: 12px; + } + + .asset-price { + font-size: 1rem; + } +} + +@media (max-width: 350px) { + .header-title { + font-size: 1rem; + } + + .tab-container { + flex-direction: column; + } + + .tab { + padding: 8px; + } +} + +/* Дополнительные утилиты */ +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.text-left { + text-align: left; +} + +.mt-1 { margin-top: 5px; } +.mt-2 { margin-top: 10px; } +.mt-3 { margin-top: 15px; } +.mt-4 { margin-top: 20px; } +.mt-5 { margin-top: 25px; } + +.mb-1 { margin-bottom: 5px; } +.mb-2 { margin-bottom: 10px; } +.mb-3 { margin-bottom: 15px; } +.mb-4 { margin-bottom: 20px; } +.mb-5 { margin-bottom: 25px; } + +.p-1 { padding: 5px; } +.p-2 { padding: 10px; } +.p-3 { padding: 15px; } +.p-4 { padding: 20px; } +.p-5 { padding: 25px; } + +.hidden { + display: none !important; +} + +.flex { + display: flex; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.justify-between { + justify-content: space-between; +} + +.gap-1 { gap: 5px; } +.gap-2 { gap: 10px; } +.gap-3 { gap: 15px; } +.gap-4 { gap: 20px; } +.gap-5 { gap: 25px; } + +/* Стили для уведомлений flash */ +.flash-messages { + position: fixed; + top: 70px; + left: 50%; + transform: translateX(-50%); + z-index: 10000; + width: 90%; + max-width: 500px; +} + +.flash-message { + background-color: white; + color: var(--text-color); + padding: 15px 20px; + margin-bottom: 10px; + border-radius: var(--border-radius); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + border-left: 4px solid var(--primary-color); + animation: slideDown 0.3s ease; + display: flex; + justify-content: space-between; + align-items: center; +} + +.flash-message.success { + border-left-color: var(--success-color); +} + +.flash-message.error { + border-left-color: var(--danger-color); +} + +.flash-message.info { + border-left-color: var(--accent-color); +} + +.flash-message.warning { + border-left-color: var(--warning-color); +} + +.flash-close { + background: none; + border: none; + color: var(--light-text); + font-size: 1.2rem; + cursor: pointer; + margin-left: 10px; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-20px) translateX(-50%); + } + to { + opacity: 1; + transform: translateY(0) translateX(-50%); + } +} + +/* Иконки */ +.icon { + display: inline-block; + width: 20px; + height: 20px; + background-size: contain; + background-repeat: no-repeat; + vertical-align: middle; +} + +.icon-home { background-image: url('data:image/svg+xml;utf8,'); } +.icon-settings { background-image: url('data:image/svg+xml;utf8,'); } +.icon-chat { background-image: url('data:image/svg+xml;utf8,'); } +.icon-stats { background-image: url('data:image/svg+xml;utf8,'); } +.icon-help { background-image: url('data:image/svg+xml;utf8,'); } \ No newline at end of file diff --git a/static/images/logo.png b/static/images/logo.png new file mode 100644 index 0000000..38d50ec Binary files /dev/null and b/static/images/logo.png differ diff --git a/static/js/game.js b/static/js/game.js new file mode 100644 index 0000000..959f688 --- /dev/null +++ b/static/js/game.js @@ -0,0 +1,129 @@ +// Логика игрового процесса + +class Game { + constructor() { + this.month = 1; + this.phase = 'action'; // action, market, event, results + this.playerCapital = 100000; + this.assets = []; + this.abilities = []; + } + + init() { + this.loadGameState(); + this.startPhaseTimer(); + this.updateUI(); + } + + startPhaseTimer() { + const phaseDurations = { + action: 120, // 2 минуты + market: 30, // 30 секунд + event: 30, // 30 секунд + results: 45 // 45 секунд + }; + + this.timer = new GameTimer('phase-timer', phaseDurations[this.phase]); + this.timer.onComplete = () => this.nextPhase(); + this.timer.start(); + } + + nextPhase() { + const phases = ['action', 'market', 'event', 'results']; + const currentIndex = phases.indexOf(this.phase); + const nextIndex = (currentIndex + 1) % phases.length; + + this.phase = phases[nextIndex]; + + if (this.phase === 'results') { + this.endMonth(); + } + + this.updatePhaseDisplay(); + this.startPhaseTimer(); + } + + endMonth() { + this.month++; + this.calculateMarketChanges(); + this.applyRandomEvents(); + this.updateLeaderboard(); + + if (this.month > 12) { + this.endGame(); + } + } + + updateUI() { + document.getElementById('game-month').textContent = `Месяц ${this.month}`; + document.getElementById('player-capital').textContent = formatCurrency(this.playerCapital); + + // Обновление прогресс-бара + const progress = (this.playerCapital / 500000) * 100; // Пример: цель 500к + document.getElementById('capital-progress').style.width = Math.min(progress, 100) + '%'; + } + + updatePhaseDisplay() { + const phaseNames = { + action: 'Фаза действий', + market: 'Реакция рынка', + event: 'Случайные события', + results: 'Итоги месяца' + }; + + document.getElementById('phase-timer').textContent = phaseNames[this.phase]; + } + + calculateMarketChanges() { + // Здесь будет сложная логика из концепции игры + console.log('Расчет изменений рынка...'); + } + + applyRandomEvents() { + // Применение случайных и политических событий + console.log('Применение событий...'); + } + + updateLeaderboard() { + // Обновление таблицы лидеров + console.log('Обновление лидерборда...'); + } + + endGame() { + alert('Игра завершена! Победитель: ...'); + window.location.href = 'rooms.html'; + } + + loadGameState() { + // Загрузка состояния игры из localStorage или сервера + const saved = localStorage.getItem('gameState'); + if (saved) { + const state = JSON.parse(saved); + Object.assign(this, state); + } + } + + saveGameState() { + localStorage.setItem('gameState', JSON.stringify({ + month: this.month, + phase: this.phase, + playerCapital: this.playerCapital, + assets: this.assets, + abilities: this.abilities + })); + } +} + +// Инициализация игры +function initGame() { + window.game = new Game(); + game.init(); + + // Автосохранение каждые 30 секунд + setInterval(() => game.saveGameState(), 30000); + + // Обработка завершения хода + document.getElementById('end-turn')?.addEventListener('click', () => { + game.nextPhase(); + }); +} \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..a62adab --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,87 @@ +// Общие функции для всех страниц + +// Инициализация табов +function initTabs() { + const tabs = document.querySelectorAll('.tab'); + if (tabs.length > 0) { + tabs.forEach(tab => { + tab.addEventListener('click', function() { + const tabId = this.getAttribute('data-tab'); + if (tabId) { + switchTab(tabId); + } + }); + }); + } +} + +function switchTab(tabId) { + // Удаляем активный класс у всех вкладок + document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); + document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); + + // Добавляем активный класс выбранной вкладке + const activeTab = document.querySelector(`.tab[data-tab="${tabId}"]`); + const activeContent = document.getElementById(`${tabId}-tab`); + + if (activeTab) activeTab.classList.add('active'); + if (activeContent) activeContent.classList.add('active'); +} + +// Таймер обратного отсчета +class GameTimer { + constructor(elementId, duration) { + this.element = document.getElementById(elementId); + this.duration = duration; + this.timeLeft = duration; + this.interval = null; + } + + start() { + this.updateDisplay(); + this.interval = setInterval(() => { + this.timeLeft--; + this.updateDisplay(); + + if (this.timeLeft <= 0) { + this.stop(); + if (this.onComplete) this.onComplete(); + } + }, 1000); + } + + stop() { + if (this.interval) { + clearInterval(this.interval); + this.interval = null; + } + } + + updateDisplay() { + if (this.element) { + const minutes = Math.floor(this.timeLeft / 60); + const seconds = this.timeLeft % 60; + this.element.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; + } + } +} + +// Форматирование чисел (валюты) +function formatCurrency(amount) { + return new Intl.NumberFormat('ru-RU', { + style: 'currency', + currency: 'RUB', + minimumFractionDigits: 0 + }).format(amount); +} + +// Инициализация при загрузке страницы +document.addEventListener('DOMContentLoaded', function() { + initTabs(); + + // Инициализация текущего пользователя + const currentUser = localStorage.getItem('currentUser'); + if (currentUser && document.getElementById('current-user')) { + document.getElementById('current-user').textContent = currentUser; + } +}); \ No newline at end of file diff --git a/templates/403.html b/templates/403.html new file mode 100644 index 0000000..afe0168 --- /dev/null +++ b/templates/403.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block title %}Доступ запрещён - Капитал & Рынок{% endblock %} + +{% block screen_content %} +
+ +
+ +
+
+ +
+
+
403
+

Доступ запрещён

+

У вас недостаточно прав для доступа к этой странице.

+ +
+
🔒💰
+

+ Эта комната может быть приватной или игра уже началась. +

+
+ + +
+ +
+

Возможные причины:

+
    +
  • Вы не авторизованы в системе
  • +
  • У вас нет прав для доступа к этой комнате
  • +
  • Игра уже началась и присоединение невозможно
  • +
  • Комната является приватной
  • +
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 0000000..00743b8 --- /dev/null +++ b/templates/404.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} + +{% block title %}Страница не найдена - Капитал & Рынок{% endblock %} + +{% block screen_content %} +
+ +
+ +
+
+ +
+
+
404
+

Страница не найдена

+

К сожалению, запрашиваемая страница не существует или была перемещена.

+ +
+
💼📉
+

+ Возможно, комната была закрыта или игра завершена. +

+
+ + +
+ +
+

Что можно сделать?

+
    +
  • Проверьте правильность URL-адреса
  • +
  • Создайте новую игровую комнату
  • +
  • Присоединитесь к другой комнате
  • +
  • Обратитесь к администратору, если это ошибка
  • +
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 0000000..1bf7eea --- /dev/null +++ b/templates/500.html @@ -0,0 +1,74 @@ +{% extends "base.html" %} + +{% block title %}Ошибка сервера - Капитал & Рынок{% endblock %} + +{% block screen_content %} +
+ +
+ +
+
+ +
+
+
500
+

Ошибка сервера

+

Произошла внутренняя ошибка сервера. Мы уже работаем над её устранением.

+ +
+
💥📊
+

+ Рынок временно не работает. Пожалуйста, попробуйте позже. +

+
+ + +
+ +
+

Техническая информация

+

+ Если ошибка повторяется, пожалуйста, свяжитесь с администратором: +

+ +
+
Время ошибки: {{ now.strftime('%Y-%m-%d %H:%M:%S') if now else '' }}
+
Путь: {{ request.path if request else '' }}
+
+ +
+ +
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..b6ef7b0 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,69 @@ + + + + + + {% block title %}Капитал & Рынок{% endblock %} + + + + + + + + + {% block head %}{% endblock %} + + + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
+ {{ message }} + +
+ {% endfor %} +
+ {% endif %} + {% endwith %} + + + {% block content %}{% endblock %} + + + + + + + + {% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/templates/game.html b/templates/game.html new file mode 100644 index 0000000..fe157c4 --- /dev/null +++ b/templates/game.html @@ -0,0 +1,40 @@ + + + + + + Капитал & Рынок - Игра + + + +
+
+ +
+ +
+ Месяц 1 +
+ +
+ + + +
+
+ + + + + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..6120269 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,210 @@ +{% extends "base.html" %} + +{% block title %}Капитал & Рынок - Экономическая стратегия{% endblock %} + +{% block content %} +
+ +
+
+ {% if url_for('static', filename='images/logo.png') %} + + + {% else %} + 💰 Капитал & Рынок + {% endif %} +
+
+ + +
+ +
+

Капитал & Рынок

+

+ Стань успешным инвестором в динамичной экономической стратегии! +

+ +
+
💰📈🏦
+
+ + +
+ + +
+

Особенности игры

+
+
+
🎮
+
+

Динамичный геймплей

+

+ Каждый месяц - новые решения, события и вызовы. Адаптируйтесь к меняющемуся рынку! +

+
+
+ +
+
👥
+
+

Мультиплеер до 10 игроков

+

+ Соревнуйтесь с друзьями или случайными соперниками. Создавайте альянсы и заключайте сделки! +

+
+
+ +
+
💡
+
+

13 уникальных способностей

+

+ Каждый игрок получает особую способность: от Кризисного инвестора до Теневого бухгалтера. +

+
+
+ +
+
📊
+
+

Реалистичная экономика

+

+ Рынок реагирует на действия всех игроков. Ваши решения влияют на цены активов! +

+
+
+
+
+ + +
+

Как начать играть?

+
+
+
+ 1 +
+
+

Создайте аккаунт

+

+ Зарегистрируйтесь или войдите как гость. Это займет менее минуты! +

+
+
+ +
+
+ 2 +
+
+

Присоединитесь к комнате

+

+ Выберите существующую комнату или создайте свою. Настройте правила игры. +

+
+
+ +
+
+ 3 +
+
+

Выберите способность

+

+ Получите случайную уникальную способность и стартовый капитал 100,000 ₽. +

+
+
+ +
+
+ 4 +
+
+

Станьте самым богатым!

+

+ Инвестируйте в акции, недвижимость, бизнес. Обыграйте конкурентов за 12 месяцев! +

+
+
+
+
+ + +
+

Игровая статистика

+
+
+
1,234
+
Активных игроков
+
+
+
567
+
Игровых комнат
+
+
+
89
+
Турниров
+
+
+
12
+
Уникальных способностей
+
+
+
+ + +
+

© 2024 Капитал & Рынок. Экономическая стратегия в реальном времени.

+

+ Правила | + Контакты | + Поддержка +

+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/lobby.html b/templates/lobby.html new file mode 100644 index 0000000..66889f6 --- /dev/null +++ b/templates/lobby.html @@ -0,0 +1,1085 @@ +{% extends "base.html" %} + +{% block title %}Лобби: {{ room.name }} - Капитал & Рынок{% endblock %} + +{% block content %} +
+ +
+ +
+ + 💰 Лобби + +
+
+ + {% if current_user.is_authenticated %} + {{ current_user.username }} + {% endif %} + +
+
+ + +
+ +
+
+
+

{{ room.name }}

+
+ Код комнаты: {{ room.code }} + +
+
+
+ {% if room.status == 'waiting' %} + Ожидание + {% elif room.status == 'playing' %} + Игра идет + {% elif room.status == 'finished' %} + Завершена + {% else %} + {{ room.status }} + {% endif %} +
+
+ +
+
+
+
Игроков:
+
+ {{ players|length }}/{{ config.MAX_PLAYERS_PER_ROOM }} +
+
+
+
Длительность:
+
+ {{ room.total_months }} месяцев +
+
+
+
Стартовый капитал:
+
+ {{ room.start_capital|format_currency }} +
+
+
+
Создатель:
+
+ {{ room.creator.username if room.creator else 'Неизвестно' }} +
+
+
+
+ + + {% if room.settings %} + {% set settings_dict = room.settings|from_json %} +
+

Настройки игры:

+
+ {% if settings_dict.allow_loans %} + + ✅ Кредиты разрешены + + {% else %} + + ❌ Кредиты запрещены + + {% endif %} + + {% if settings_dict.allow_black_market %} + + ⚫ Чёрный рынок + + {% else %} + + ⚪ Без чёрного рынка + + {% endif %} + + {% if settings_dict.private_room %} + + 🔒 Приватная + + {% else %} + + 🔓 Публичная + + {% endif %} +
+
+ {% endif %} + + +
+ {% if current_player.is_admin and room.status == 'waiting' %} + + {% elif room.status == 'playing' %} + + {% endif %} + + {% if room.status == 'waiting' %} + + {% endif %} + + + + + + {% if current_player.is_admin %} + + {% endif %} +
+ + + {% if room.status == 'waiting' %} +
+
+ Готовы к старту: + {{ players|selectattr('is_ready')|list|length }}/{{ players|length }} +
+
+
+
+
+ {% if players|length < 2 %} +
+ ⚠️ Нужно минимум 2 игрока для начала игры +
+ {% endif %} +
+ {% endif %} +
+ + +
+

👥 Участники ({{ players|length }})

+ + {% if players %} +
    + {% for player in players %} +
  • +
    +
    + {{ player.user.username[0]|upper if player.user and player.user.username else '?' }} +
    +
    +
    + {{ player.user.username if player.user else 'Игрок' }} + {% if player.is_admin %} + Админ + {% endif %} + {% if player.user_id == current_user.id %} + Вы + {% endif %} +
    +
    + {{ player.capital|format_currency }} +
    +
    +
    +
    + {% if room.status == 'waiting' %} +
    + {% if player.is_ready %} + ✅ Готов + {% else %} + ⏳ Ожидание + {% endif %} +
    + {% endif %} +
    + {{ get_ability_name(player.ability) }} +
    +
    +
  • + {% endfor %} +
+ {% else %} +
+
👤
+

В комнате пока нет игроков

+

Пригласите друзей!

+
+ {% endif %} +
+ + +
+

💬 Чат комнаты

+
+
+ Чат загружается... +
+
+
+ + +
+
+ + +
+

📊 Статистика комнаты

+
+
+
{{ players|length }}
+
Игроков
+
+
+
{{ room.current_month }}
+
Текущий месяц
+
+
+
+ {{ (players|map(attribute='capital')|sum)|format_currency }} +
+
Общий капитал
+
+
+
+ {% if players|length > 0 %} + {{ ((players|selectattr('is_ready')|list|length / players|length * 100)|round(1)) }}% + {% else %} + 0% + {% endif %} +
+
Готовы
+
+
+
+
+
+ + + + + + + + + +{% endblock %} + +{% block scripts %} + + + +{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..689e437 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,114 @@ +{% extends "base.html" %} + +{% block title %}Вход - Капитал & Рынок{% endblock %} + +{% block screen_content %} +
+
+ +
+
+ +
+
+

Вход в игру

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+ +
+

Тестовые аккаунты

+

+ Для быстрого тестирования игры: +

+ + +
+
+ + + +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..27426da --- /dev/null +++ b/templates/register.html @@ -0,0 +1,178 @@ +{% extends "base.html" %} + +{% block title %}Регистрация - Капитал & Рынок{% endblock %} + +{% block screen_content %} +
+ +
+ +
+
+ +
+
+

Создание аккаунта

+ +
+
+ + + + Будет отображаться другим игрокам + +
+ +
+ + + + Только для восстановления пароля + +
+ +
+ + +
+ +
+ + +
+ + + + + + +
+
+ +
+

Почему стоит зарегистрироваться?

+
    +
  • 🎮 Сохраняйте прогресс в играх
  • +
  • 📊 Отслеживайте статистику и рейтинг
  • +
  • 👥 Создавайте приватные комнаты
  • +
  • 🏆 Участвуйте в турнирах и соревнованиях
  • +
  • 💬 Общайтесь с другими игроками
  • +
+
+
+ + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/rooms.html b/templates/rooms.html new file mode 100644 index 0000000..06f10bb --- /dev/null +++ b/templates/rooms.html @@ -0,0 +1,568 @@ +{% extends "base.html" %} + +{% block title %}Комнаты - Капитал & Рынок{% endblock %} + +{% block content %} +
+ +
+ +
+ + 💰 Комнаты + +
+
+ + {% if current_user.is_authenticated %} + {{ current_user.username }} + {% endif %} + +
+
+ + +
+ + + + + + +
+

📢 Доступные комнаты

+
+ {% if rooms %} + Найдено {{ rooms|length }} комнат + {% else %} + Нет доступных комнат + {% endif %} +
+ +
    + {% if rooms %} + {% for room in rooms %} +
  • +
    +
    + {{ room.player_count }} +
    +
    +
    + {{ room.name }} + + Месяц {{ room.current_month }}/{{ room.total_months }} + +
    +
    + Создатель: {{ room.creator.username if room.creator else 'Система' }} • + Игроков: {{ room.player_count }}/{{ config.MAX_PLAYERS_PER_ROOM }} +
    + {% if room.settings %} +
    + {% set settings = room.settings|from_json %} + {% if settings.allow_loans %} + Кредиты + {% endif %} + {% if settings.allow_black_market %} + Чёрный рынок + {% endif %} +
    + {% endif %} +
    +
    +
    + {% if room.status == 'waiting' %} + Ожидание + {% elif room.status == 'playing' %} + Игра идет + {% elif room.status == 'full' %} + Заполнена + {% else %} + {{ room.status }} + {% endif %} +
    +
  • + {% endfor %} + {% else %} +
  • +
    🏢
    +

    Пока нет доступных комнат

    +

    Создайте первую комнату!

    +
  • + {% endif %} +
+
+ + +
+

⭐ Ваши комнаты

+ {% if user_rooms %} +
    + {% for room in user_rooms %} +
  • +
    +
    + {{ room.player_count }} +
    +
    +
    + {{ room.name }} + {% if room.creator_id == current_user.id %} + Админ + {% endif %} +
    +
    + Месяц {{ room.current_month }}/{{ room.total_months }} • + Игроков: {{ room.player_count }}/{{ config.MAX_PLAYERS_PER_ROOM }} +
    +
    +
    +
    + {% if room.status == 'waiting' %} + Ожидание + {% elif room.status == 'playing' %} + В игре + {% else %} + {{ room.status }} + {% endif %} +
    +
  • + {% endfor %} +
+ {% else %} +
+
👤
+

Вы пока не участвуете в комнатах

+

Присоединитесь к существующей или создайте свою

+
+ {% endif %} +
+ + +
+

⚡ Быстрые действия

+
+ + + + +
+
+ + + {% if current_user.is_authenticated %} +
+

📊 Ваша статистика

+
+
+
{{ + current_user.total_games }} +
+
Всего игр
+
+
+
{{ + current_user.games_won }} +
+
Побед
+
+
+
+ {% if current_user.total_games > 0 %} + {{ "%.1f"|format(current_user.games_won / current_user.total_games * 100) }}% + {% else %} + 0% + {% endif %} +
+
Процент побед
+
+
+
+ {{ current_user.total_earnings|format_currency }} +
+
Заработано
+
+
+
+ {% endif %} +
+
+ + + +{% endblock %} + +{% block scripts %} + + + +{% endblock %} \ No newline at end of file