diff --git "a/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/Lab1/desktop.py" "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/Lab1/desktop.py"
new file mode 100644
index 0000000..777269b
--- /dev/null
+++ "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/Lab1/desktop.py"
@@ -0,0 +1,337 @@
+# desktop.py - Этап 1: Десктопное приложение для РоботОператор
+
+import tkinter as tk
+from tkinter import messagebox
+from collections import deque
+from main import (
+ DirectionTypePanelDir,
+ CellTypePanel,
+ OperatorRobotMaze,
+ OperatorRobotCell,
+ OperatorRobot
+)
+
+
+class OperatorRobotVisualizer:
+
+ def __init__(self, robot: OperatorRobot, cell_size=60):
+ self.robot = robot
+ self.cell_size = cell_size
+
+ self.root = tk.Tk()
+ self.root.title("Робот Оператор - Панели")
+
+ self.colors = {
+ CellTypePanel.FLOOR.value: "white",
+ CellTypePanel.PANEL.value: "lightblue",
+ CellTypePanel.ACTIVE.value: "green",
+ CellTypePanel.SLUICE.value: "gray",
+ CellTypePanel.WALL.value: "black",
+ CellTypePanel.BLOCK.value: "darkgray",
+ CellTypePanel.FINISH.value: "gold"
+ }
+
+ canvas_width = robot.maze.width * cell_size
+ canvas_height = robot.maze.height * cell_size
+ self.canvas = tk.Canvas(
+ self.root,
+ width=canvas_width,
+ height=canvas_height,
+ bg="white"
+ )
+ self.canvas.pack(padx=10, pady=10)
+
+ self.info_label = tk.Label(
+ self.root,
+ text="Готов к работе",
+ font=("Arial", 12)
+ )
+ self.info_label.pack(pady=5)
+
+ button_frame = tk.Frame(self.root)
+ button_frame.pack(pady=5)
+
+ self.start_button = tk.Button(
+ button_frame,
+ text="Начать работу",
+ command=self.start_work
+ )
+ self.start_button.pack(side=tk.LEFT, padx=5)
+
+ self.step_button = tk.Button(
+ button_frame,
+ text="Один шаг",
+ command=self.make_step
+ )
+ self.step_button.pack(side=tk.LEFT, padx=5)
+
+ self.robot_marker = None
+ self.is_working = False
+
+ self.draw_grid()
+
+ def draw_grid(self):
+ self.canvas.delete("all")
+
+ for y in range(self.robot.maze.height):
+ for x in range(self.robot.maze.width):
+ display_y = self.robot.maze.height - 1 - y
+
+ cell = self.robot.maze.get_cell_by_coordinates(x, y)
+ if not cell:
+ continue
+
+ color = self.colors.get(cell.cell_type.value, "white")
+
+ x1 = x * self.cell_size
+ y1 = display_y * self.cell_size
+ x2 = x1 + self.cell_size
+ y2 = y1 + self.cell_size
+
+ self.canvas.create_rectangle(
+ x1, y1, x2, y2,
+ fill=color,
+ outline="gray"
+ )
+
+ text = self._get_cell_label(cell.cell_type)
+ if text:
+ self.canvas.create_text(
+ (x1 + x2) / 2, (y1 + y2) / 2,
+ text=text,
+ font=("Arial", 10)
+ )
+
+ self.draw_robot()
+
+ def _get_cell_label(self, cell_type: CellTypePanel) -> str:
+ labels = {
+ CellTypePanel.FLOOR: "П",
+ CellTypePanel.PANEL: "Пн",
+ CellTypePanel.ACTIVE: "А",
+ CellTypePanel.SLUICE: "Ш",
+ CellTypePanel.WALL: "■",
+ CellTypePanel.BLOCK: "▓",
+ CellTypePanel.FINISH: "F"
+ }
+ return labels.get(cell_type, "")
+
+ def draw_robot(self):
+ if self.robot_marker:
+ self.canvas.delete(self.robot_marker)
+
+ if not self.robot.current_cell:
+ return
+
+ x = self.robot.current_cell.x
+ y = self.robot.current_cell.y
+ display_y = self.robot.maze.height - 1 - y
+
+ x_center = x * self.cell_size + self.cell_size / 2
+ y_center = display_y * self.cell_size + self.cell_size / 2
+ radius = self.cell_size / 3
+
+ self.robot_marker = self.canvas.create_oval(
+ x_center - radius, y_center - radius,
+ x_center + radius, y_center + radius,
+ fill="red", outline="darkred", width=2
+ )
+
+ self.canvas.create_text(
+ x_center, y_center,
+ text="R",
+ font=("Arial", 14, "bold"),
+ fill="white"
+ )
+
+ def update_info(self, text: str):
+ self.info_label.config(text=text)
+
+ def _is_free(self, x: int, y: int) -> bool:
+ if not (0 <= x < self.robot.maze.width and 0 <= y < self.robot.maze.height):
+ return False
+ cell = self.robot.maze.get_cell_by_coordinates(x, y)
+ if not cell:
+ return False
+ return cell.cell_type not in (CellTypePanel.WALL, CellTypePanel.BLOCK)
+
+ def _find_nearest_target(self):
+ targets = []
+ for y in range(self.robot.maze.height):
+ for x in range(self.robot.maze.width):
+ cell = self.robot.maze.get_cell_by_coordinates(x, y)
+ if cell and cell.cell_type in (CellTypePanel.FLOOR, CellTypePanel.PANEL):
+ targets.append((x, y))
+
+ if targets:
+ return targets
+
+ for y in range(self.robot.maze.height):
+ for x in range(self.robot.maze.width):
+ cell = self.robot.maze.get_cell_by_coordinates(x, y)
+ if cell and cell.cell_type == CellTypePanel.FINISH:
+ return [(x, y)]
+
+ return []
+
+ def _bfs_next_step(self, start, goals):
+ goals = set(goals)
+ queue = deque([start])
+ prev = {start: None}
+
+ while queue:
+ x, y = queue.popleft()
+
+ if (x, y) in goals:
+ cur = (x, y)
+ while prev[cur] is not None and prev[cur] != start:
+ cur = prev[cur]
+ return cur if cur != start else None
+
+ neighbors = [
+ (x + 1, y),
+ (x - 1, y),
+ (x, y + 1),
+ (x, y - 1),
+ (x - 1, y + 1),
+ (x + 1, y - 1)
+ ]
+
+ for nx, ny in neighbors:
+ if (nx, ny) not in prev and self._is_free(nx, ny):
+ prev[(nx, ny)] = (x, y)
+ queue.append((nx, ny))
+
+ return None
+
+ def move_robot(self) -> bool:
+ x, y = self.robot.current_cell.x, self.robot.current_cell.y
+ goals = self._find_nearest_target()
+
+ if not goals:
+ self.update_info("Целей нет (все Пол/Панель обработаны)")
+ return False
+
+ next_pos = self._bfs_next_step((x, y), goals)
+
+ if next_pos is None:
+ self.update_info("Путь к цели не найден")
+ return False
+
+ nx, ny = next_pos
+
+ if nx == x + 1 and ny == y:
+ return self.robot.channel_right() is not None
+ elif nx == x - 1 and ny == y:
+ return self.robot.channel_left() is not None
+ elif nx == x and ny == y + 1:
+ return self.robot.channel_forward() is not None
+ elif nx == x and ny == y - 1:
+ return self.robot.channel_backward() is not None
+ elif nx == x - 1 and ny == y + 1:
+ return self.robot.lift_diag() is not None
+ elif nx == x + 1 and ny == y - 1:
+ return self.robot.descend_diag() is not None
+
+ return False
+
+ def make_step(self):
+ if not self.robot.current_cell:
+ self.update_info("Робот не найден!")
+ return
+
+ current_cell = self.robot.current_cell
+ x, y = current_cell.x, current_cell.y
+
+ if current_cell.cell_type == CellTypePanel.FLOOR:
+ self.robot.floor()
+ self.update_info(f"Позиция ({x}, {y}): Пол → Панель")
+ elif current_cell.cell_type == CellTypePanel.PANEL:
+ self.robot.panel()
+ self.update_info(f"Позиция ({x}, {y}): Панель → Активно")
+ elif current_cell.cell_type == CellTypePanel.FINISH:
+ self.update_info(f"Позиция ({x}, {y}): ФИНИШ! Работа завершена!")
+ self.draw_grid()
+ self.is_working = False
+ return
+ else:
+ self.update_info(f"Позиция ({x}, {y}): Ячейка не требует обработки")
+
+ self.move_robot()
+
+ self.draw_grid()
+ self.root.update()
+
+ def start_work(self):
+ self.is_working = True
+ self.start_button.config(state=tk.DISABLED)
+ self.automatic_work()
+
+ def automatic_work(self):
+ if not self.is_working:
+ self.start_button.config(state=tk.NORMAL)
+ return
+
+ if not self.robot.current_cell:
+ self.update_info("Ошибка: робот не найден!")
+ self.is_working = False
+ self.start_button.config(state=tk.NORMAL)
+ return
+
+ current_cell = self.robot.current_cell
+
+ if current_cell.cell_type == CellTypePanel.FINISH:
+ self.update_info("РАБОТА ЗАВЕРШЕНА! Робот достиг финиша!")
+ self.is_working = False
+ self.start_button.config(state=tk.NORMAL)
+ return
+
+ self.make_step()
+
+ self.root.after(100, self.automatic_work)
+
+ def run(self):
+ self.root.mainloop()
+
+
+def create_test_grid(width: int, height: int):
+ cell_values = []
+
+ for y in range(height):
+ row = []
+ for x in range(width):
+ if x == 0 and y == 0:
+ cell_value = CellTypePanel.FLOOR.value | 0x8
+ elif x == width - 1 and y == height - 1:
+ cell_value = CellTypePanel.FINISH.value
+ elif (x == 2 and y == 1) or (x == 3 and y == 2):
+ cell_value = CellTypePanel.WALL.value
+ elif x == 1 and y == 2:
+ cell_value = CellTypePanel.BLOCK.value
+ elif (x + y) % 3 == 0:
+ cell_value = CellTypePanel.PANEL.value
+ else:
+ cell_value = CellTypePanel.FLOOR.value
+
+ row.append(cell_value)
+ cell_values.append(row)
+
+ return cell_values
+
+
+def main():
+ width = 7
+ height = 5
+
+ cell_values = create_test_grid(width, height)
+
+ maze = OperatorRobotMaze(cells=cell_values)
+
+ robot = OperatorRobot(maze)
+
+ visualizer = OperatorRobotVisualizer(robot)
+ visualizer.run()
+
+
+if __name__ == "__main__":
+ main()
diff --git "a/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/Lab1/lab1.mp4" "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/Lab1/lab1.mp4"
new file mode 100644
index 0000000..ff76486
Binary files /dev/null and "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/Lab1/lab1.mp4" differ
diff --git "a/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/Lab1/main.py" "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/Lab1/main.py"
new file mode 100644
index 0000000..fc9bf88
--- /dev/null
+++ "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/Lab1/main.py"
@@ -0,0 +1,238 @@
+from enum import Enum
+from typing import Optional, Iterator, List
+
+
+class DirectionTypePanelDir(Enum):
+ CHANNEL_NORTH = "ChannelNorth"
+ CHANNEL_SOUTH = "ChannelSouth"
+ CHANNEL_WEST = "ChannelWest"
+ CHANNEL_EAST = "ChannelEast"
+ DIAG_NW = "DiagNorthWest"
+ DIAG_SE = "DiagSouthEast"
+
+
+class CellTypePanel(Enum):
+ FLOOR = 0x0
+ PANEL = 0x1
+ ACTIVE = 0x2
+ SLUICE = 0x3
+ WALL = 0x4
+ BLOCK = 0x5
+ FINISH = 0x6
+
+ @classmethod
+ def from_value(cls, value: int) -> 'CellTypePanel':
+ base_value = value & 0x7
+ for cell_type in cls:
+ if cell_type.value == base_value:
+ return cell_type
+ return cls.FLOOR
+
+
+class OperatorRobotCell:
+ def __init__(self, x: int = 0, y: int = 0, cell_value: int = 0x0):
+ self.x = x
+ self.y = y
+ self.has_robot = (cell_value & 0x8) != 0
+ self.cell_type = CellTypePanel.from_value(cell_value)
+
+
+class OperatorRobotMaze:
+ def __init__(self, width: int = None, height: int = None, cells: List[List[int]] = None):
+ self.width = width if width is not None else 0
+ self.height = height if height is not None else 0
+ self.cells = []
+
+ if cells is not None:
+ self.load_from_values(cells)
+ elif width is not None and height is not None:
+ self.initialize_maze()
+
+ def load_from_values(self, cell_values: List[List[int]]):
+ if not cell_values:
+ self.height = 0
+ self.width = 0
+ self.cells = []
+ return
+
+ self.height = len(cell_values)
+ self.width = len(cell_values[0]) if self.height > 0 else 0
+ self.cells = []
+
+ for row_idx in range(self.height - 1, -1, -1):
+ y = row_idx
+ row = []
+ for x in range(self.width):
+ cell_value = cell_values[row_idx][x]
+ cell = OperatorRobotCell(x, y, cell_value)
+ row.append(cell)
+ self.cells.append(row)
+
+ def initialize_maze(self, cell_type: CellTypePanel = None):
+ if cell_type is None:
+ cell_type = CellTypePanel.FLOOR
+
+ self.cells = []
+ for list_y in range(self.height):
+ y = self.height - 1 - list_y
+ row = []
+ for x in range(self.width):
+ cell_value = cell_type.value
+ cell = OperatorRobotCell(x, y, cell_value)
+ row.append(cell)
+ self.cells.append(row)
+
+ def get_cell_by_coordinates(self, x: int, y: int) -> Optional[OperatorRobotCell]:
+ if 0 <= x < self.width and 0 <= y < self.height:
+ list_y = self.height - 1 - y
+ return self.cells[list_y][x]
+ return None
+
+ def get_neighbor_cell(self, current_cell: OperatorRobotCell,
+ search_direction: DirectionTypePanelDir) -> Optional[OperatorRobotCell]:
+ if not self.cells or not current_cell:
+ return None
+
+ x, y = current_cell.x, current_cell.y
+
+ offsets = {
+ DirectionTypePanelDir.CHANNEL_NORTH.value: (0, 1),
+ DirectionTypePanelDir.CHANNEL_SOUTH.value: (0, -1),
+ DirectionTypePanelDir.CHANNEL_WEST.value: (-1, 0),
+ DirectionTypePanelDir.CHANNEL_EAST.value: (1, 0),
+ DirectionTypePanelDir.DIAG_NW.value: (-1, 1),
+ DirectionTypePanelDir.DIAG_SE.value: (1, -1),
+ }
+
+ dx, dy = offsets.get(search_direction.value, (0, 0))
+ return self.get_cell_by_coordinates(x + dx, y + dy)
+
+ def get_iterator(self) -> Iterator[OperatorRobotCell]:
+
+ class SnakeIterator:
+ def __init__(self, maze):
+ self.maze = maze
+ self.x = 0
+ self.y = 0
+ self.move_right = True
+ self.done = False
+ self.first = True
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ if self.done:
+ raise StopIteration
+
+ if self.first:
+ self.first = False
+ cell = self.maze.get_cell_by_coordinates(self.x, self.y)
+ return cell
+
+ if self.move_right:
+ if self.x < self.maze.width - 1:
+ self.x += 1
+ else:
+ if self.y < self.maze.height - 1:
+ self.y += 1
+ self.move_right = False
+ else:
+ self.done = True
+ else:
+ if self.x > 0:
+ self.x -= 1
+ else:
+ if self.y < self.maze.height - 1:
+ self.y += 1
+ self.move_right = True
+ else:
+ self.done = True
+
+ if self.done:
+ raise StopIteration
+
+ cell = self.maze.get_cell_by_coordinates(self.x, self.y)
+ return cell
+
+ return SnakeIterator(self)
+
+
+class OperatorRobot:
+
+ def __init__(self, maze: OperatorRobotMaze = None):
+ self.maze = maze
+ self.current_cell = None
+
+ if maze and maze.cells:
+ for y in range(maze.height):
+ for x in range(maze.width):
+ cell = maze.get_cell_by_coordinates(x, y)
+ if cell and cell.has_robot:
+ self.current_cell = cell
+ return
+
+ def _move_to_cell(self, new_cell: Optional[OperatorRobotCell]) -> Optional[OperatorRobotCell]:
+ if new_cell and new_cell.cell_type not in [CellTypePanel.WALL, CellTypePanel.BLOCK]:
+ if self.current_cell:
+ self.current_cell.has_robot = False
+ self.current_cell = new_cell
+ new_cell.has_robot = True
+ return new_cell
+ return None
+
+ def channel_forward(self) -> Optional[OperatorRobotCell]:
+ if self.maze and self.current_cell:
+ new_cell = self.maze.get_neighbor_cell(
+ self.current_cell, DirectionTypePanelDir.CHANNEL_NORTH
+ )
+ return self._move_to_cell(new_cell)
+ return None
+
+ def channel_backward(self) -> Optional[OperatorRobotCell]:
+ if self.maze and self.current_cell:
+ new_cell = self.maze.get_neighbor_cell(
+ self.current_cell, DirectionTypePanelDir.CHANNEL_SOUTH
+ )
+ return self._move_to_cell(new_cell)
+ return None
+
+ def channel_left(self) -> Optional[OperatorRobotCell]:
+ if self.maze and self.current_cell:
+ new_cell = self.maze.get_neighbor_cell(
+ self.current_cell, DirectionTypePanelDir.CHANNEL_WEST
+ )
+ return self._move_to_cell(new_cell)
+ return None
+
+ def channel_right(self) -> Optional[OperatorRobotCell]:
+ if self.maze and self.current_cell:
+ new_cell = self.maze.get_neighbor_cell(
+ self.current_cell, DirectionTypePanelDir.CHANNEL_EAST
+ )
+ return self._move_to_cell(new_cell)
+ return None
+
+ def lift_diag(self) -> Optional[OperatorRobotCell]:
+ if self.maze and self.current_cell:
+ new_cell = self.maze.get_neighbor_cell(
+ self.current_cell, DirectionTypePanelDir.DIAG_NW
+ )
+ return self._move_to_cell(new_cell)
+ return None
+
+ def descend_diag(self) -> Optional[OperatorRobotCell]:
+ if self.maze and self.current_cell:
+ new_cell = self.maze.get_neighbor_cell(
+ self.current_cell, DirectionTypePanelDir.DIAG_SE
+ )
+ return self._move_to_cell(new_cell)
+ return None
+
+ def panel(self):
+ if self.current_cell and self.current_cell.cell_type == CellTypePanel.PANEL:
+ self.current_cell.cell_type = CellTypePanel.ACTIVE
+
+ def floor(self):
+ if self.current_cell and self.current_cell.cell_type == CellTypePanel.FLOOR:
+ self.current_cell.cell_type = CellTypePanel.PANEL
diff --git "a/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/app.js" "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/app.js"
new file mode 100644
index 0000000..3ed7d02
--- /dev/null
+++ "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/app.js"
@@ -0,0 +1,145 @@
+const WIDTH = 7;
+const HEIGHT = 5;
+
+let maze;
+let robot;
+let isWorking = false;
+
+function createTestGrid(width, height) {
+ const cellValues = [];
+ for (let y = 0; y < height; y++) {
+ const row = [];
+ for (let x = 0; x < width; x++) {
+ let cellValue;
+ if (x === 0 && y === 0) {
+ cellValue = CellType.FLOOR | 0x8;
+ } else if (x === width - 1 && y === height - 1) {
+ cellValue = CellType.FINISH;
+ } else if ((x === 2 && y === 1) || (x === 3 && y === 2)) {
+ cellValue = CellType.WALL;
+ } else if (x === 1 && y === 2) {
+ cellValue = CellType.BLOCK;
+ } else if ((x + y) % 3 === 0) {
+ cellValue = CellType.PANEL;
+ } else {
+ cellValue = CellType.FLOOR;
+ }
+ row.push(cellValue);
+ }
+ cellValues.push(row);
+ }
+ return cellValues;
+}
+
+function drawGrid() {
+ const grid = document.getElementById('grid');
+ grid.innerHTML = '';
+ grid.style.gridTemplateColumns = `repeat(${WIDTH}, 70px)`;
+
+ for (let y = HEIGHT - 1; y >= 0; y--) {
+ for (let x = 0; x < WIDTH; x++) {
+ const cell = maze.getCell(x, y);
+ const cellDiv = document.createElement('div');
+ cellDiv.className = 'cell';
+
+ const typeNames = ['floor', 'panel', 'active', 'sluice', 'wall', 'block', 'finish'];
+ const labels = ['П', 'Пн', 'А', 'Ш', '■', '▓', 'F'];
+
+ cellDiv.classList.add(typeNames[cell.type] || 'floor');
+ cellDiv.textContent = labels[cell.type] || '';
+
+ if (cell.hasRobot) {
+ const robotDiv = document.createElement('div');
+ robotDiv.className = 'robot';
+ robotDiv.textContent = 'R';
+ cellDiv.appendChild(robotDiv);
+ }
+
+ grid.appendChild(cellDiv);
+ }
+ }
+}
+
+function updateInfo(text) {
+ document.getElementById('info').textContent = text;
+}
+
+function makeStep() {
+ const cell = robot.currentCell;
+ if (!cell) return;
+
+ if (cell.type === CellType.FINISH) {
+ updateInfo('Работа завершена');
+ isWorking = false;
+ document.getElementById('startBtn').disabled = false;
+ drawGrid();
+ return;
+ }
+
+ if (cell.type === CellType.FLOOR) {
+ robot.floor();
+ updateInfo(`Позиция (${robot.currentCell.x}, ${robot.currentCell.y}): Пол → Панель`);
+ drawGrid();
+ return;
+ } else if (cell.type === CellType.PANEL) {
+ robot.panel();
+ updateInfo(`Позиция (${robot.currentCell.x}, ${robot.currentCell.y}): Панель → Активно`);
+ drawGrid();
+ return;
+ }
+
+ const moved = robot.moveTowardsGoal();
+ if (!moved) {
+ updateInfo('Нет доступного пути к целям');
+ isWorking = false;
+ document.getElementById('startBtn').disabled = false;
+ } else {
+ updateInfo(`Движение к позиции (${robot.currentCell.x}, ${robot.currentCell.y})`);
+ }
+ drawGrid();
+}
+
+function startWork() {
+ isWorking = true;
+ document.getElementById('startBtn').disabled = true;
+ automaticWork();
+}
+
+function automaticWork() {
+ if (!isWorking) {
+ document.getElementById('startBtn').disabled = false;
+ return;
+ }
+
+ const cell = robot.currentCell;
+
+ if (cell && cell.type === CellType.FINISH) {
+ updateInfo('Работа завершена');
+ isWorking = false;
+ document.getElementById('startBtn').disabled = false;
+ return;
+ }
+
+ makeStep();
+
+ if (isWorking) {
+ setTimeout(automaticWork, 200);
+ }
+}
+
+function resetGame() {
+ isWorking = false;
+ document.getElementById('startBtn').disabled = false;
+
+ const cellValues = createTestGrid(WIDTH, HEIGHT);
+ maze = new OperatorRobotMaze(WIDTH, HEIGHT, cellValues);
+ robot = new OperatorRobot(maze);
+
+ drawGrid();
+ updateInfo('Готов к работе');
+}
+
+const cellValues = createTestGrid(WIDTH, HEIGHT);
+maze = new OperatorRobotMaze(WIDTH, HEIGHT, cellValues);
+robot = new OperatorRobot(maze);
+drawGrid();
\ No newline at end of file
diff --git "a/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/index.html" "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/index.html"
new file mode 100644
index 0000000..e953afd
--- /dev/null
+++ "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/index.html"
@@ -0,0 +1,58 @@
+
+
+
+
+
+ Робот Оператор - Панели
+
+
+
+
+
🤖 Робот Оператор
+
+
+ Готов к работе
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git "a/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/lab2.mp4" "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/lab2.mp4"
new file mode 100644
index 0000000..16924b7
Binary files /dev/null and "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/lab2.mp4" differ
diff --git "a/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/robot.js" "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/robot.js"
new file mode 100644
index 0000000..401fdbc
--- /dev/null
+++ "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/robot.js"
@@ -0,0 +1,253 @@
+const CellType = {
+ FLOOR: 0x0,
+ PANEL: 0x1,
+ ACTIVE: 0x2,
+ SLUICE: 0x3,
+ WALL: 0x4,
+ BLOCK: 0x5,
+ FINISH: 0x6
+};
+
+class OperatorRobotCell {
+ constructor(x, y, cellValue = 0x0) {
+ this.x = x;
+ this.y = y;
+ this.hasRobot = (cellValue & 0x8) !== 0;
+ this.type = this.cellTypeFromValue(cellValue);
+ }
+
+ cellTypeFromValue(value) {
+ const baseValue = value & 0x7;
+ return baseValue;
+ }
+}
+
+class OperatorRobotMaze {
+ constructor(width = 0, height = 0, cells = null) {
+ this.width = width;
+ this.height = height;
+ this.cells = [];
+
+ if (cells !== null) {
+ this.loadFromValues(cells);
+ } else if (width > 0 && height > 0) {
+ this.initializeMaze();
+ }
+ }
+
+ loadFromValues(cellValues) {
+ if (!cellValues || cellValues.length === 0) {
+ this.height = 0;
+ this.width = 0;
+ this.cells = [];
+ return;
+ }
+
+ this.height = cellValues.length;
+ this.width = cellValues[0].length;
+ this.cells = [];
+
+ for (let y = 0; y < this.height; y++) {
+ const row = [];
+ for (let x = 0; x < this.width; x++) {
+ const cellValue = cellValues[y][x];
+ const cell = new OperatorRobotCell(x, y, cellValue);
+ row.push(cell);
+ }
+ this.cells.push(row);
+ }
+ }
+
+ initializeMaze(cellType = CellType.FLOOR) {
+ this.cells = [];
+ for (let y = 0; y < this.height; y++) {
+ const row = [];
+ for (let x = 0; x < this.width; x++) {
+ const cell = new OperatorRobotCell(x, y, cellType);
+ row.push(cell);
+ }
+ this.cells.push(row);
+ }
+ }
+
+ getCell(x, y) {
+ if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
+ return this.cells[y][x];
+ }
+ return null;
+ }
+
+ isFree(x, y) {
+ if (x < 0 || x >= this.width || y < 0 || y >= this.height) return false;
+ const cell = this.getCell(x, y);
+ return cell && cell.type !== CellType.WALL && cell.type !== CellType.BLOCK;
+ }
+}
+
+class OperatorRobot {
+ constructor(maze) {
+ this.maze = maze;
+ this.currentCell = null;
+
+ if (maze && maze.cells) {
+ for (let y = 0; y < maze.height; y++) {
+ for (let x = 0; x < maze.width; x++) {
+ const cell = maze.getCell(x, y);
+ if (cell && cell.hasRobot) {
+ this.currentCell = cell;
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ moveToCell(newCell) {
+ if (newCell && newCell.type !== CellType.WALL && newCell.type !== CellType.BLOCK) {
+ if (this.currentCell) {
+ this.currentCell.hasRobot = false;
+ }
+ this.currentCell = newCell;
+ newCell.hasRobot = true;
+ return newCell;
+ }
+ return null;
+ }
+
+ channelForward() {
+ if (this.maze && this.currentCell) {
+ const newCell = this.maze.getCell(this.currentCell.x, this.currentCell.y + 1);
+ return this.moveToCell(newCell);
+ }
+ return null;
+ }
+
+ channelBackward() {
+ if (this.maze && this.currentCell) {
+ const newCell = this.maze.getCell(this.currentCell.x, this.currentCell.y - 1);
+ return this.moveToCell(newCell);
+ }
+ return null;
+ }
+
+ channelLeft() {
+ if (this.maze && this.currentCell) {
+ const newCell = this.maze.getCell(this.currentCell.x - 1, this.currentCell.y);
+ return this.moveToCell(newCell);
+ }
+ return null;
+ }
+
+ channelRight() {
+ if (this.maze && this.currentCell) {
+ const newCell = this.maze.getCell(this.currentCell.x + 1, this.currentCell.y);
+ return this.moveToCell(newCell);
+ }
+ return null;
+ }
+
+ liftDiag() {
+ if (this.maze && this.currentCell) {
+ const newCell = this.maze.getCell(this.currentCell.x - 1, this.currentCell.y + 1);
+ return this.moveToCell(newCell);
+ }
+ return null;
+ }
+
+ descendDiag() {
+ if (this.maze && this.currentCell) {
+ const newCell = this.maze.getCell(this.currentCell.x + 1, this.currentCell.y - 1);
+ return this.moveToCell(newCell);
+ }
+ return null;
+ }
+
+ panel() {
+ if (this.currentCell && this.currentCell.type === CellType.PANEL) {
+ this.currentCell.type = CellType.ACTIVE;
+ }
+ }
+
+ floor() {
+ if (this.currentCell && this.currentCell.type === CellType.FLOOR) {
+ this.currentCell.type = CellType.PANEL;
+ }
+ }
+
+ findNearestTargets() {
+ const targets = [];
+ for (let y = 0; y < this.maze.height; y++) {
+ for (let x = 0; x < this.maze.width; x++) {
+ const cell = this.maze.getCell(x, y);
+ if (cell && (cell.type === CellType.FLOOR || cell.type === CellType.PANEL)) {
+ targets.push([x, y]);
+ }
+ }
+ }
+ if (targets.length > 0) return targets;
+
+ for (let y = 0; y < this.maze.height; y++) {
+ for (let x = 0; x < this.maze.width; x++) {
+ const cell = this.maze.getCell(x, y);
+ if (cell && cell.type === CellType.FINISH) {
+ return [[x, y]];
+ }
+ }
+ }
+ return [];
+ }
+
+ bfsNextStep(start, goals) {
+ const goalsSet = new Set(goals.map(g => `${g[0]},${g[1]}`));
+ const queue = [[start[0], start[1]]];
+ const prev = { [`${start[0]},${start[1]}`]: null };
+
+ while (queue.length > 0) {
+ const [x, y] = queue.shift();
+ const key = `${x},${y}`;
+
+ if (goalsSet.has(key)) {
+ let cur = [x, y];
+ let curKey = key;
+ const startKey = `${start[0]},${start[1]}`;
+
+ while (prev[curKey] !== null) {
+ const prevPos = prev[curKey];
+ const prevKey = `${prevPos[0]},${prevPos[1]}`;
+ if (prevKey === startKey) {
+ return cur;
+ }
+ cur = prevPos;
+ curKey = prevKey;
+ }
+ return null;
+ }
+
+ const neighbors = [
+ [x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1],
+ [x - 1, y + 1], [x + 1, y - 1]
+ ];
+
+ for (const [nx, ny] of neighbors) {
+ const nKey = `${nx},${ny}`;
+ if (!prev.hasOwnProperty(nKey) && this.maze.isFree(nx, ny)) {
+ prev[nKey] = [x, y];
+ queue.push([nx, ny]);
+ }
+ }
+ }
+ return null;
+ }
+
+ moveTowardsGoal() {
+ const goals = this.findNearestTargets();
+ if (goals.length === 0) return false;
+
+ const nextPos = this.bfsNextStep([this.currentCell.x, this.currentCell.y], goals);
+ if (!nextPos) return false;
+
+ const [nx, ny] = nextPos;
+ const newCell = this.maze.getCell(nx, ny);
+ return this.moveToCell(newCell) !== null;
+ }
+}
\ No newline at end of file
diff --git "a/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/robot_server.py " "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/robot_server.py "
new file mode 100644
index 0000000..b61f565
--- /dev/null
+++ "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/robot_server.py "
@@ -0,0 +1,350 @@
+from enum import Enum
+from typing import Optional, List, Tuple
+from collections import deque
+import json
+
+
+class CellType(Enum):
+ FLOOR = 0x0
+ PANEL = 0x1
+ ACTIVE = 0x2
+ SLUICE = 0x3
+ WALL = 0x4
+ BLOCK = 0x5
+ FINISH = 0x6
+
+
+class OperatorRobotCell:
+ def __init__(self, x: int = 0, y: int = 0, cell_value: int = 0x0):
+ self.x = x
+ self.y = y
+ self.has_robot = (cell_value & 0x8) != 0
+ self.cell_type = self._cell_type_from_value(cell_value)
+
+ def _cell_type_from_value(self, value: int) -> CellType:
+ base_value = value & 0x7
+ for cell_type in CellType:
+ if cell_type.value == base_value:
+ return cell_type
+ return CellType.FLOOR
+
+ def to_dict(self):
+ return {
+ 'x': self.x,
+ 'y': self.y,
+ 'type': self.cell_type.value,
+ 'has_robot': self.has_robot
+ }
+
+
+class OperatorRobotMaze:
+ def __init__(self, width: int = None, height: int = None, cells: List[List[int]] = None):
+ self.width = width if width is not None else 0
+ self.height = height if height is not None else 0
+ self.cells = []
+
+ if cells is not None:
+ self.load_from_values(cells)
+ elif width is not None and height is not None:
+ self.initialize_maze()
+
+ def load_from_values(self, cell_values: List[List[int]]):
+ if not cell_values:
+ self.height = 0
+ self.width = 0
+ self.cells = []
+ return
+
+ self.height = len(cell_values)
+ self.width = len(cell_values[0]) if self.height > 0 else 0
+ self.cells = []
+
+ for y in range(self.height):
+ row = []
+ for x in range(self.width):
+ cell_value = cell_values[y][x]
+ cell = OperatorRobotCell(x, y, cell_value)
+ row.append(cell)
+ self.cells.append(row)
+
+ def initialize_maze(self, cell_type: CellType = None):
+ if cell_type is None:
+ cell_type = CellType.FLOOR
+
+ self.cells = []
+ for y in range(self.height):
+ row = []
+ for x in range(self.width):
+ cell_value = cell_type.value
+ cell = OperatorRobotCell(x, y, cell_value)
+ row.append(cell)
+ self.cells.append(row)
+
+ def get_cell(self, x: int, y: int) -> Optional[OperatorRobotCell]:
+ if 0 <= x < self.width and 0 <= y < self.height:
+ return self.cells[y][x]
+ return None
+
+ def is_free(self, x: int, y: int) -> bool:
+ if not (0 <= x < self.width and 0 <= y < self.height):
+ return False
+ cell = self.get_cell(x, y)
+ if not cell:
+ return False
+ return cell.cell_type not in (CellType.WALL, CellType.BLOCK)
+
+ def to_dict(self):
+ return {
+ 'width': self.width,
+ 'height': self.height,
+ 'cells': [[cell.to_dict() for cell in row] for row in self.cells]
+ }
+
+
+class OperatorRobot:
+ def __init__(self, maze: OperatorRobotMaze = None):
+ self.maze = maze
+ self.current_cell = None
+
+ if maze and maze.cells:
+ for y in range(maze.height):
+ for x in range(maze.width):
+ cell = maze.get_cell(x, y)
+ if cell and cell.has_robot:
+ self.current_cell = cell
+ return
+
+ def _move_to_cell(self, new_cell: Optional[OperatorRobotCell]) -> Optional[OperatorRobotCell]:
+ if new_cell and new_cell.cell_type not in [CellType.WALL, CellType.BLOCK]:
+ if self.current_cell:
+ self.current_cell.has_robot = False
+ self.current_cell = new_cell
+ new_cell.has_robot = True
+ return new_cell
+ return None
+
+ def channel_forward(self) -> Optional[OperatorRobotCell]:
+ if self.maze and self.current_cell:
+ new_cell = self.maze.get_cell(self.current_cell.x, self.current_cell.y + 1)
+ return self._move_to_cell(new_cell)
+ return None
+
+ def channel_backward(self) -> Optional[OperatorRobotCell]:
+ if self.maze and self.current_cell:
+ new_cell = self.maze.get_cell(self.current_cell.x, self.current_cell.y - 1)
+ return self._move_to_cell(new_cell)
+ return None
+
+ def channel_left(self) -> Optional[OperatorRobotCell]:
+ if self.maze and self.current_cell:
+ new_cell = self.maze.get_cell(self.current_cell.x - 1, self.current_cell.y)
+ return self._move_to_cell(new_cell)
+ return None
+
+ def channel_right(self) -> Optional[OperatorRobotCell]:
+ if self.maze and self.current_cell:
+ new_cell = self.maze.get_cell(self.current_cell.x + 1, self.current_cell.y)
+ return self._move_to_cell(new_cell)
+ return None
+
+ def lift_diag(self) -> Optional[OperatorRobotCell]:
+ if self.maze and self.current_cell:
+ new_cell = self.maze.get_cell(self.current_cell.x - 1, self.current_cell.y + 1)
+ return self._move_to_cell(new_cell)
+ return None
+
+ def descend_diag(self) -> Optional[OperatorRobotCell]:
+ if self.maze and self.current_cell:
+ new_cell = self.maze.get_cell(self.current_cell.x + 1, self.current_cell.y - 1)
+ return self._move_to_cell(new_cell)
+ return None
+
+ def panel(self):
+ if self.current_cell and self.current_cell.cell_type == CellType.PANEL:
+ self.current_cell.cell_type = CellType.ACTIVE
+
+ def floor(self):
+ if self.current_cell and self.current_cell.cell_type == CellType.FLOOR:
+ self.current_cell.cell_type = CellType.PANEL
+
+ def find_nearest_targets(self) -> List[Tuple[int, int]]:
+ targets = []
+ for y in range(self.maze.height):
+ for x in range(self.maze.width):
+ cell = self.maze.get_cell(x, y)
+ if cell and cell.cell_type in (CellType.FLOOR, CellType.PANEL):
+ targets.append((x, y))
+
+ if targets:
+ return targets
+
+ for y in range(self.maze.height):
+ for x in range(self.maze.width):
+ cell = self.maze.get_cell(x, y)
+ if cell and cell.cell_type == CellType.FINISH:
+ return [(x, y)]
+
+ return []
+
+ def bfs_next_step(self, start: Tuple[int, int], goals: List[Tuple[int, int]]) -> Optional[Tuple[int, int]]:
+ goals_set = set(goals)
+ queue = deque([start])
+ prev = {start: None}
+
+ while queue:
+ x, y = queue.popleft()
+
+ if (x, y) in goals_set:
+ cur = (x, y)
+ while prev[cur] is not None and prev[cur] != start:
+ cur = prev[cur]
+ return cur if cur != start else None
+
+ neighbors = [
+ (x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1),
+ (x - 1, y + 1), (x + 1, y - 1)
+ ]
+
+ for nx, ny in neighbors:
+ if (nx, ny) not in prev and self.maze.is_free(nx, ny):
+ prev[(nx, ny)] = (x, y)
+ queue.append((nx, ny))
+
+ return None
+
+ def move_towards_goal(self) -> bool:
+ goals = self.find_nearest_targets()
+ if not goals:
+ return False
+
+ next_pos = self.bfs_next_step((self.current_cell.x, self.current_cell.y), goals)
+ if not next_pos:
+ return False
+
+ new_cell = self.maze.get_cell(next_pos[0], next_pos[1])
+ return self._move_to_cell(new_cell) is not None
+
+ def make_step(self) -> dict:
+ """Выполнить один шаг и вернуть результат"""
+ if not self.current_cell:
+ return {'status': 'error', 'message': 'Робот не найден'}
+
+ cell = self.current_cell
+
+ if cell.cell_type == CellType.FINISH:
+ return {
+ 'status': 'finished',
+ 'message': 'Работа завершена',
+ 'position': (cell.x, cell.y)
+ }
+
+ if cell.cell_type == CellType.FLOOR:
+ self.floor()
+ return {
+ 'status': 'processing',
+ 'message': f'Позиция ({cell.x}, {cell.y}): Пол → Панель',
+ 'position': (cell.x, cell.y)
+ }
+ elif cell.cell_type == CellType.PANEL:
+ self.panel()
+ return {
+ 'status': 'processing',
+ 'message': f'Позиция ({cell.x}, {cell.y}): Панель → Активно',
+ 'position': (cell.x, cell.y)
+ }
+
+ moved = self.move_towards_goal()
+ if not moved:
+ return {
+ 'status': 'error',
+ 'message': 'Нет доступного пути к целям'
+ }
+
+ return {
+ 'status': 'moving',
+ 'message': f'Движение к позиции ({self.current_cell.x}, {self.current_cell.y})',
+ 'position': (self.current_cell.x, self.current_cell.y)
+ }
+
+ def get_state(self) -> dict:
+ """Получить текущее состояние робота и лабиринта"""
+ return {
+ 'maze': self.maze.to_dict(),
+ 'robot_position': (self.current_cell.x, self.current_cell.y) if self.current_cell else None
+ }
+
+
+def create_test_grid(width: int, height: int) -> List[List[int]]:
+ """Создать тестовую сетку"""
+ cell_values = []
+
+ for y in range(height):
+ row = []
+ for x in range(width):
+ if x == 0 and y == 0:
+ cell_value = CellType.FLOOR.value | 0x8 # Пол + робот
+ elif x == width - 1 and y == height - 1:
+ cell_value = CellType.FINISH.value
+ elif (x == 2 and y == 1) or (x == 3 and y == 2):
+ cell_value = CellType.WALL.value
+ elif x == 1 and y == 2:
+ cell_value = CellType.BLOCK.value
+ elif (x + y) % 3 == 0:
+ cell_value = CellType.PANEL.value
+ else:
+ cell_value = CellType.FLOOR.value
+
+ row.append(cell_value)
+ cell_values.append(row)
+
+ return cell_values
+
+
+# Пример использования с Flask
+"""
+from flask import Flask, jsonify, request
+from flask_cors import CORS
+
+app = Flask(__name__)
+CORS(app)
+
+# Глобальное состояние
+game_state = None
+
+@app.route('/api/init', methods=['POST'])
+def init_game():
+ global game_state
+ data = request.json
+ width = data.get('width', 7)
+ height = data.get('height', 5)
+
+ cell_values = create_test_grid(width, height)
+ maze = OperatorRobotMaze(width, height, cell_values)
+ robot = OperatorRobot(maze)
+
+ game_state = {'maze': maze, 'robot': robot}
+
+ return jsonify(robot.get_state())
+
+@app.route('/api/step', methods=['POST'])
+def make_step():
+ if not game_state:
+ return jsonify({'error': 'Game not initialized'}), 400
+
+ result = game_state['robot'].make_step()
+ state = game_state['robot'].get_state()
+
+ return jsonify({
+ 'result': result,
+ 'state': state
+ })
+
+@app.route('/api/state', methods=['GET'])
+def get_state():
+ if not game_state:
+ return jsonify({'error': 'Game not initialized'}), 400
+
+ return jsonify(game_state['robot'].get_state())
+
+if __name__ == '__main__':
+ app.run(debug=True, port=5000)
\ No newline at end of file
diff --git "a/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/styles.css" "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/styles.css"
new file mode 100644
index 0000000..a8ee65a
--- /dev/null
+++ "b/5030102_30201/\320\233\320\270\321\202\320\276\320\262\321\207\320\265\320\275\320\272\320\276/lab2/styles.css"
@@ -0,0 +1,186 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ min-height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 20px;
+}
+
+.container {
+ background: white;
+ border-radius: 20px;
+ padding: 30px;
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
+ max-width: 900px;
+}
+
+h1 {
+ text-align: center;
+ color: #667eea;
+ margin-bottom: 20px;
+ font-size: 2em;
+}
+
+.info-panel {
+ background: #f8f9fa;
+ padding: 15px;
+ border-radius: 10px;
+ margin-bottom: 20px;
+ text-align: center;
+ font-size: 1.1em;
+ color: #333;
+ min-height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.grid-container {
+ display: inline-block;
+ border: 3px solid #667eea;
+ border-radius: 10px;
+ overflow: hidden;
+ margin: 0 auto 20px;
+ display: block;
+ width: fit-content;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.grid {
+ display: grid;
+ gap: 2px;
+ background: #ddd;
+ padding: 2px;
+}
+
+.cell {
+ width: 70px;
+ height: 70px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+ font-size: 14px;
+ position: relative;
+ transition: all 0.3s ease;
+}
+
+.cell.floor { background: white; color: #999; }
+.cell.panel { background: #87ceeb; color: #333; }
+.cell.active { background: #32cd32; color: white; }
+.cell.sluice { background: #808080; color: white; }
+.cell.wall { background: #000; color: white; }
+.cell.block { background: #555; color: white; }
+.cell.finish { background: #ffd700; color: #333; }
+
+.robot {
+ position: absolute;
+ width: 40px;
+ height: 40px;
+ background: #ff4444;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-size: 18px;
+ font-weight: bold;
+ border: 3px solid #cc0000;
+ z-index: 10;
+ animation: pulse 1s infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { transform: scale(1); }
+ 50% { transform: scale(1.1); }
+}
+
+.controls {
+ display: flex;
+ gap: 15px;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+button {
+ padding: 12px 30px;
+ font-size: 16px;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-weight: bold;
+ text-transform: uppercase;
+}
+
+button.start {
+ background: #32cd32;
+ color: white;
+}
+
+button.start:hover {
+ background: #28a428;
+ transform: translateY(-2px);
+ box-shadow: 0 5px 15px rgba(50, 205, 50, 0.4);
+}
+
+button.step {
+ background: #667eea;
+ color: white;
+}
+
+button.step:hover {
+ background: #5568d3;
+ transform: translateY(-2px);
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
+}
+
+button.reset {
+ background: #ff6b6b;
+ color: white;
+}
+
+button.reset:hover {
+ background: #ee5555;
+ transform: translateY(-2px);
+ box-shadow: 0 5px 15px rgba(255, 107, 107, 0.4);
+}
+
+button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none !important;
+}
+
+.legend {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 10px;
+ margin-top: 20px;
+ padding: 15px;
+ background: #f8f9fa;
+ border-radius: 10px;
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 14px;
+}
+
+.legend-color {
+ width: 30px;
+ height: 30px;
+ border-radius: 5px;
+ border: 2px solid #ddd;
+}
\ No newline at end of file