BBC maze

 


На Mastodon  есть полузаглохший канал BBC Microbot, на котором публикуют разные программы на BBC BASIC. Для начинающих программа покажется сложной - я уже писал об этом. Полгода у меня валялась в папке одна программа, с которой я собирался разобраться.  Прошло примерно три часа диалога с Яндексом, чтобы разобраться с принципом работы. Исходник:

https://bbcmic.ro/?t=dvf4r

10 MODE4:C=0:@%!4=8:@%!12=-8:Q%=@%+4:FORS%=0TO1023STEP8:FORR%=0TO1279STEP8:X%=R%:Y%=S%:MOVEX%,Y%

20 FORD%=C-3TOC:B%=D%*4AND12:V%=X%+@%!B%:W%=Y%+Q%!B%:IFPOINT(V%,W%)NEXT,,:VDU5,21

30 D%=HIMEM:NEXTD%:X%=V%:Y%=W%:DRAWX%,Y%:C=C+.125:GOTO20


Диалог был таким:
- разбери алгоритм программы, вот текст
- молчок
 разбил на пробелы, скинул текст, задал вопрос
- ооооо, это круто! вот алгоритм.
- картинка не похожа на оригинал, вот ссылка.
- хммм, пошарил по докам, вот вариант.
- какое значение выдаст POINT(), если координаты вышли за диапазон экран.
- упс, сейчас разберусь.

Получил исходник на Си, скомпилировал, запустил, получил картинку формата .pgm, но так и не смог посмотреть. Вот оно:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <math.h>

#define WIDTH  1280
#define HEIGHT 1024

static uint8_t screen[HEIGHT][WIDTH];

/*
 * BBC BASIC POINT(x,y):
 *   Returns -1 (blocked) for off-screen coordinates.
 *   Returns 0 (free) for on-screen empty pixels.
 *   Returns 1 (blocked) for on-screen drawn pixels.
 */
static int point(int x, int y)
{
    if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT)
        return -1;  /* off-screen = blocked */
    return screen[y][x];
}

/* Bresenham line */
static void draw_line(int x0, int y0, int x1, int y1)
{
    int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
    int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
    int err = dx + dy, e2;
    for (;;) {
        if (x0 >= 0 && x0 < WIDTH && y0 >= 0 && y0 < HEIGHT)
            screen[y0][x0] = 1;
        if (x0 == x1 && y0 == y1) break;
        e2 = 2 * err;
        if (e2 >= dy) { err += dy; x0 += sx; }
        if (e2 <= dx) { err += dx; y0 += sy; }
    }
}

/*
 * @% defaults to &90A = 2314 in BBC BASIC II.
 * Program writes 8 to addr @%+4 = 2318, -8 to addr @%+12 = 2326.
 * Addresses @%+0 = 2314, @%+8 = 2322, @%+16 = 2330 are in cleared
 * user workspace (zeroed after boot), so all read back as 0.
 *
 * Direction table (B% = 0,4,8,12):
 *   B%=0:  dx=0,  dy=8   -> UP
 *   B%=4:  dx=8,  dy=0   -> RIGHT
 *   B%=8:  dx=0,  dy=-8  -> DOWN
 *   B%=12: dx=-8, dy=0   -> LEFT
 */
static const int dx[4] = { 0,  8,  0, -8 };
static const int dy[4] = { 8,  0, -8,  0 };

static int b_from_d(int d) { return (d * 4) & 12; }

/* Truncation toward zero, matching BBC BASIC INT() */
static int trunc_int(double v) { return (int)v; }
static int floor_int(double v) { return (int)floor(v); }

int main(void)
{
    memset(screen, 0, sizeof(screen));

    double C = 0.0;
    int lines = 0;

    for (int S = 0; S < 1024; S += 8) {
        for (int R = 0; R < 1280; R += 8) {
            int X = R, Y = S;

            /* FOR D%=C-3 TO C: try 3-4 directions */
            int d_start = trunc_int(C - 3.0);
            int d_last  = floor_int(C);

            int found = 0, V = 0, W = 0;
            for (int D = d_start; D <= d_last; D++) {
                int B = b_from_d(D) / 4;
                V = X + dx[B];
                W = Y + dy[B];
                if (point(V, W) == 0) { found = 1; break; }
            }
            if (!found) continue;

            /* Draw spiral */
            while (lines < 200000) {
                d_start = trunc_int(C - 3.0);
                d_last  = floor_int(C);

                found = 0;
                for (int D = d_start; D <= d_last; D++) {
                    int B = b_from_d(D) / 4;
                    V = X + dx[B];
                    W = Y + dy[B];
                    if (point(V, W) == 0) { found = 1; break; }
                }
                if (!found) break;

                draw_line(X, Y, V, W);
                X = V; Y = W;
                lines++;
                C += 0.125;
            }
        }
    }

    long px = 0;
    for (int y = 0; y < HEIGHT; y++)
        for (int x = 0; x < WIDTH; x++)
            if (screen[y][x]) px++;

    printf("Lines: %d, Pixels: %ld\n", lines, px);

    FILE *f = fopen("spiral.pgm", "wb");
    if (f) {
        fprintf(f, "P5\n%d %d\n255\n", WIDTH, HEIGHT);
        for (int y = HEIGHT - 1; y >= 0; y--)
            fwrite(screen[y], 1, WIDTH, f);
        fclose(f);
        printf("Saved spiral.pgm\n");
    }
    return 0;
}

Переписал код на PureBasic и получил хер пойми что. Исправил опечатки и картинка готова:

Dim dx(4)
Dim dy(4)

dx(0)=0
dx(1)=8/2
dx(2)=0
dx(3)=-8/2

dy(0)=8/2
dy(1)=0
dy(2)=-8/2
dy(3)=0

Procedure b_from_d(d)
  ProcedureReturn (d * 4) & 12
EndProcedure

Procedure pt(x,y)
  If (x < 0) Or (x >= 1280) Or (y < 0) Or (y >= 1024)
    ProcedureReturn -1;  /* off-screen = blocked */
  EndIf
  ProcedureReturn Point(x,y)
EndProcedure

If OpenWindow(0, 0, 0, 1280,1024, "BBC maze", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  CanvasGadget(0, 0, 0, 1280, 1024)
  
  c.f=0
  lines=0
  If StartDrawing(CanvasOutput(0))
    Box(0,0,1280,1024,0)
    For S = 0 To 1024 Step 4
      For R = 0 To 1280 Step 4
        X = R
        Y = S
        d_start =Int(C - 3)
        d_last  =Int(C)
        found = 0
        V = 0
        W = 0;
        For D = d_start To d_last
          B = b_from_d(D) / 4;
          V = X + dx(B)
          W = Y + dy(B)
          If Pt(V, W) = 0
            found = 1
            Break 1
          EndIf
        Next D
;         If Not found
;           Continue
;         EndIf
        If Not found
          Goto cont
        EndIf
        ;/* Draw spiral */
        While lines < 200000
          d_start = Int(C - 3);
          d_last  = Int(C)    ;
          
          found = 0;
          For D = d_start To d_last
            B = b_from_d(D) / 4;
            V = X + dx(B)
            W = Y + dy(B)
            If Pt(V, W) = 0
              found = 1
              Break
            EndIf
          Next d
          If Not found
            Break;
          EndIf
          LineXY(X, Y, V, W,$FFFFFF)
          X = V
          Y = W;
          lines+1
          C +0.125;
        Wend
cont:
      Next R
    Next S
    StopDrawing()
  EndIf
  
  Repeat
    Event = WaitWindowEvent()
  Until Event = #PB_Event_CloseWindow
EndIf

Оставлю кусок лога, который хоть что-то объясняет:
Что рисует программа
Программа заполняет экран спиралевидным узором — прямоугольными спиралями, которые выходят за границы экрана и возвращаются обратно, постепенно заполняя всё доступное пространство.

Разбор по строкам
Строка 10 — инициализация
10 MODE4:C=0:@%!4=8:@%!12=-8:Q%=@%+4:
   FORS%=0TO1023STEP8:FORR%=0TO1279STEP8:
   X%=R%:Y%=S%:MOVEX%,Y%
MODE 4 — графический режим 320×256, 2 цвета. Но в BBC Micro логические графические координаты всегда 0–1279 (X) × 0–1023 (Y), независимо от режима. Начало координат — в нижнем левом углу.

C=0 — счётчик направления (вещественный, увеличивается на 0.125 за шаг).

Таблица направлений через @%: это самый хитрый приём программы. @% в BBC BASIC — это специальная целочисленная переменная для форматирования вывода, по умолчанию равна 0. Программа использует её значение (0) как базовый адрес и записывает направления прямо в нулевую страницу памяти:

@%!4=8 — записывает 32-битное значение 8 по абсолютному адресу 4
@%!12=-8 — записывает -8 по абсолютному адресу 12
Q%=@%+4=4 — указатель со сдвигом для Y-компонент
Получается таблица из четырёх направлений (по 8 пикселей), где X-компоненты читаются из @%!B%, а Y-компоненты — из Q%!B% (тот же массив, но со сдвигом на 4 байта):

B% @%!B% (dx) Q%!B% (dy) Направление
0 0 8 Вверх
4 8 0 Вправо
8 0 -8 Вниз
12 -8 0 Влево
Программа рассчитывает, что по адресам 0, 8 и 16 в нулевой странице уже лежат нули — что на BBC Micro так и есть.

Вложенные циклы: S% (Y) от 0 до 1023 с шагом 8, R% (X) от 0 до 1279 с шагом 8 — программа сканирует экран по сетке 8×8 пикселей. Для каждой точки выполняется MOVE X%,Y% — перемещение графического курсора.

Строка 20 — поиск свободного направления
basic
20 FORD%=C-3TOC:B%=D%*4AND12:V%=X%+@%!B%:W%=Y%+Q%!B%:
   IFPOINT(V%,W%)NEXT,,:VDU5,21
FOR D%=C-3 TO C — цикл по 4 значениям D%. Но по факту проверяется только первое значение (доказательство ниже).

B%=D%*4 AND 12 — отображение D% на индекс таблицы направлений {0, 4, 8, 12} с циклическим повторением каждые 4 значения. Благодаря AND 12 отрицательные значения D% тоже корректно замыкаются:

D% D%×4 AND 12 B% Направление
-3 -12 4 4 Вправо
-2 -8 8 8 Вниз
-1 -4 12 12 Влево
0 0 0 0 Вверх
1 4 4 4 Вправо
... ... ... ... (цикл)
V%=X%+@%!B% и W%=Y%+Q%!B% — координаты следующей точки: текущая позиция плюс вектор направления.

IF POINT(V%,W%) NEXT,, — ключевая проверка. POINT(V%,W%) возвращает 1, если пиксель уже нарисован, и 0, если нет:

Пиксель нарисован → выполняется NEXT,, — закрывает два внутренних цикла (D% и R%), перескакивает к следующей точке сетки. Программа сдаётся на этой позиции.
Пиксель свободен → IF ложен, весь остаток строки пропускается, выполнение переходит к строке 30.
В BBC BASIC IF без THEN делает всю оставшуюся часть строки условной. Поэтому :VDU5,21 — это мёртвый код: если POINT истинен, NEXT,, уходит на другую итерацию до того, как VDU5,21 успеет выполниться; если POINT ложен, вся строка пропускается. VDU 5,21 не выполняется ни в одном случае. 

Строка 30 — рисование и цикл
30 D%=HIMEM:NEXTD%:X%=V%:Y%=W%:DRAWX%,Y%:C=C+.125:GOTO20
D%=HIMEM — присваивает D% очень большое значение (HIMEM — верхняя граница памяти), чтобы принудительно выйти из цикла FOR D%.
NEXT D% — закрывает цикл D% (значение уже за пределами диапазона).
X%=V%:Y%=W% — обновляет текущую позицию на найденную свободную точку.
DRAW X%,Y% — рисует линию от предыдущей позиции к новой.
C=C+.125 — сдвигает счётчик направления.
GOTO 20 — возвращается к началу цикла D%, начиная новый виток рисования с обновлённой позицией и новым C.
Алгоритм в целом
Программа обходит экран по сетке 8×8 пикселей (строка за строкой).
В каждой точке пробует одно направление (определяемое счётчиком C).
Если целевая точка свободна — рисует туда линию, увеличивает C на 0.125 и продолжает рисование уже из новой позиции.
Если целевая точка занята — переходит к следующей точке сетки.
Счётчик C медленно вращает предпочтительное направление: при использовании усечения к нулю (как в BBC BASIC) получается цикл Вправо×1 → Вниз×8 → Влево×8 → Вверх×16 → Вправо×8 → .... Направление «Вверх» получает 16 шагов вместо 8 из-за асимметрии усечения отрицательных и положительных чисел к нулю.
Результат — прямоугольные спирали. Первая спираль начинается из (0,0), идёт вправо 1 шаг, потом вниз 8 шагов (за нижний край экрана), влево 8 шагов (за левый край), вверх 16 шагов (возвращаясь на экран), и так далее. Когда спираль упирается в нарисованные пиксели, программа переходит к следующей точке сетки и начинает новую спираль.

С учетом измененной программы можно разобраться в принципе работы.

Комментарии