Загрузка данных


import pygame
import random
import sys
import json
from datetime import datetime, timedelta

CELL_SIZE = 60
MIN_SIZE, MAX_SIZE = 5, 10
WIDTH, HEIGHT = 900, 950
FPS = 15

LINE_COLORS = {
    1: (255, 60, 60),
    2: (60, 150, 255),
    3: (60, 220, 60),
    4: (255, 200, 40),
    5: (190, 60, 255),
    6: (255, 120, 40),
    7: (40, 220, 200),
    8: (255, 80, 180),
    9: (140, 100, 240),
    10: (200, 255, 120)
}

THEMES = {
    "dark": {
        "bg": (12, 12, 18),
        "panel": (25, 25, 35),
        "board": (18, 18, 25),
        "grid": (45, 45, 60),
        "text": (220, 220, 230)
    },

    "light": {
        "bg": (220, 220, 220),
        "panel": (180, 180, 180),
        "board": (240, 240, 240),
        "grid": (140, 140, 140),
        "text": (20, 20, 20)
    }
}

ACCENT = (80, 140, 255)
WIN_COLOR = (60, 255, 110)
DIM = (80, 80, 90)

pygame.init()


def get_color(idx):
    return LINE_COLORS.get(idx, (100, 100, 100))


def generate_level(size, count=None):
    if count is None:
        count = max(3, (size * size) // 10)

    mixers = []
    occupied = set()
    attempts = 0

    while len(mixers) < count and attempts < 300:
        x, y = random.randint(1, size - 2), random.randint(1, size - 2)

        if (x, y) not in occupied:
            c1, c2 = random.sample(range(1, 11), 2)

            mixers.append({
                'pos': (x, y),
                'req': [c1, c2],
                'filled': [False, False],
                'active': False,
                'type': 'mixer'
            })

            occupied.add((x, y))

        attempts += 1

    return mixers


class FlowAlchemy:
    def draw_help_panel(self, screen, theme):

        help_lines = [
            "УПРАВЛЕНИЕ:",
            "ЛКМ - рисование линии",
            "ПКМ - установить источник",
            "D - режим рисования",
            "S - разделитель",
            "E - ластик",
            "N - новый уровень",
            "R - перезапуск",
            "+ / - изменение размера поля",
            "T - смена темы",
            "F1 - рекорды за день",
            "F2 - рекорды за месяц",
            "F3 - рекорды за год",
            "Q - выход"
        ]

        panel_x = 20
        panel_y = HEIGHT - 260

        pygame.draw.rect(
            screen,
            theme["panel"],
            (panel_x, panel_y, 320, 230),
            border_radius=10
        )

        for i, line in enumerate(help_lines):

            color = WIN_COLOR if i == 0 else theme["text"]

            txt = self.small.render(line, True, color)

            screen.blit(
                txt,
                (
                    panel_x + 15,
                    panel_y + 10 + i * 16
                )
            )

    def draw_color_help(self, screen, theme):

        panel_x = 360
        panel_y = HEIGHT - 260

        pygame.draw.rect(
            screen,
            theme["panel"],
            (panel_x, panel_y, 500, 230),
            border_radius=10
        )

        title = self.small.render(
            "ЦВЕТА:",
            True,
            WIN_COLOR
        )

        screen.blit(title, (panel_x + 15, panel_y + 10))

        for c in range(1, 11):

            row = (c - 1) // 5
            col = (c - 1) % 5

            x = panel_x + 40 + col * 90
            y = panel_y + 60 + row * 80

            pygame.draw.circle(
                screen,
                get_color(c),
                (x, y),
                18
            )

            key_name = str(c if c != 10 else 0)

            txt = self.small.render(
                f"[{key_name}]",
                True,
                theme["text"]
            )

            screen.blit(
                txt,
                (x - 12, y + 28)
            )





    def __init__(self, initial_size=10):

        self.grid_size = initial_size
        self.size = initial_size
        self.cell = CELL_SIZE

        self._calc_layout()

        self.font = pygame.font.SysFont("Segoe UI", 24, bold=True)
        self.small = pygame.font.SysFont("Segoe UI", 18)

        self.theme = "dark"
        self.stats_mode = 30

        self.reset()

    def _calc_layout(self):

        self.width = self.size * self.cell
        self.height = self.size * self.cell

        self.board_x = (WIDTH - self.width) // 2
        self.board_y = 90
        self.bottom_y = self.board_y + self.height + 15

    def reset(self, mixers=None):

        self.size = self.grid_size

        self._calc_layout()

        self.grid = [[0] * self.size for _ in range(self.size)]
        self.is_source = [[False] * self.size for _ in range(self.size)]
        self.objects = [[None] * self.size for _ in range(self.size)]

        self.mixers = mixers or generate_level(self.size)

        for m in self.mixers:
            x, y = m['pos']

            self.objects[y][x] = m
            self.grid[y][x] = 0

            m['filled'] = [False, False]
            m['active'] = False

        self.selected_color = 1
        self.tool = 'draw'
        self.drawing = False
        self.won = False

        self.score = 0
        self.start_time = datetime.now()

        self.msg = "F1/F2/F3 - рекорды | T - тема"
        self.msg_timer = 200

        self.check_connections()

    def save_result(self):

        result = {
            "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "score": self.score,
            "field_size": self.size
        }

        try:
            with open("results.json", "r", encoding="utf-8") as f:
                data = json.load(f)

        except:
            data = []

        data.append(result)

        with open("results.json", "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=4)

    def load_results(self):

        try:
            with open("results.json", "r", encoding="utf-8") as f:
                return json.load(f)

        except:
            return []

    def get_best_results(self, days=None):

        data = self.load_results()

        if days is not None:

            border = datetime.now() - timedelta(days=days)

            filtered = []

            for r in data:

                d = datetime.strptime(r["date"], "%Y-%m-%d %H:%M:%S")

                if d >= border:
                    filtered.append(r)

            data = filtered

        data.sort(key=lambda x: x["score"], reverse=True)

        return data[:10]

    def _show_msg(self, text, duration=120):

        self.msg = text
        self.msg_timer = duration

    def _get_cell(self, pos):

        mx, my = pos

        if not (
                self.board_x <= mx < self.board_x + self.width and
                self.board_y <= my < self.board_y + self.height
        ):
            return None

        return (
            (mx - self.board_x) // self.cell,
            (my - self.board_y) // self.cell
        )

    def _cell_color(self, x, y):

        obj = self.objects[y][x]

        if obj and obj.get('type') == 'splitter':
            return obj.get('color', 0)

        return self.grid[y][x]

    def _clear_color(self, color):

        for y in range(self.size):
            for x in range(self.size):

                if self.grid[y][x] == color:
                    self.grid[y][x] = 0
                    self.is_source[y][x] = False

                obj = self.objects[y][x]

                if obj and obj.get('type') == 'splitter':
                    if obj.get('color') == color:
                        obj['color'] = 0

    def _is_valid_placement(self, x, y, color):

        if not (0 <= x < self.size and 0 <= y < self.size):
            return False

        obj = self.objects[y][x]

        if obj and obj.get('type') == 'mixer':
            return False

        if self.grid[y][x] not in (0, color):
            return False

        return True

    def check_connections(self):

        for m in self.mixers:

            m['filled'] = [False, False]
            m['active'] = False

            mx, my = m['pos']

            for dx, dy in [(0,1),(0,-1),(1,0),(-1,0)]:

                nx, ny = mx + dx, my + dy

                if 0 <= nx < self.size and 0 <= ny < self.size:

                    neighbor_color = self._cell_color(nx, ny)

                    if neighbor_color > 0:

                        if neighbor_color == m['req'][0]:
                            m['filled'][0] = True

                        if neighbor_color == m['req'][1]:
                            m['filled'][1] = True

            if m['filled'][0] and m['filled'][1]:
                m['active'] = True

        self.won = all(m['active'] for m in self.mixers)

        if self.won:

            time_bonus = max(
                0,
                100 - int((datetime.now() - self.start_time).seconds / 5)
            )

            self.score = self.size * 100 + time_bonus

            self._show_msg(
                f"ПОБЕДА! Очки: {self.score}",
                300
            )

            self.save_result()

    def handle_event(self, event):

        cell = self._get_cell(event.pos) if event.type in (
            pygame.MOUSEBUTTONDOWN,
            pygame.MOUSEMOTION
        ) else None

        if event.type == pygame.KEYDOWN:

            if event.key in (
                pygame.K_1, pygame.K_2, pygame.K_3,
                pygame.K_4, pygame.K_5, pygame.K_6,
                pygame.K_7, pygame.K_8, pygame.K_9,
                pygame.K_0
            ):

                self.selected_color = (
                    10 if event.key == pygame.K_0
                    else int(event.unicode)
                )

            if event.key == pygame.K_n:
                self.reset(generate_level(self.size))

            if event.key == pygame.K_r:
                self.reset(self.mixers)

            if event.key in (
                pygame.K_PLUS,
                pygame.K_EQUALS,
                pygame.K_KP_PLUS
            ):

                if self.grid_size < MAX_SIZE:
                    self.grid_size += 1
                    self.reset()

            if event.key in (
                pygame.K_MINUS,
                pygame.K_UNDERSCORE,
                pygame.K_KP_MINUS
            ):

                if self.grid_size > MIN_SIZE:
                    self.grid_size -= 1
                    self.reset()

            if event.key == pygame.K_d:
                self.tool = 'draw'

            if event.key == pygame.K_s:
                self.tool = 'splitter'

            if event.key == pygame.K_e:
                self.tool = 'eraser'

            if event.key == pygame.K_t:

                self.theme = (
                    "light"
                    if self.theme == "dark"
                    else "dark"
                )

            if event.key == pygame.K_F1:
                self.stats_mode = 1

            if event.key == pygame.K_F2:
                self.stats_mode = 30

            if event.key == pygame.K_F3:
                self.stats_mode = 365

            if event.key == pygame.K_q:
                pygame.quit()
                sys.exit()

            if event.key == pygame.K_i:
                self.won = True

        if cell:

            cx, cy = cell

            obj_at_cell = self.objects[cy][cx]

            if (
                    event.type == pygame.MOUSEBUTTONDOWN
                    and event.button == 3
                    and self.tool == 'draw'
            ):

                if not (
                        obj_at_cell and
                        obj_at_cell.get('type') == 'mixer'
                ):

                    self._clear_color(self.selected_color)

                    self.grid[cy][cx] = self.selected_color
                    self.is_source[cy][cx] = True
                    self.objects[cy][cx] = None

                    self.check_connections()

            elif (
                    event.type == pygame.MOUSEBUTTONDOWN
                    and event.button == 1
            ):

                if self.tool == 'draw':

                    if self._cell_color(cx, cy) == self.selected_color:
                        self.drawing = True
                        self.last_cell = (cx, cy)

                elif self.tool == 'splitter':

                    if self.grid[cy][cx] == 0 and not obj_at_cell:

                        self.objects[cy][cx] = {
                            'type': 'splitter',
                            'color': 0
                        }

                elif self.tool == 'eraser':

                    if obj_at_cell and obj_at_cell.get('type') == 'mixer':
                        return

                    self.grid[cy][cx] = 0
                    self.is_source[cy][cx] = False

                    if (
                            obj_at_cell and
                            obj_at_cell.get('type') == 'splitter'
                    ):

                        self.objects[cy][cx] = None

                    self.check_connections()

            elif (
                    event.type == pygame.MOUSEMOTION
                    and self.drawing
                    and pygame.mouse.get_pressed()[0]
            ):

                if self._is_valid_placement(
                        cx,
                        cy,
                        self.selected_color
                ):

                    dx = abs(cx - self.last_cell[0])
                    dy = abs(cy - self.last_cell[1])

                    if dx + dy == 1:

                        self.grid[cy][cx] = self.selected_color

                        if (
                                obj_at_cell and
                                obj_at_cell.get('type') == 'splitter'
                        ):

                            obj_at_cell['color'] = self.selected_color

                        self.last_cell = (cx, cy)

                        self.check_connections()

            if event.type == pygame.MOUSEBUTTONUP:
                self.drawing = False

    def draw(self, screen):
        
        
        theme = THEMES[self.theme]

        screen.fill(theme["bg"])

        pygame.draw.rect(
            screen,
            theme["panel"],
            (0, 0, WIDTH, self.board_y - 10)
        )

        pygame.draw.rect(
            screen,
            ACCENT,
            (20, self.board_y - 15, WIDTH - 40, 3),
            border_radius=2
        )

        title = self.font.render(
            "Алхимия потоков",
            True,
            theme["text"]
        )

        screen.blit(
            title,
            (
                WIDTH // 2 - title.get_width() // 2,
                15
            )
        )

        active = sum(1 for m in self.mixers if m['active'])

        status = (
            f"Активировано: {active}/{len(self.mixers)} "
            f"| Поле: {self.size}x{self.size}"
        )

        st = self.small.render(status, True, theme["text"])

        screen.blit(
            st,
            (
                WIDTH // 2 - st.get_width() // 2,
                45
            )
        )

        score_text = self.small.render(
            f"Очки: {self.score}",
            True,
            WIN_COLOR
        )

        screen.blit(score_text, (20, 20))

        pygame.draw.rect(
            screen,
            theme["board"],
            (
                self.board_x,
                self.board_y,
                self.width,
                self.height
            ),
            border_radius=6
        )

        for i in range(1, self.size):

            pygame.draw.line(
                screen,
                theme["grid"],
                (
                    self.board_x + i * self.cell,
                    self.board_y
                ),
                (
                    self.board_x + i * self.cell,
                    self.board_y + self.height
                ),
                1
            )

            pygame.draw.line(
                screen,
                theme["grid"],
                (
                    self.board_x,
                    self.board_y + i * self.cell
                ),
                (
                    self.board_x + self.width,
                    self.board_y + i * self.cell
                ),
                1
            )

        for y in range(self.size):
            for x in range(self.size):

                c = self._cell_color(x, y)

                if c > 0:

                    for dx, dy in [(0,1),(1,0)]:

                        nx, ny = x + dx, y + dy

                        if (
                                0 <= nx < self.size and
                                0 <= ny < self.size and
                                self._cell_color(nx, ny) == c
                        ):

                            p1 = (
                                x * self.cell + self.cell // 2 + self.board_x,
                                y * self.cell + self.cell // 2 + self.board_y
                            )

                            p2 = (
                                nx * self.cell + self.cell // 2 + self.board_x,
                                ny * self.cell + self.cell // 2 + self.board_y
                            )

                            pygame.draw.line(
                                screen,
                                get_color(c),
                                p1,
                                p2,
                                self.cell - 16
                            )

        for y in range(self.size):
            for x in range(self.size):

                cx = x * self.cell + self.cell // 2 + self.board_x
                cy = y * self.cell + self.cell // 2 + self.board_y

                obj = self.objects[y][x]

                if self.is_source[y][x]:

                    pygame.draw.circle(
                        screen,
                        get_color(self.grid[y][x]),
                        (cx, cy),
                        self.cell // 3
                    )

                    pygame.draw.circle(
                        screen,
                        (255,255,255),
                        (cx, cy),
                        self.cell // 3,
                        3
                    )

                elif obj:

                    if obj['type'] == 'mixer':

                        bg = (
                            (20,140,60)
                            if obj['active']
                            else (25,30,35)
                        )

                        pygame.draw.rect(
                            screen,
                            bg,
                            (cx-24, cy-24, 48, 48),
                            border_radius=10
                        )

                        for i, rc in enumerate(obj['req']):

                            px, py = (
                                (cx-12, cy-12)
                                if i == 0
                                else (cx+12, cy+12)
                            )

                            pygame.draw.circle(
                                screen,
                                get_color(rc),
                                (px, py),
                                8
                            )

                    elif obj['type'] == 'splitter':

                        col = (
                            get_color(obj['color'])
                            if obj['color'] > 0
                            else (90,95,110)
                        )

                        pygame.draw.polygon(
                            screen,
                            col,
                            [
                                (cx, cy-18),
                                (cx+18, cy),
                                (cx, cy+18),
                                (cx-18, cy)
                            ]
                        )

        results = self.get_best_results(self.stats_mode)

        titles = {
            1: "ТОП за день",
            30: "ТОП за месяц",
            365: "ТОП за год"
        }

        rx = WIDTH - 250
        ry = 120

        title = self.small.render(
            titles[self.stats_mode],
            True,
            theme["text"]
        )

        screen.blit(title, (rx, ry))

        for i, r in enumerate(results[:5]):

            txt = (
                f"{i+1}. "
                f"{r['score']} очк."
            )

            surf = self.small.render(
                txt,
                True,
                theme["text"]
            )

            screen.blit(
                surf,
                (
                    rx,
                    ry + 30 + i * 25
                )
            )

        if self.msg and self.msg_timer > 0:

            surf = self.small.render(
                self.msg,
                True,
                WIN_COLOR
            )

            screen.blit(
                surf,
                (
                    WIDTH // 2 - surf.get_width() // 2,
                    HEIGHT - 30
                )
            )

            self.msg_timer -= 1

            elapsed = (datetime.now() - self.start_time).seconds
            finaltime = elapsed
            minutes = elapsed // 60
            seconds = elapsed % 60

            pygame.display.set_caption(
                f"Алхимия потоков | Время: {minutes:02}:{seconds:02}"
            )
            if self.won:
                elapsed = finaltime
                minutes = elapsed // 60
                seconds = elapsed % 60


                pygame.display.set_caption(
                    #f"Алхимия потоков | Время: {minutes:02}:{seconds:02}"
                    f"Вы победили {minutes:02}:{seconds:02}"
                )

            
        self.draw_help_panel(screen, theme)
        self.draw_color_help(screen, theme)


        pygame.display.flip()


def main():

    screen = pygame.display.set_mode(
        (WIDTH, HEIGHT)
    )

    pygame.display.set_caption("Алхимия потоков")

    clock = pygame.time.Clock()

    game = FlowAlchemy(10)

    while True:

        clock.tick(FPS)

        for event in pygame.event.get():

            if event.type == pygame.QUIT:

                pygame.quit()
                sys.exit()

            game.handle_event(event)

        game.draw(screen)


if __name__ == "__main__":
    main()