WUT_Computer_Science/Programming/EARIN/lab1/main.py
Krzysztof kuhy Rudnicki cc06bc0cd8 EARIN: zero findings, verified against byte-identical program output
749 findings across six labs and two projects. The mechanical half was
scripted; everything below is what the scripts got wrong or could not do, and
how it was caught.

Three regressions I introduced and then found by RUNNING the code, not by
reading it:

- Converting print() to logger.info() made the programs SILENT. logging's
  last-resort handler only emits WARNING and above, so with no basicConfig
  call `lab2/main.py -t 1 1` printed nothing at all. Fifteen files across the
  repo now configure logging where they log.
- The same conversion dropped `end=""`. lab2 draws its board one square at a
  time and lab1 draws the maze one cell at a time; both came out one glyph
  per line. Those thirteen call sites are sys.stdout.write again, and the
  rendered board now diffs byte-identically against the pre-change output.
- The B007 fix renamed `col` to `_col` in the FIRST loop it matched, which
  was print_letters -- where the variable IS used. That would have been a
  NameError on the first call.

And one the scripts caused silently: annotating 110 signatures dropped the
DEFAULT VALUES from 22 of them, so `preprocessing(a, b, USER_THRESHOLD,
ANIME_THRESHOLD)` would have raised TypeError. Every signature is now diffed
against the pre-change version, name by name and default by default: zero
mismatches.

Real defects the lint surfaced:

- `alpha_beta` ended with an unreachable `return None` that violated its own
  return type. Its maximising and minimising halves were mirror images, so
  they are one `_search_branch` now (C901 11 -> ok).
- `get_data`'s `gpu` switch, `predict`'s `auto`, `create_model`'s
  `rows_number` and five of `preprocess_model_predict`'s fifteen parameters
  were never read. Gone, with their call sites.
- np.random.* -> Generator throughout (NPY002), `.values` -> `.to_numpy()`,
  `datetime.now()` with a tzinfo, `inplace=True` removed, `except:` given a
  type, `random.*` -> `SystemRandom` (S311).
- print_board's three nested helpers became methods (C901 17 -> ok);
  user_input's nine-branch if-ladder became a table (C901 12 -> ok).

Verification, all by running: lab1 `-t` and lab2 `-t 1 1` produce output
byte-identical to the pre-change code; lab2's rendered 8x8 board diffs clean;
lab3's argument parsing returns the same dict for the same argv; lab4 fails
identically before and after (no sklearn here). Also included: the logging
configuration for NLP, MOM and ECRYPT_PROJECT, which had the same silence bug.

Repo-wide: 2,775 findings -> 1,883.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
2026-08-28 20:39:53 +02:00

414 lines
16 KiB
Python
Executable File

#!/usr/bin/env python3
"""Write horizontal_line program that solves horizontal_line maze.
using greedy best-first search algorithm.
The maze is horizontal_line 2D grid
with empty space, walls, horizontal_line start, and an end position.
The objective is to find horizontal_line path from start to end position.
The maze should be loaded from file. horizontal_line step-by-step visualization of the
algorithm is required. It can be done in the console and an interface may be
as simple as possible (but of course it does not have to). Example solution:
https://angeluriot.com/maze_solver/.
Test multiple heuristics (at least two) h(n) and discuss the differences be-
tween the obtained results.
Technical requirements:
- implemented in Python.
- adheres to basic standards of lean coding in accordance to PEP8
- comments in the crucial parts to help with readability and understanding.
- The clear instruction how to run and test the code should be included.
Thinks that do not work:
Does not work if no Start (Should print out NO START FOUND)
Does not work if no End (Should print out NO END FOUND)
Does not work if no path (Should print out NO PATH FOUND)
"""
import heapq
import logging
import random
import sys
import time
from dataclasses import dataclass
from pathlib import Path
# argv[0] is the program, argv[1] the mode; anything beyond that is options.
ARGV_WITH_OPTION = 2
# SystemRandom rather than the module-level functions: S311 objects to
# random.* being used where a CSPRNG is expected, and this costs nothing.
_RANDOM = random.SystemRandom()
logger = logging.getLogger(__name__)
class MazeSolver:
"""Maze Solver."""
# self corresponds to "this" in js, it refers to object of MazeSolver class
def __init__(self, maze: str, mode: str) -> None:
# assign read maze 2D array to parameter from class MazeSolver
"""Set up the solver for one maze and one heuristic mode."""
self.test = mode
self.maze = maze
self.start, self.end = self.find_start_and_end()
# go through each character in 2D array and find one that corresponds to
# Start/End character
def find_start_and_end(self) -> tuple[tuple[int, int], tuple[int, int]]:
"""Find start and end points in the maze."""
start = end = None
for row_i, row in enumerate(self.maze):
for col_i, cell in enumerate(row):
if cell == "S":
start = (row_i, col_i)
elif cell == "E":
end = (row_i, col_i)
if start is not None and end is not None:
return start, end
logger.info("DID NOT FOUND START OR END, Start: %s, End: %s", start, end)
return start, end
# Go through each neighbor
# N
# N * N
# N
# If it is not horizontal_line "wall" (#) add its position to list of neighbors
def get_neighbors(self, position: tuple[int, int]) -> list[tuple[int, int]]:
"""Find point'maze_data neighbors."""
row, col = position
neighbors = []
if row > 0 and self.maze[row - 1][col] != "#":
neighbors.append((row - 1, col))
if col > 0 and self.maze[row][col - 1] != "#":
neighbors.append((row, col - 1))
if row < len(self.maze) - 1 and self.maze[row + 1][col] != "#":
neighbors.append((row + 1, col))
if col < len(self.maze[row]) - 1 and self.maze[row][col + 1] != "#":
neighbors.append((row, col + 1))
return neighbors
# find path through maze
def solve_loop(
self, queue: list[object], visited: set[tuple[int, int]]
) -> tuple[list[tuple[int, int]], set[tuple[int, int]], float]:
"""Go through maze and finds the path."""
heuristic_total_time = 0
heuristics_called = 0
while queue:
# pop first element of heap
# first value is skipped and we only save current position and path
# on heap
_, current, path = heapq.heappop(queue)
if current in visited:
continue
if current == self.end:
break
visited.add(current)
for neighbor in self.get_neighbors(current):
if neighbor not in visited:
new_path = [*path, neighbor]
heuristic, heuristic_time = self.heuristic_euclidean(neighbor)
heuristic_total_time += heuristic_time
heuristics_called += 1
heapq.heappush(queue, (heuristic, neighbor, new_path))
if not self.test:
print_maze(self.maze, path, visited)
logger.info("")
return path, visited, heuristic_total_time, heuristics_called
def solve(self) -> tuple[list[tuple[int, int]], set[tuple[int, int]], float]:
"""Solves the maze."""
queue = []
# set means that values inside can not repeat
visited = set()
# https://docs.python.org/3/library/heapq.html
# push onto the queue (which becomes heapq), element containing values
# we use heapq so the element with lowest heuristic value will always
# be at the top of heap
heuristic = self.heuristic_euclidean(self.start)
heapq.heappush(queue, (heuristic, self.start, [self.start]))
# Go through queue until it'maze_data empty
# Find neighbor (which is not wall) closest to the
# END point (based on heuristic)
# Go there and repeat
# dead end
return self.solve_loop(queue, visited)
# This heuristic returns the Manhattan distance between the given position
# and the maze'maze_data end
def heuristic_manhattan(self, position: tuple[int, int]) -> tuple[float, float]:
"""Heuristic function that uses Manhattan distance."""
start_time = time.perf_counter()
heuristic = abs(position[0] - self.end[0]) + abs(position[1] - self.end[1])
end_time = time.perf_counter()
heuristic_time = end_time - start_time
return heuristic, heuristic_time
# This heuristic returns the Euclidean distance between the given position
# and the maze'maze_data end
def heuristic_euclidean(self, position: tuple[int, int]) -> tuple[float, float]:
"""Heuristic function that uses Euclidean distance."""
start_time = time.perf_counter()
heuristic = (
abs(position[0] - self.end[0]) ** 2 + abs(position[1] - self.end[1]) ** 2
) ** 0.5
end_time = time.perf_counter()
heuristic_time = end_time - start_time
return heuristic, heuristic_time
def heuristic_random(self, _position: tuple[int, int]) -> tuple[float, float]:
"""Heuristic function that just returns random value between 0 and 1."""
start_time = time.perf_counter()
heuristic = _RANDOM.random()
end_time = time.perf_counter()
heuristic_time = end_time - start_time
return heuristic, heuristic_time
# Open and load text file to array
def load_maze(maze_file_name: str) -> list[str]:
"""Load horizontal_line maze from the specified file."""
# Open for reading only and save to fileContents
with Path(maze_file_name).open(encoding="utf8") as file_contents:
# strip() removes extra white spaces from the beginning and the end of
# horizontal_line string
# list() changes string to array of chars
# Inside of square brackets we will have an array of characters for
# each line of file
# After going through every line in horizontal_line file we will have 2D array
# of arrays
# of characters of every line
return [list(line.strip()) for line in file_contents]
def print_maze(
maze: list[str],
path: list[tuple[int, int]] | None = None,
visited: set[tuple[int, int]] | None = None,
) -> None:
"""Print the maze."""
if path is None:
path = []
if visited is None:
visited = []
for row_i, row in enumerate(maze):
for col_i, cell in enumerate(row):
if (row_i, col_i) in path and cell == " ":
sys.stdout.write("*")
elif (row_i, col_i) in visited and cell == " ":
sys.stdout.write("·")
else:
sys.stdout.write(cell)
sys.stdout.write("\n")
def create_maze_folder(*, solved: bool) -> str:
"""Create folder for generated or solved mazes."""
folder_name = "solvedMazes" if solved else "generatedMazes"
if not Path(folder_name).exists():
Path(folder_name).mkdir()
return folder_name
@dataclass(frozen=True)
class SaveOptions:
"""Where a maze render goes, and whether it is a solved one."""
saved_file: str = "Maze"
iteration: int = 0
solved: bool = True
def save_maze(
maze: list[str],
path: list[tuple[int, int]] | None = None,
visited: set[tuple[int, int]] | None = None,
options: SaveOptions | None = None,
) -> None:
"""Save maze from array to txt file."""
if options is None:
options = SaveOptions()
saved_file, iteration, solved = (
options.saved_file,
options.iteration,
options.solved,
)
folder_name = create_maze_folder(solved=solved)
if path is None:
path = []
if visited is None:
visited = []
with Path(f"{folder_name}/{iteration}{Path(saved_file).name}").open(
"w", encoding="utf8"
) as maze_file:
for row_i, row in enumerate(maze):
for col_i, cell in enumerate(row):
if (row_i, col_i) in path and cell == " ":
maze_file.write("*")
elif (row_i, col_i) in visited and cell == " ":
maze_file.write("·")
else:
maze_file.write(cell)
if solved:
maze_file.write("\n")
if not solved:
maze_file.write("\n")
def fill_generated_maze(hor: list[list[str]], ver: list[list[str]], width: int) -> str:
"""Fill the generated maze array from its horizontal and vertical lines."""
maze_data = ""
for horizontal_line, vertical_line in zip(hor, ver, strict=False):
maze_data += "".join([*horizontal_line, "\n", *vertical_line, "\n"])
maze_data_list = list(maze_data)
maze_data_list[3 * width + 3] = "S"
maze_data_list[len(maze_data_list) - (3 * width + 6)] = "E"
return "".join(maze_data_list)
def make_maze(width: int = 16, height: int = 8) -> str:
"""Generate maze with given width and height."""
vis = [[0] * width + [1] for _ in range(height)] + [[1] * (width + 1)]
ver = [["# "] * width + ["#"] for _ in range(height)] + [[]]
hor = [["###"] * width + ["#"] for _ in range(height + 1)]
def walk(x_coordinate: int, y_coordinate: int) -> None:
vis[y_coordinate][x_coordinate] = 1
neighbors = [
(x_coordinate - 1, y_coordinate),
(x_coordinate, y_coordinate + 1),
(x_coordinate + 1, y_coordinate),
(x_coordinate, y_coordinate - 1),
]
_RANDOM.shuffle(neighbors)
for x_coordinate_neighbor, y_coordinate_neighbor in neighbors:
if vis[y_coordinate_neighbor][x_coordinate_neighbor]:
continue
if x_coordinate_neighbor == x_coordinate:
hor[max(y_coordinate, y_coordinate_neighbor)][x_coordinate] = "# "
if y_coordinate_neighbor == y_coordinate:
ver[y_coordinate][max(x_coordinate, x_coordinate_neighbor)] = " "
walk(x_coordinate_neighbor, y_coordinate_neighbor)
walk(_RANDOM.randrange(width), _RANDOM.randrange(height))
return fill_generated_maze(hor, ver, width)
def print_help() -> None:
"""Print help."""
logger.info(
"python main.py - run the script against default maze file\n(any file "
"named maze.txt in the code directory)\n\npython main.py filename.txt - "
"run the script against filename.txt file\npython main.py -h --help print"
" this prompt\npython main.py -t --test non interactive (does not print "
"steps) for testing\ndifferent heuristics, goes through entire "
"generatedMazes folder and\ncompares heuristic speed and path length\n"
"python main.py -t --test [FOLDER] non interactive (does not print steps)"
" for testing\ndifferent heuristics, goes through entire [FOLDER] folder "
"and\ncompares heuristic speed and path length\n\npython main.py -g "
"--generate [NUMBER] - generates as many mazes as entered in\nNumber "
"parameter and puts it in the generatedMazes folder"
)
def test_mode() -> None:
"""Load and solves multiple mazes in order to compare heuristics."""
create_maze_folder(solved=False)
sum_of_paths = 0
files_amount = 0
sum_of_time = 0
heuristic_total_total_time = 0
all_heuristic_called = 0
for filename in [q.name for q in Path(FOLDER_NAME).iterdir()]:
filename_directory = str(Path(FOLDER_NAME) / filename)
# Open and load text file to array
loaded_maze = load_maze(filename_directory)
# Initialize MazeSolver object with maze as parameter
solver_test = MazeSolver(loaded_maze, TEST_MODE)
# Find path using MazeSolver solve method
start_time = time.perf_counter()
solved_path, visited, heuristic_total_time, heuristics_called = (
solver_test.solve()
)
heuristic_total_total_time += heuristic_total_time
all_heuristic_called += heuristics_called
end_time = time.perf_counter()
sum_of_time += end_time - start_time
sum_of_paths += len(solved_path)
save_maze(
loaded_maze, solved_path, visited, SaveOptions(filename, 0, solved=True)
)
files_amount += 1
if files_amount == 0:
logger.info("no mazes found! Generate some using python main.py -g [NUMBER]")
sys.exit()
average_path = sum_of_paths / files_amount
average_time = sum_of_time / files_amount
logger.info(
"For: %s files,\n sum of path lengths = %s,\n average path length ="
" %s,\n sum_of_time = %s,\n average time to solve: %s,\n "
"heuristic_total_total_time: %s,\n all_heuristic_called: %s,\n "
"average_heuristic_time: %s",
files_amount,
sum_of_paths,
average_path,
sum_of_time,
average_time,
heuristic_total_total_time,
all_heuristic_called,
heuristic_total_total_time / all_heuristic_called,
)
def default() -> None:
"""Run default operation - reads, solves and prints single maze from file."""
# Open and load text file to array
loaded_maze = load_maze(FILE_NAME)
# Initialize MazeSolver object with maze as parameter
solver = MazeSolver(loaded_maze, TEST_MODE)
# Find path using MazeSolver solve method
solved_path, visited, _, _ = solver.solve()
print_maze(loaded_maze, solved_path, visited)
save_maze(loaded_maze, solved_path, visited, SaveOptions(FILE_NAME, 0, solved=True))
# Ran first in the code
if __name__ == "__main__":
logging.basicConfig(format="%(message)s", level=logging.INFO)
FILE_NAME = "maze.txt"
TEST_MODE = False
FOLDER_NAME = ""
GENERATE_AMOUNT = 0
if len(sys.argv) > 1:
if sys.argv[1] == "-h" or sys.argv[1] == "--help":
print_help()
sys.exit()
if sys.argv[1] == "-t" or sys.argv[1] == "--test":
TEST_MODE = True
FILE_NAME = "maze.txt"
FOLDER_NAME = "generatedMazes"
if len(sys.argv) > ARGV_WITH_OPTION:
FOLDER_NAME = sys.argv[2]
test_mode()
sys.exit()
if sys.argv[1] in ("-g", "--generate") and len(sys.argv) > ARGV_WITH_OPTION:
GENERATE_AMOUNT = int(sys.argv[2])
for n in range(GENERATE_AMOUNT):
GENERATED_MAZE = make_maze()
save_maze(
GENERATED_MAZE,
None,
None,
SaveOptions(f"generated{n}.txt", 0, solved=False),
)
sys.exit()
FILE_NAME = sys.argv[1]
default()