From 038b307d701b84b338f86b0dc279752a926f44f5 Mon Sep 17 00:00:00 2001 From: Kavalar Date: Mon, 2 Feb 2026 19:18:25 +0300 Subject: [PATCH] fix --- .gitignore | 3 + __pycache__/app.cpython-310.pyc | Bin 0 -> 26207 bytes __pycache__/config.cpython-310.pyc | Bin 0 -> 1164 bytes app.py | 1368 ++++++++++++++++++++++++++++ config.py | 35 + logo.py | 23 + main.py | 16 + static/css/style.css | 890 ++++++++++++++++++ static/images/logo.png | Bin 0 -> 2548 bytes static/js/game.js | 129 +++ static/js/main.js | 87 ++ templates/403.html | 52 ++ templates/404.html | 46 + templates/500.html | 74 ++ templates/base.html | 69 ++ templates/game.html | 40 + templates/index.html | 210 +++++ templates/lobby.html | 1085 ++++++++++++++++++++++ templates/login.html | 114 +++ templates/register.html | 178 ++++ templates/rooms.html | 568 ++++++++++++ 21 files changed, 4987 insertions(+) create mode 100644 .gitignore create mode 100644 __pycache__/app.cpython-310.pyc create mode 100644 __pycache__/config.cpython-310.pyc create mode 100644 app.py create mode 100644 config.py create mode 100644 logo.py create mode 100644 main.py create mode 100644 static/css/style.css create mode 100644 static/images/logo.png create mode 100644 static/js/game.js create mode 100644 static/js/main.js create mode 100644 templates/403.html create mode 100644 templates/404.html create mode 100644 templates/500.html create mode 100644 templates/base.html create mode 100644 templates/game.html create mode 100644 templates/index.html create mode 100644 templates/lobby.html create mode 100644 templates/login.html create mode 100644 templates/register.html create mode 100644 templates/rooms.html 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 0000000000000000000000000000000000000000..0a0cd1e0e1a548242fff3247fcaa49c264b0eb40 GIT binary patch literal 26207 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 Qb9DhMRL6T-Q?EVy2M7%*761SM literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..38d50ec6d39d666cd5d1fca5463d73e6634caaf3 GIT binary patch literal 2548 zcmcImS67pZ68*qPQw)M2RisESdT0RzHAo2{H9#yJx(L#ww**B{F%;=dSeV^HhzTW8{!8^~=^lr%TzNEpZGScwH6Jz)PR>2b2mqyGc0nYex$@%UvV znSsXaYdIE z-;0@lG@T8-Gl8IcqlwOA?te=k-Ls z8r77yP;sEBOD!Y;mGGl9$r^wlHa=XRJ_VHgCMXWXe3Fz79%#rcC)^`NnrK0niz|QH z<}f>zgJ=hot(MMCVU2WQj&AHsN!mLzn7*pem$LGgfNm|xU(^30W9Z$H4QsN2)tmb4 zrfskimfbCO7|Zd{OG~b5n~aV;>!0R~uXO9-IaOhrquZ>i^<2wwp-XCUZg=f+n#|$D z{y;hqvK@864XQWDzqtD4vk4}M+oh<#e6X(xv~|1+6mtPb zMIwi032$hPr5NWhs?iCNEf*fqmr}cHo3!Kf4l<1j3AbF?Z}63FDVOYAMabP9)lEHR z@;AqM5RPZeKO`9n`&o<8MCg#lX&H~lAWNT>1Z-t5z}&GmI^5m(z1&_L2H z`buRuPH^qw4x6wOGau|2Ju&vAaaPsoECCHtfSi={*_T*ENINIsARB#DI2z3A!DIv* zuV@<8bP&DCt12O=cs=LM-EzCuxxJ3C<%xd}S`;ifexehfEKtXTz)PuSGSHaKo`zCy z{klQVqv~I~WyA_w#bnDiW0!ZEs+#a>@ico*3Z86|1#J^k_7FNnTB&Ddd6c&6S_M@0 z6H6kdX|sVoQMwuYvzK)XK717t3wZV>WNLpxY!>rfCLJ&Nj=BhjcNI_6H4Di zpoCq%{1h|UCRzIV|WE3RYG;mX&-C03&n+JlCgcW;d zLwzbjA8oc9i}}-=+Urq>F)L$bdlSc1I;dI>B8JsEh+w~|Es9R~B6iG*i{l~D%-H`D zb!@S9;T}AV?keJd3lGU(;YK4s-PP~!nqwqSitIPUwJ)sylJRWOLU1Ys#yMD|&2B8w z)4N>r_mmKKB?@Z^w#M;S5)Ue3cOc0Z&vt-sUzKb<>!j6)Fu_ z@H#S7brCdez6Y`CvNIW(Zoa+FE7FVB;BbWq^zP8@gAVnV&~C=>@1C{mjeoen;H91^ z*LAV6Xx{zESD!Lf07S$a|N3V-#;Fn#qNjj;bgXY@_d49-!n3PxQzMY|snc0tv*R~* zJ8PJ)UzPAl4Wy@APWcI5|5|bEYS{owtg$Y7^~-mm#xp5%c?MlB;JrQ9dFn2IGRsUX zqcbuI1PCTh&ZAP9!s-tSF~T>Ojbd27=d{6R8i0qCg~3Jon^GB1!Uog-9clDU>eFUN zo_|LLIbPz;cnf5=vR4RZSz#IZE-eLZ;mfbM$mq)wk$2_##5;UG=ZZ_fp6p-xXs~6| zkqIimtdU2mhjIg?pL+$3u)bn`+@_Q$wPuGXsV82GI2zrIs1J~Dqg9y$^?SlFP{|v; zX^!)jy{X`h3da=o36pyNsZ)5$0iY?a^@o?Cp_{KY28#7LZT-RSKRA(o3c<4i_tFRw zI)Rb~3p>_5c9)8;T_{cd=JUYw5}OAfo7S)+&CUWtYw-(-!;>5S7jw^B7Pp`CVtpUe z0vBEAG_Im52`0uPkxC7ZXxWWt{yKLsQsx{nFkGp-%ug7cxSxCYO_=vlkQIt|m9GxI z1kTeQT+y7-m+_|j*tnA>^Y_1q>aJj5+;FoSQZ6=}hZ_jh z;i|Jl?c@6gm}TacNaoqE5EsItI$Fb-VVOs_$x9(Ny;c-8niu_ONQ6yNHY(>#R$s-P z*{=)@;K8%x?@g(pQ8l!W%-N57Y@&&fyTJWiJPy_P+rIt@*ZYJ5F(Qd@W5Pd!G!f&F zKDun>m3-buWO% zD*@ZO6Ne_=ZOG0Yr0AS|cL&^*U7jXS_il~qE#)Gw)j6?cnXPv_Co%2g@lEi2l*#B= z(BQaeXOUp!%zzd_tAmKb4b|&ZZFS8K)7U#f2EWx!Sf9W3)~1ZK4#79=)E*s4={?~*G`Vt;S5obNhLj@?9rY~(T1grEjROFfsm*mP H)IIS(r%dHl literal 0 HcmV?d00001 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