This commit is contained in:
2026-02-02 19:18:25 +03:00
commit 038b307d70
21 changed files with 4987 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.idea
.venv
app.db

Binary file not shown.

Binary file not shown.

1368
app.py Normal file

File diff suppressed because it is too large Load Diff

35
config.py Normal file
View File

@@ -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()

23
logo.py Normal file
View File

@@ -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")

16
main.py Normal file
View File

@@ -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/

890
static/css/style.css Normal file
View File

@@ -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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>'); }
.icon-settings { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>'); }
.icon-chat { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>'); }
.icon-stats { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M16 11V3H8v6H2v12h20V11h-6zm-6-6h4v14h-4V5zm-6 6h4v8H4v-8zm16 8h-4v-6h4v6z"/></svg>'); }
.icon-help { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>'); }

BIN
static/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

129
static/js/game.js Normal file
View File

@@ -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();
});
}

87
static/js/main.js Normal file
View File

@@ -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;
}
});

52
templates/403.html Normal file
View File

@@ -0,0 +1,52 @@
{% extends "base.html" %}
{% block title %}Доступ запрещён - Капитал & Рынок{% endblock %}
{% block screen_content %}
<div class="header">
<a href="{{ url_for('rooms') }}" class="back-button"></a>
<div class="logo-container">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Капитал & Рынок" class="logo">
</div>
</div>
<div class="container">
<div class="card text-center">
<div style="font-size: 5rem; color: var(--warning-color); margin: 20px 0;">403</div>
<h2>Доступ запрещён</h2>
<p>У вас недостаточно прав для доступа к этой странице.</p>
<div style="margin: 30px 0;">
<div style="font-size: 8rem; color: #f0f0f0; margin-bottom: 20px;">🔒💰</div>
<p style="color: var(--light-text); font-size: 0.9rem;">
Эта комната может быть приватной или игра уже началась.
</p>
</div>
<div class="flex flex-col gap-2 mt-4">
{% if current_user.is_authenticated %}
<a href="{{ url_for('rooms') }}" class="button">
Вернуться к списку комнат
</a>
{% else %}
<a href="{{ url_for('login') }}" class="button">
Войти в систему
</a>
{% endif %}
<a href="{{ url_for('index') }}" class="button secondary">
На главную
</a>
</div>
</div>
<div class="card">
<h3>Возможные причины:</h3>
<ul style="padding-left: 20px; margin-top: 10px;">
<li style="margin-bottom: 8px;">Вы не авторизованы в системе</li>
<li style="margin-bottom: 8px;">У вас нет прав для доступа к этой комнате</li>
<li style="margin-bottom: 8px;">Игра уже началась и присоединение невозможно</li>
<li>Комната является приватной</li>
</ul>
</div>
</div>
{% endblock %}

46
templates/404.html Normal file
View File

@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}Страница не найдена - Капитал & Рынок{% endblock %}
{% block screen_content %}
<div class="header">
<a href="{{ url_for('rooms') }}" class="back-button"></a>
<div class="logo-container">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Капитал & Рынок" class="logo">
</div>
</div>
<div class="container">
<div class="card text-center">
<div style="font-size: 5rem; color: var(--primary-color); margin: 20px 0;">404</div>
<h2>Страница не найдена</h2>
<p>К сожалению, запрашиваемая страница не существует или была перемещена.</p>
<div style="margin: 30px 0;">
<div style="font-size: 8rem; color: #f0f0f0; margin-bottom: 20px;">💼📉</div>
<p style="color: var(--light-text); font-size: 0.9rem;">
Возможно, комната была закрыта или игра завершена.
</p>
</div>
<div class="flex flex-col gap-2 mt-4">
<a href="{{ url_for('rooms') }}" class="button">
Вернуться к списку комнат
</a>
<a href="{{ url_for('index') }}" class="button secondary">
На главную
</a>
</div>
</div>
<div class="card">
<h3>Что можно сделать?</h3>
<ul style="padding-left: 20px; margin-top: 10px;">
<li style="margin-bottom: 8px;">Проверьте правильность URL-адреса</li>
<li style="margin-bottom: 8px;">Создайте новую игровую комнату</li>
<li style="margin-bottom: 8px;">Присоединитесь к другой комнате</li>
<li>Обратитесь к администратору, если это ошибка</li>
</ul>
</div>
</div>
{% endblock %}

74
templates/500.html Normal file
View File

@@ -0,0 +1,74 @@
{% extends "base.html" %}
{% block title %}Ошибка сервера - Капитал & Рынок{% endblock %}
{% block screen_content %}
<div class="header">
<a href="{{ url_for('rooms') }}" class="back-button"></a>
<div class="logo-container">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Капитал & Рынок" class="logo">
</div>
</div>
<div class="container">
<div class="card text-center">
<div style="font-size: 5rem; color: var(--danger-color); margin: 20px 0;">500</div>
<h2>Ошибка сервера</h2>
<p>Произошла внутренняя ошибка сервера. Мы уже работаем над её устранением.</p>
<div style="margin: 30px 0;">
<div style="font-size: 8rem; color: #f0f0f0; margin-bottom: 20px;">💥📊</div>
<p style="color: var(--light-text); font-size: 0.9rem;">
Рынок временно не работает. Пожалуйста, попробуйте позже.
</p>
</div>
<div class="flex flex-col gap-2 mt-4">
<button onclick="window.location.reload()" class="button">
Попробовать снова
</button>
<a href="{{ url_for('rooms') }}" class="button secondary">
Вернуться к списку комнат
</a>
<a href="{{ url_for('index') }}" class="button">
На главную
</a>
</div>
</div>
<div class="card">
<h3>Техническая информация</h3>
<p style="margin-top: 10px; font-size: 0.9rem; color: var(--light-text);">
Если ошибка повторяется, пожалуйста, свяжитесь с администратором:
</p>
<div style="background-color: #f8f9fa; padding: 15px; border-radius: var(--border-radius); margin-top: 15px; font-family: monospace; font-size: 0.85rem;">
<div>Время ошибки: <span id="error-time">{{ now.strftime('%Y-%m-%d %H:%M:%S') if now else '' }}</span></div>
<div>Путь: <span id="error-path">{{ request.path if request else '' }}</span></div>
</div>
<div style="margin-top: 20px;">
<button onclick="copyErrorInfo()" class="button secondary">
Скопировать информацию об ошибке
</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function copyErrorInfo() {
const errorInfo = `Ошибка 500\nПуть: ${document.getElementById('error-path').textContent}\nВремя: ${document.getElementById('error-time').textContent}\nUser Agent: ${navigator.userAgent}`;
navigator.clipboard.writeText(errorInfo).then(() => {
alert('Информация об ошибке скопирована в буфер обмена');
});
}
// Автоматическая перезагрузка через 30 секунд
setTimeout(() => {
console.log('Пытаемся перезагрузить страницу через 30 секунд после ошибки...');
}, 30000);
</script>
{% endblock %}

69
templates/base.html Normal file
View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Капитал & Рынок{% endblock %}</title>
<!-- Статические файлы -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="icon" href="{{ url_for('static', filename='images/logo.png') }}">
<!-- WebSocket -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.4/socket.io.min.js"></script>
{% block head %}{% endblock %}
</head>
<body>
<!-- Уведомления -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-messages">
{% for category, message in messages %}
<div class="flash-message {{ category }}">
{{ message }}
<button class="flash-close" onclick="this.parentElement.remove()">×</button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- Контент страницы -->
{% block content %}{% endblock %}
<!-- Общие скрипты -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<!-- Инициализация WebSocket -->
<script>
// Подключение к WebSocket
const socket = io();
// Базовая обработка подключения
socket.on('connect', function() {
console.log('Connected to server');
});
socket.on('disconnect', function() {
console.log('Disconnected from server');
});
// Глобальные обработчики событий
socket.on('chat_message', function(data) {
// Обработка сообщений чата
console.log('New message:', data);
});
socket.on('player_joined', function(data) {
console.log('Player joined:', data.username);
});
socket.on('player_left', function(data) {
console.log('Player left:', data.username);
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>

40
templates/game.html Normal file
View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Капитал & Рынок - Игра</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="screen active">
<div class="header">
<a href="lobby.html" class="back-button" onclick="return confirmExit()"></a>
<div class="logo-container">
<img src="logo.png" alt="Капитал & Рынок" class="logo">
</div>
<span id="game-month">Месяц 1</span>
</div>
<div class="container">
<!-- Остальной код игрового экрана (табы, карточки и т.д.) -->
<!-- Используем тот же HTML из предыдущей версии, но разбиваем на отдельные страницы -->
<!-- Полный код смотри в предыдущем сообщении -->
</div>
</div>
<script src="js/main.js"></script>
<script src="js/game.js"></script>
<script>
// Подтверждение выхода из игры
function confirmExit() {
return confirm('Вы уверены, что хотите выйти из игры? Текущий прогресс может быть потерян.');
}
// Инициализация игры
document.addEventListener('DOMContentLoaded', function() {
initGame();
});
</script>
</body>
</html>

210
templates/index.html Normal file
View File

@@ -0,0 +1,210 @@
{% extends "base.html" %}
{% block title %}Капитал & Рынок - Экономическая стратегия{% endblock %}
{% block content %}
<div class="screen active">
<!-- Шапка -->
<div class="header">
<div class="logo-container">
{% if url_for('static', filename='images/logo.png') %}
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Капитал & Рынок" class="logo"
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
<span class="logo-text" style="display: none; color: white; font-weight: bold; font-size: 1.2rem;">💰 Капитал & Рынок</span>
{% else %}
<span class="logo-text" style="color: white; font-weight: bold; font-size: 1.2rem;">💰 Капитал & Рынок</span>
{% endif %}
</div>
</div>
<!-- Основной контент -->
<div class="container">
<!-- Приветственная карточка -->
<div class="card text-center">
<h1 style="margin-bottom: 15px;">Капитал & Рынок</h1>
<p style="margin: 15px 0; font-size: 1.1rem; color: var(--light-text);">
Стань успешным инвестором в динамичной экономической стратегии!
</p>
<div style="margin: 25px 0;">
<div style="font-size: 4rem; margin-bottom: 20px;">💰📈🏦</div>
</div>
<div class="flex flex-col gap-3" style="max-width: 300px; margin: 0 auto;">
<a href="{{ url_for('login') }}" class="button" style="font-size: 1.1rem; padding: 15px;">
Войти в игру
</a>
<a href="{{ url_for('register') }}" class="button success" style="font-size: 1.1rem; padding: 15px;">
Создать аккаунт
</a>
<a href="{{ url_for('quick_login', username='Гость') }}" class="button secondary" style="padding: 12px;">
Быстрый старт (гостевая игра)
</a>
</div>
</div>
<!-- Особенности игры -->
<div class="card">
<h3 style="text-align: center; margin-bottom: 20px;">Особенности игры</h3>
<div style="display: grid; grid-template-columns: 1fr; gap: 15px;">
<div style="display: flex; align-items: flex-start; gap: 15px; padding: 15px; background-color: #f8f9fa; border-radius: var(--border-radius);">
<div style="font-size: 2rem; flex-shrink: 0;">🎮</div>
<div>
<h4 style="margin-bottom: 5px;">Динамичный геймплей</h4>
<p style="font-size: 0.9rem; color: var(--light-text); margin: 0;">
Каждый месяц - новые решения, события и вызовы. Адаптируйтесь к меняющемуся рынку!
</p>
</div>
</div>
<div style="display: flex; align-items: flex-start; gap: 15px; padding: 15px; background-color: #f8f9fa; border-radius: var(--border-radius);">
<div style="font-size: 2rem; flex-shrink: 0;">👥</div>
<div>
<h4 style="margin-bottom: 5px;">Мультиплеер до 10 игроков</h4>
<p style="font-size: 0.9rem; color: var(--light-text); margin: 0;">
Соревнуйтесь с друзьями или случайными соперниками. Создавайте альянсы и заключайте сделки!
</p>
</div>
</div>
<div style="display: flex; align-items: flex-start; gap: 15px; padding: 15px; background-color: #f8f9fa; border-radius: var(--border-radius);">
<div style="font-size: 2rem; flex-shrink: 0;">💡</div>
<div>
<h4 style="margin-bottom: 5px;">13 уникальных способностей</h4>
<p style="font-size: 0.9rem; color: var(--light-text); margin: 0;">
Каждый игрок получает особую способность: от Кризисного инвестора до Теневого бухгалтера.
</p>
</div>
</div>
<div style="display: flex; align-items: flex-start; gap: 15px; padding: 15px; background-color: #f8f9fa; border-radius: var(--border-radius);">
<div style="font-size: 2rem; flex-shrink: 0;">📊</div>
<div>
<h4 style="margin-bottom: 5px;">Реалистичная экономика</h4>
<p style="font-size: 0.9rem; color: var(--light-text); margin: 0;">
Рынок реагирует на действия всех игроков. Ваши решения влияют на цены активов!
</p>
</div>
</div>
</div>
</div>
<!-- Как начать играть -->
<div class="card">
<h3 style="text-align: center; margin-bottom: 20px;">Как начать играть?</h3>
<div style="counter-reset: step-counter;">
<div style="display: flex; align-items: flex-start; gap: 15px; margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #eee;">
<div style="background-color: var(--primary-color); color: white; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-weight: bold;">
1
</div>
<div>
<h4 style="margin-bottom: 5px;">Создайте аккаунт</h4>
<p style="font-size: 0.95rem; color: var(--light-text); margin: 0;">
Зарегистрируйтесь или войдите как гость. Это займет менее минуты!
</p>
</div>
</div>
<div style="display: flex; align-items: flex-start; gap: 15px; margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #eee;">
<div style="background-color: var(--primary-color); color: white; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-weight: bold;">
2
</div>
<div>
<h4 style="margin-bottom: 5px;">Присоединитесь к комнате</h4>
<p style="font-size: 0.95rem; color: var(--light-text); margin: 0;">
Выберите существующую комнату или создайте свою. Настройте правила игры.
</p>
</div>
</div>
<div style="display: flex; align-items: flex-start; gap: 15px; margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #eee;">
<div style="background-color: var(--primary-color); color: white; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-weight: bold;">
3
</div>
<div>
<h4 style="margin-bottom: 5px;">Выберите способность</h4>
<p style="font-size: 0.95rem; color: var(--light-text); margin: 0;">
Получите случайную уникальную способность и стартовый капитал 100,000 ₽.
</p>
</div>
</div>
<div style="display: flex; align-items: flex-start; gap: 15px;">
<div style="background-color: var(--primary-color); color: white; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-weight: bold;">
4
</div>
<div>
<h4 style="margin-bottom: 5px;">Станьте самым богатым!</h4>
<p style="font-size: 0.95rem; color: var(--light-text); margin: 0;">
Инвестируйте в акции, недвижимость, бизнес. Обыграйте конкурентов за 12 месяцев!
</p>
</div>
</div>
</div>
</div>
<!-- Статистика (заглушка) -->
<div class="card text-center">
<h3 style="margin-bottom: 15px;">Игровая статистика</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-top: 20px;">
<div style="padding: 15px; background-color: #f0f8ff; border-radius: var(--border-radius);">
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">1,234</div>
<div style="font-size: 0.9rem; color: var(--light-text);">Активных игроков</div>
</div>
<div style="padding: 15px; background-color: #f0f8ff; border-radius: var(--border-radius);">
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">567</div>
<div style="font-size: 0.9rem; color: var(--light-text);">Игровых комнат</div>
</div>
<div style="padding: 15px; background-color: #f0f8ff; border-radius: var(--border-radius);">
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">89</div>
<div style="font-size: 0.9rem; color: var(--light-text);">Турниров</div>
</div>
<div style="padding: 15px; background-color: #f0f8ff; border-radius: var(--border-radius);">
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">12</div>
<div style="font-size: 0.9rem; color: var(--light-text);">Уникальных способностей</div>
</div>
</div>
</div>
<!-- Футер -->
<div style="text-align: center; padding: 20px 0; color: var(--light-text); font-size: 0.9rem;">
<p>© 2024 Капитал & Рынок. Экономическая стратегия в реальном времени.</p>
<p style="margin-top: 10px;">
<a href="#" style="color: var(--primary-color); text-decoration: none; margin: 0 10px;">Правила</a> |
<a href="#" style="color: var(--primary-color); text-decoration: none; margin: 0 10px;">Контакты</a> |
<a href="#" style="color: var(--primary-color); text-decoration: none; margin: 0 10px;">Поддержка</a>
</p>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Простая анимация для привлечения внимания
document.addEventListener('DOMContentLoaded', function() {
const emojis = document.querySelector('.text-center .flex');
if (emojis) {
emojis.style.opacity = '0';
emojis.style.transform = 'translateY(20px)';
setTimeout(() => {
emojis.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
emojis.style.opacity = '1';
emojis.style.transform = 'translateY(0)';
}, 300);
}
// Подсветка кнопок при наведении
const buttons = document.querySelectorAll('.button');
buttons.forEach(button => {
button.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
});
button.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
});
});
});
</script>
{% endblock %}

1085
templates/lobby.html Normal file

File diff suppressed because it is too large Load Diff

114
templates/login.html Normal file
View File

@@ -0,0 +1,114 @@
{% extends "base.html" %}
{% block title %}Вход - Капитал & Рынок{% endblock %}
{% block screen_content %}
<div class="header">
<div class="logo-container">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Капитал & Рынок" class="logo">
</div>
</div>
<div class="container">
<div class="card">
<h2>Вход в игру</h2>
<form method="POST" action="{{ url_for('login') }}" class="auth-form">
<div class="input-group">
<label for="username">Имя пользователя</label>
<input type="text" id="username" name="username"
placeholder="Введите ваш никнейм"
value="{{ request.form.username if request.form }}"
required autofocus>
</div>
<div class="input-group">
<label for="password">Пароль</label>
<input type="password" id="password" name="password"
placeholder="Введите пароль" required>
</div>
<div class="input-group" style="flex-direction: row; align-items: center; gap: 10px;">
<input type="checkbox" id="remember" name="remember" checked>
<label for="remember" style="margin: 0;">Запомнить меня</label>
</div>
<button type="submit" class="button">Войти</button>
<div class="auth-links">
<a href="{{ url_for('register') }}">Создать аккаунт</a>
<a href="#" onclick="showQuickLogin()">Быстрый вход (тест)</a>
</div>
</form>
</div>
<div class="card">
<h3>Тестовые аккаунты</h3>
<p style="margin-bottom: 15px; color: var(--light-text); font-size: 0.9rem;">
Для быстрого тестирования игры:
</p>
<div class="flex flex-col gap-2">
<a href="{{ url_for('quick_login', username='Игрок1') }}" class="button secondary">
Войти как Игрок1
</a>
<a href="{{ url_for('quick_login', username='Игрок2') }}" class="button secondary">
Войти как Игрок2
</a>
<a href="{{ url_for('quick_login', username='Инвестор') }}" class="button secondary">
Войти как Инвестор
</a>
</div>
</div>
</div>
<!-- Модальное окно быстрого входа -->
<div id="quick-login-modal" class="modal-backdrop">
<div class="modal">
<h3>Быстрый вход</h3>
<p>Введите имя для быстрого входа (будет создан аккаунт если его нет):</p>
<div class="input-group mt-3">
<input type="text" id="quick-username" placeholder="Имя пользователя" style="width: 100%;">
</div>
<div class="flex gap-2 mt-4">
<button onclick="doQuickLogin()" class="button success">Войти</button>
<button onclick="hideQuickLogin()" class="button secondary">Отмена</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function showQuickLogin() {
event.preventDefault();
document.getElementById('quick-login-modal').classList.add('active');
document.getElementById('quick-username').focus();
}
function hideQuickLogin() {
document.getElementById('quick-login-modal').classList.remove('active');
}
function doQuickLogin() {
const username = document.getElementById('quick-username').value.trim();
if (username) {
window.location.href = `/quick_login/${encodeURIComponent(username)}`;
} else {
alert('Введите имя пользователя');
}
}
// Автозаполнение формы для теста
document.addEventListener('DOMContentLoaded', function() {
// Если это демо-версия, можно автозаполнить форму
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('demo') === '1') {
document.getElementById('username').value = 'test_user';
document.getElementById('password').value = 'test123';
}
});
</script>
{% endblock %}

178
templates/register.html Normal file
View File

@@ -0,0 +1,178 @@
{% extends "base.html" %}
{% block title %}Регистрация - Капитал & Рынок{% endblock %}
{% block screen_content %}
<div class="header">
<a href="{{ url_for('login') }}" class="back-button"></a>
<div class="logo-container">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Капитал & Рынок" class="logo">
</div>
</div>
<div class="container">
<div class="card">
<h2>Создание аккаунта</h2>
<form method="POST" action="{{ url_for('register') }}" class="auth-form">
<div class="input-group">
<label for="username">Имя пользователя *</label>
<input type="text" id="username" name="username"
placeholder="Придумайте никнейм (мин. 3 символа)"
value="{{ request.form.username if request.form }}"
required autofocus>
<small style="color: var(--light-text); font-size: 0.85rem;">
Будет отображаться другим игрокам
</small>
</div>
<div class="input-group">
<label for="email">Email *</label>
<input type="email" id="email" name="email"
placeholder="Ваш email"
value="{{ request.form.email if request.form }}"
required>
<small style="color: var(--light-text); font-size: 0.85rem;">
Только для восстановления пароля
</small>
</div>
<div class="input-group">
<label for="password">Пароль *</label>
<input type="password" id="password" name="password"
placeholder="Придумайте пароль (мин. 4 символа)" required>
</div>
<div class="input-group">
<label for="password2">Повторите пароль *</label>
<input type="password" id="password2" name="password2"
placeholder="Повторите пароль" required>
</div>
<div class="input-group mt-2">
<div style="background-color: #f5f5f5; padding: 10px; border-radius: var(--border-radius);">
<label style="display: flex; align-items: flex-start; gap: 10px; cursor: pointer;">
<input type="checkbox" name="terms" required style="margin-top: 3px;">
<span style="font-size: 0.9rem;">
Я согласен с <a href="#" onclick="showTerms()">правилами игры</a> и
<a href="#" onclick="showPrivacy()">политикой конфиденциальности</a>
</span>
</label>
</div>
</div>
<button type="submit" class="button success mt-3">Зарегистрироваться</button>
<div class="auth-links">
<a href="{{ url_for('login') }}">Уже есть аккаунт? Войти</a>
</div>
</form>
</div>
<div class="card">
<h3>Почему стоит зарегистрироваться?</h3>
<ul style="padding-left: 20px; margin-top: 10px;">
<li style="margin-bottom: 8px;">🎮 Сохраняйте прогресс в играх</li>
<li style="margin-bottom: 8px;">📊 Отслеживайте статистику и рейтинг</li>
<li style="margin-bottom: 8px;">👥 Создавайте приватные комнаты</li>
<li style="margin-bottom: 8px;">🏆 Участвуйте в турнирах и соревнованиях</li>
<li>💬 Общайтесь с другими игроками</li>
</ul>
</div>
</div>
<!-- Модальные окна с правилами -->
<div id="terms-modal" class="modal-backdrop">
<div class="modal" style="max-width: 600px;">
<h3>Правила игры "Капитал & Рынок"</h3>
<div style="max-height: 400px; overflow-y: auto; padding-right: 10px;">
<h4>1. Основные правила</h4>
<p>Игра проходит в реальном времени, каждый месяц длится 2-5 минут.</p>
<h4>2. Поведение игроков</h4>
<p>Запрещены оскорбления, нецензурная лексика, мошенничество.</p>
<h4>3. Использование способностей</h4>
<p>Способности можно использовать согласно их описанию и ограничениям.</p>
<h4>4. Определение победителя</h4>
<p>Победителем становится игрок с наибольшим капиталом после 12 месяцев.</p>
<h4>5. Дисквалификация</h4>
<p>Администратор может дисквалифицировать игрока за нарушение правил.</p>
</div>
<button onclick="hideTerms()" class="button mt-3">Понятно</button>
</div>
</div>
<div id="privacy-modal" class="modal-backdrop">
<div class="modal" style="max-width: 600px;">
<h3>Политика конфиденциальности</h3>
<div style="max-height: 400px; overflow-y: auto; padding-right: 10px;">
<h4>1. Собираемые данные</h4>
<p>Мы собираем только необходимые данные: имя пользователя, email (опционально), статистику игр.</p>
<h4>2. Использование данных</h4>
<p>Данные используются только для работы игры: идентификации, статистики, рейтингов.</p>
<h4>3. Безопасность</h4>
<p>Пароли хранятся в зашифрованном виде. Мы не передаем ваши данные третьим лицам.</p>
<h4>4. Cookies</h4>
<p>Используем только технические cookies для работы сессий.</p>
</div>
<button onclick="hidePrivacy()" class="button mt-3">Понятно</button>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function showTerms() {
event.preventDefault();
document.getElementById('terms-modal').classList.add('active');
}
function hideTerms() {
document.getElementById('terms-modal').classList.remove('active');
}
function showPrivacy() {
event.preventDefault();
document.getElementById('privacy-modal').classList.add('active');
}
function hidePrivacy() {
document.getElementById('privacy-modal').classList.remove('active');
}
// Валидация формы
document.querySelector('form').addEventListener('submit', function(e) {
const password = document.getElementById('password').value;
const password2 = document.getElementById('password2').value;
const terms = document.querySelector('input[name="terms"]');
if (password !== password2) {
e.preventDefault();
alert('Пароли не совпадают!');
document.getElementById('password2').focus();
return false;
}
if (password.length < 4) {
e.preventDefault();
alert('Пароль должен быть не менее 4 символов!');
document.getElementById('password').focus();
return false;
}
if (!terms.checked) {
e.preventDefault();
alert('Необходимо согласиться с правилами игры!');
return false;
}
return true;
});
</script>
{% endblock %}

568
templates/rooms.html Normal file
View File

@@ -0,0 +1,568 @@
{% extends "base.html" %}
{% block title %}Комнаты - Капитал & Рынок{% endblock %}
{% block content %}
<div class="screen active">
<!-- Шапка -->
<div class="header">
<a href="{{ url_for('index') }}" class="back-button"></a>
<div class="logo-container">
<span class="logo-text" style="color: white; font-weight: bold; font-size: 1.2rem;">
💰 Комнаты
</span>
</div>
<div style="margin-left: auto;">
<span style="font-size: 0.9rem;">
{% if current_user.is_authenticated %}
{{ current_user.username }}
{% endif %}
</span>
</div>
</div>
<!-- Основной контент -->
<div class="container">
<!-- Поиск и создание комнаты -->
<div class="search-bar">
<input type="text" id="room-search" placeholder="Поиск комнат по названию..."
onkeyup="searchRooms()">
<button onclick="searchRooms()">🔍</button>
</div>
<button class="button" onclick="createRoom()" style="margin-bottom: 20px;">
Создать новую комнату
</button>
<!-- Доступные комнаты -->
<div class="card">
<h3>📢 Доступные комнаты</h3>
<div style="margin: 10px 0; color: var(--light-text); font-size: 0.9rem;">
{% if rooms %}
Найдено {{ rooms|length }} комнат
{% else %}
Нет доступных комнат
{% endif %}
</div>
<ul class="room-list" id="room-list">
{% if rooms %}
{% for room in rooms %}
<li class="room-item" onclick="joinRoom('{{ room.code }}')">
<div class="room-info">
<div class="room-avatar"
style="background-color: {% if room.status == 'waiting' %}#4caf50{% elif room.status == 'playing' %}#ff9800{% else %}#9e9e9e{% endif %};">
{{ room.player_count }}
</div>
<div style="flex: 1;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<strong>{{ room.name }}</strong>
<span style="font-size: 0.8rem; color: var(--light-text);">
Месяц {{ room.current_month }}/{{ room.total_months }}
</span>
</div>
<div class="room-meta">
Создатель: {{ room.creator.username if room.creator else 'Система' }} •
Игроков: {{ room.player_count }}/{{ config.MAX_PLAYERS_PER_ROOM }}
</div>
{% if room.settings %}
<div style="margin-top: 3px;">
{% set settings = room.settings|from_json %}
{% if settings.allow_loans %}
<span style="background-color: #e3f2fd; color: #1976d2; padding: 2px 6px; border-radius: 10px; font-size: 0.75rem; margin-right: 5px;">Кредиты</span>
{% endif %}
{% if settings.allow_black_market %}
<span style="background-color: #f3e5f5; color: #7b1fa2; padding: 2px 6px; border-radius: 10px; font-size: 0.75rem;">Чёрный рынок</span>
{% endif %}
</div>
{% endif %}
</div>
</div>
<div class="room-status {{ room.status }}">
{% if room.status == 'waiting' %}
Ожидание
{% elif room.status == 'playing' %}
Игра идет
{% elif room.status == 'full' %}
Заполнена
{% else %}
{{ room.status }}
{% endif %}
</div>
</li>
{% endfor %}
{% else %}
<li style="text-align: center; padding: 20px; color: var(--light-text);">
<div style="font-size: 3rem; margin-bottom: 10px;">🏢</div>
<p>Пока нет доступных комнат</p>
<p style="font-size: 0.9rem;">Создайте первую комнату!</p>
</li>
{% endif %}
</ul>
</div>
<!-- Ваши комнаты -->
<div class="card">
<h3>⭐ Ваши комнаты</h3>
{% if user_rooms %}
<ul class="room-list" id="my-room-list">
{% for room in user_rooms %}
<li class="room-item" onclick="joinRoom('{{ room.code }}')">
<div class="room-info">
<div class="room-avatar" style="background-color: var(--primary-color);">
{{ room.player_count }}
</div>
<div style="flex: 1;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<strong>{{ room.name }}</strong>
{% if room.creator_id == current_user.id %}
<span style="background-color: #fff3e0; color: #ef6c00; padding: 2px 8px; border-radius: 10px; font-size: 0.75rem;">Админ</span>
{% endif %}
</div>
<div class="room-meta">
Месяц {{ room.current_month }}/{{ room.total_months }} •
Игроков: {{ room.player_count }}/{{ config.MAX_PLAYERS_PER_ROOM }}
</div>
</div>
</div>
<div class="room-status {{ room.status }}">
{% if room.status == 'waiting' %}
Ожидание
{% elif room.status == 'playing' %}
В игре
{% else %}
{{ room.status }}
{% endif %}
</div>
</li>
{% endfor %}
</ul>
{% else %}
<div style="text-align: center; padding: 20px; color: var(--light-text);">
<div style="font-size: 3rem; margin-bottom: 10px;">👤</div>
<p>Вы пока не участвуете в комнатах</p>
<p style="font-size: 0.9rem;">Присоединитесь к существующей или создайте свою</p>
</div>
{% endif %}
</div>
<!-- Быстрые действия -->
<div class="card">
<h3>⚡ Быстрые действия</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px;">
<button class="button secondary" onclick="createQuickRoom('Быстрая игра')">
Быстрая игра
</button>
<button class="button secondary" onclick="createQuickRoom('Для новичков')">
Для новичков
</button>
<button class="button secondary" onclick="createQuickRoom('Турнир')">
Турнир
</button>
<button class="button secondary" onclick="createQuickRoom('С друзьями')">
С друзьями
</button>
</div>
</div>
<!-- Статистика -->
{% if current_user.is_authenticated %}
<div class="card">
<h3>📊 Ваша статистика</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-top: 15px;">
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">{{
current_user.total_games }}
</div>
<div style="font-size: 0.9rem; color: var(--light-text);">Всего игр</div>
</div>
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">{{
current_user.games_won }}
</div>
<div style="font-size: 0.9rem; color: var(--light-text);">Побед</div>
</div>
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">
{% if current_user.total_games > 0 %}
{{ "%.1f"|format(current_user.games_won / current_user.total_games * 100) }}%
{% else %}
0%
{% endif %}
</div>
<div style="font-size: 0.9rem; color: var(--light-text);">Процент побед</div>
</div>
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">
{{ current_user.total_earnings|format_currency }}
</div>
<div style="font-size: 0.9rem; color: var(--light-text);">Заработано</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Модальное окно создания комнаты -->
<div id="create-room-modal" class="modal-backdrop">
<div class="modal">
<h3>Создание комнаты</h3>
<form id="create-room-form" onsubmit="return submitRoomForm(event)">
<div class="input-group">
<label for="room-name">Название комнаты *</label>
<input type="text" id="room-name" name="name"
placeholder="Например: Быки и медведи" required>
</div>
<div class="input-group">
<label for="total-months">Длительность игры</label>
<select id="total-months" name="total_months">
<option value="6">6 месяцев (быстрая игра)</option>
<option value="12" selected>12 месяцев (стандартная)</option>
<option value="18">18 месяцев (продвинутая)</option>
<option value="24">24 месяца (экспертная)</option>
</select>
</div>
<div class="input-group">
<label for="start-capital">Стартовый капитал</label>
<input type="number" id="start-capital" name="start_capital"
value="100000" min="50000" max="1000000" step="10000">
</div>
<div class="input-group">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="allow-loans" name="allow_loans" checked>
<span>Разрешить кредиты</span>
</label>
</div>
<div class="input-group">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="allow-black-market" name="allow_black_market">
<span>Разрешить чёрный рынок</span>
</label>
</div>
<div class="input-group">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="private-room" name="private_room">
<span>Приватная комната (по приглашению)</span>
</label>
</div>
<div class="flex gap-2 mt-4">
<button type="submit" class="button success">Создать комнату</button>
<button type="button" onclick="hideCreateRoomModal()" class="button secondary">Отмена</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Глобальные переменные
let allRooms = [];
// Инициализация при загрузке
document.addEventListener('DOMContentLoaded', function() {
console.log('Rooms page loaded');
// Сохраняем все комнаты для поиска
const roomItems = document.querySelectorAll('.room-item');
allRooms = Array.from(roomItems).map(item => ({
name: item.querySelector('strong')?.textContent.toLowerCase() || '',
element: item
}));
// Подключение к комнате через WebSocket
if (typeof socket !== 'undefined') {
console.log('Socket connected, joining global room');
socket.emit('join_global_room');
}
});
// Поиск комнат
function searchRooms() {
const searchTerm = document.getElementById('room-search').value.toLowerCase();
const roomList = document.getElementById('room-list');
if (!roomList) return;
const rooms = roomList.querySelectorAll('.room-item');
rooms.forEach(room => {
const roomName = room.querySelector('strong')?.textContent.toLowerCase() || '';
if (roomName.includes(searchTerm) || searchTerm === '') {
room.style.display = 'flex';
} else {
room.style.display = 'none';
}
});
}
// Создание комнаты
function createRoom() {
console.log('Opening create room modal');
const modal = document.getElementById('create-room-modal');
if (modal) {
modal.classList.add('active');
document.getElementById('room-name').focus();
}
}
// Быстрое создание комнаты
function createQuickRoom(name) {
console.log('Creating quick room:', name);
// Создаем форму данных
const formData = new FormData();
formData.append('name', name);
formData.append('total_months', '12');
formData.append('start_capital', '100000');
formData.append('allow_loans', 'on');
formData.append('allow_black_market', 'off');
formData.append('private_room', 'off');
// Отправляем запрос
createRoomRequest(formData);
}
// Отправка формы создания комнаты
function submitRoomForm(event) {
console.log('Submitting room form');
event.preventDefault();
const form = document.getElementById('create-room-form');
const formData = new FormData(form);
createRoomRequest(formData);
return false;
}
// Функция для отправки запроса на создание комнаты
function createRoomRequest(formData) {
console.log('Sending create room request');
fetch('/room/create', {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json',
}
})
.then(response => {
console.log('Response status:', response.status);
console.log('Response headers:', response.headers.get('content-type'));
// Проверяем тип ответа
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return response.json();
} else {
return response.text().then(text => {
console.error('Expected JSON, got:', text.substring(0, 200));
throw new Error('Server returned HTML instead of JSON');
});
}
})
.then(data => {
console.log('Create room response:', data);
if (data.error) {
alert('Ошибка: ' + data.error);
} else if (data.redirect) {
// Редирект в лобби комнаты
window.location.href = data.redirect;
} else if (data.success) {
// Редирект по коду комнаты
window.location.href = `/room/${data.room_code}`;
} else {
alert('Неизвестный ответ от сервера');
}
})
.catch(error => {
console.error('Error creating room:', error);
alert('Ошибка при создании комнаты. Проверьте консоль для деталей.');
});
}
// Скрыть модальное окно
function hideCreateRoomModal() {
const modal = document.getElementById('create-room-modal');
if (modal) {
modal.classList.remove('active');
}
}
// Присоединение к комнате
function joinRoom(roomCode) {
console.log('Joining room:', roomCode);
window.location.href = `/room/${roomCode}`;
}
// Обновление статуса онлайн
function updateOnlineStatus() {
if (typeof socket !== 'undefined' && socket.connected) {
socket.emit('user_online', {
timestamp: new Date().toISOString()
});
}
}
// Периодическое обновление статуса
setInterval(updateOnlineStatus, 30000);
updateOnlineStatus();
// Обработка клавиши Escape для закрытия модального окна
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
hideCreateRoomModal();
}
});
// Закрытие модального окна при клике на фон
document.addEventListener('click', function(event) {
const modal = document.getElementById('create-room-modal');
if (modal && modal.classList.contains('active') && event.target === modal) {
hideCreateRoomModal();
}
});
</script>
<style>
/* Дополнительные стили для rooms.html */
.room-item {
cursor: pointer;
transition: all 0.3s ease;
border-radius: var(--border-radius);
margin: 5px 0;
padding: 12px;
border: 1px solid transparent;
}
.room-item:hover {
background-color: #f5f5f5;
border-color: var(--primary-color);
transform: translateX(5px);
}
.room-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 1.1rem;
margin-right: 12px;
flex-shrink: 0;
}
.room-status {
padding: 5px 10px;
border-radius: 15px;
font-size: 0.8rem;
font-weight: bold;
text-transform: uppercase;
white-space: nowrap;
}
.room-status.waiting {
background-color: #e3f2fd;
color: #1976d2;
}
.room-status.playing {
background-color: #fff3e0;
color: #ef6c00;
}
.room-status.full {
background-color: #ffebee;
color: #d32f2f;
}
.search-bar {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.search-bar input {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: var(--border-radius);
font-size: 1rem;
}
.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: background-color 0.3s;
}
.search-bar button:hover {
background-color: #0077b3;
}
.modal-backdrop {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-backdrop.active {
display: flex;
}
.modal {
background-color: white;
border-radius: var(--border-radius);
padding: 20px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
}
@media (max-width: 480px) {
.room-item {
flex-direction: column;
align-items: flex-start;
}
.room-status {
align-self: flex-end;
margin-top: 10px;
}
.search-bar {
flex-direction: column;
}
.search-bar button {
width: 100%;
padding: 12px;
}
}
</style>
{% endblock %}