File size: 6,038 Bytes
96e3e70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61557a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96e3e70
 
 
61557a0
 
96e3e70
 
 
 
f227b96
 
 
 
 
 
 
 
 
 
 
96e3e70
 
 
 
 
 
 
 
 
 
 
 
 
61557a0
 
 
 
 
 
 
 
96e3e70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61557a0
96e3e70
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
from __future__ import annotations

from typing import Iterable


def split_lines(board: str) -> list[str]:
    return board.replace("\r", "").split("\n")


def pad_lines(lines: Iterable[str], width: int) -> list[str]:
    return [line.ljust(width) for line in lines]


def join_grid(grid: list[list[str]]) -> str:
    return "\n".join("".join(row) for row in grid)


def _parse_loopy(problem_ascii: str) -> tuple[list[str], list[tuple[int, int]], list[tuple[int, int]]]:
    lines = split_lines(problem_ascii)
    rows = max(0, (len(lines) - 3) // 2)
    cols = max(0, ((len(lines[0]) if lines else 0) - 3) // 2)
    horizontal_edges: list[tuple[int, int]] = []
    vertical_edges: list[tuple[int, int]] = []

    for row in range(rows + 1):
        for col in range(cols):
            horizontal_edges.append((1 + 2 * row, 2 + 2 * col))

    for row in range(rows):
        for col in range(cols + 1):
            vertical_edges.append((2 + 2 * row, 1 + 2 * col))

    return lines, horizontal_edges, vertical_edges


def _decorated_loopy_dimensions(problem_ascii: str) -> tuple[int, int]:
    puzzle_lines, _, _ = _parse_loopy(problem_ascii)
    width = len(puzzle_lines[0]) if puzzle_lines else 0
    height = len(puzzle_lines)
    return width, height


def _is_compact_loopy_board(problem_ascii: str, board_ascii: str) -> bool:
    lines = split_lines(board_ascii)
    decorated_width, decorated_height = _decorated_loopy_dimensions(problem_ascii)
    compact_width = max(0, decorated_width - 2)
    compact_height = max(0, decorated_height - 2)
    return (
        len(lines) == compact_height
        and all(len(line) <= compact_width for line in lines)
        and compact_width > 0
        and compact_height > 0
    )


def _expand_compact_loopy_board(problem_ascii: str, board_ascii: str) -> str:
    puzzle_lines, _, _ = _parse_loopy(problem_ascii)
    decorated_width, _ = _decorated_loopy_dimensions(problem_ascii)
    compact_lines = pad_lines(split_lines(board_ascii), max(0, decorated_width - 2))
    grid = [list(line) for line in pad_lines(puzzle_lines, decorated_width)]

    for row_idx, line in enumerate(compact_lines, start=1):
        if row_idx >= len(grid) - 1:
            break
        for col_idx, char in enumerate(line, start=1):
            if col_idx >= len(grid[row_idx]) - 1:
                break
            grid[row_idx][col_idx] = char

    return join_grid(grid)


def normalize_loopy_board(problem_ascii: str, board_ascii: str) -> str:
    puzzle_lines, horizontal_edges, vertical_edges = _parse_loopy(problem_ascii)
    width = len(puzzle_lines[0]) if puzzle_lines else 0
    if _is_compact_loopy_board(problem_ascii, board_ascii):
        board_ascii = _expand_compact_loopy_board(problem_ascii, board_ascii)
    grid = [list(line) for line in pad_lines(split_lines(board_ascii), width)]
    if not grid:
        grid = [list(line) for line in pad_lines(puzzle_lines, width)]

    # A submitted board with the wrong number of rows/columns must not cause an
    # IndexError below; pad to the decorated puzzle's dimensions so edge lookups
    # always land inside the grid (a too-small/misshapen board simply stays blank
    # in the missing cells and verifies as not solved).
    height = len(puzzle_lines)
    while len(grid) < height:
        grid.append([])
    for grid_row in grid:
        if len(grid_row) < width:
            grid_row.extend(" " * (width - len(grid_row)))

    for row, col in horizontal_edges:
        current = grid[row][col]
        if current != "-":
            grid[row][col] = "x"

    for row, col in vertical_edges:
        current = grid[row][col]
        if current != "|":
            grid[row][col] = "x"

    return join_grid(grid)


def compact_loopy_board(problem_ascii: str, board_ascii: str) -> str:
    normalized = normalize_loopy_board(problem_ascii, board_ascii)
    lines = split_lines(normalized)
    if len(lines) <= 2:
        return normalized
    return "\n".join(line[1:-1] for line in lines[1:-1])


def _parse_pattern_rows(board_ascii: str) -> tuple[list[str], list[str], list[str]]:
    lines = split_lines(board_ascii)
    clue_lines: list[str] = []
    content_lines: list[str] = []
    border_lines: list[str] = []

    grid_started = False
    for line in lines:
        if "|" in line:
            grid_started = True
            content_lines.append(line)
        elif "+" in line and "-" in line:
            if grid_started:
                border_lines.append(line)
            else:
                clue_lines.append(line)
        else:
            clue_lines.append(line)

    return clue_lines, content_lines, border_lines


def normalize_pattern_board(board_ascii: str) -> str:
    clue_lines, content_lines, border_lines = _parse_pattern_rows(board_ascii)
    if not content_lines:
        return board_ascii

    normalized: list[str] = [*clue_lines]
    border_iter = iter(border_lines)

    for content in content_lines:
        pieces = content.split("|")
        if len(pieces) < 3:
            normalized.append(content)
            continue
        next_pieces = [pieces[0]]
        for cell in pieces[1:-1]:
            next_pieces.append(".." if cell == "  " else cell)
        next_pieces.append(pieces[-1])
        normalized.append("|".join(next_pieces))
        try:
            normalized.append(next(border_iter))
        except StopIteration:
            pass

    return "\n".join(normalized)


def normalize_board_for_display(*, puzzle_type: str, problem_ascii: str, board_ascii: str) -> str:
    if puzzle_type == "loopy":
        return normalize_loopy_board(problem_ascii, board_ascii)
    if puzzle_type == "pattern":
        return normalize_pattern_board(board_ascii)
    return board_ascii


def normalize_board_for_submission(*, puzzle_type: str, problem_ascii: str, board_ascii: str) -> str:
    if puzzle_type == "loopy":
        return compact_loopy_board(problem_ascii, board_ascii)
    if puzzle_type == "pattern":
        return normalize_pattern_board(board_ascii)
    return board_ascii