/*
  pacmangalaksija.c
  Galaksija Z80 (real HW) - z88dk +gal


  Build:
    zcc +gal -create-app -o pacman pacmangalaksija.c
*/

typedef unsigned char u8;
typedef unsigned int  u16;

void clg(void);

/* =================== VIDEO RAM (DFILE) =================== */
#define DFILE ((volatile unsigned char*)0x2800)
#define C_COLS 32
#define C_ROWS 16

static void scr_putc(u8 cx, u8 cy, unsigned char ch)
{
    DFILE[(u16)cy * C_COLS + cx] = ch;
}

static void scr_puts(u8 cx, u8 cy, const char *s)
{
    u16 p = (u16)cy * C_COLS + cx;
    while (*s && cx < C_COLS) {
        DFILE[p++] = (unsigned char)(*s++);
        ++cx;
    }
}

static void scr_puts_bytes(u8 cx, u8 cy, const unsigned char *s)
{
    u16 p = (u16)cy * C_COLS + cx;
    while (*s && cx < C_COLS) {
        DFILE[p++] = *s++;
        ++cx;
    }
}

static void scr_clear_all(unsigned char fill)
{
    u16 i;
    for (i = 0; i < (u16)(C_COLS * C_ROWS); ++i) DFILE[i] = fill;
}

#define CH_SOLID ((unsigned char)0xFF)

/* crude delay */
static void delay_msish(u16 n)
{
    while (n--) {
        volatile u16 i;
        for (i=0; i<500; ++i) {}
    }
}

/* =================== STARTUP: BORDER + UČITAVANJE (DFILE only) =================== */
static void show_loading_first(void)
{
    scr_clear_all((unsigned char)' ');

    for (u8 x=0; x<C_COLS; ++x) {
        scr_putc(x, 0, CH_SOLID);
        scr_putc(x, (u8)(C_ROWS-1), CH_SOLID);
    }
    for (u8 y=0; y<C_ROWS; ++y) {
        scr_putc(0, y, CH_SOLID);
        scr_putc((u8)(C_COLS-1), y, CH_SOLID);
    }

    /* Galaksija: 0x5B=Č, 0x5C=Ć, 0x5D=Ž, 0x5E=Š */
    static const unsigned char UCITAVANJE[] = {
        'U', 0x5B, 'I', 'T', 'A', 'V', 'A', 'N', 'J', 'E', 0
    };
    scr_puts_bytes(10, 7, UCITAVANJE);

    delay_msish(25);
}

/* =================== FAST PIXEL PLOT (LUT) =================== */
static u16 pix_off[48][64];
static u8  pix_mask[48][64];

static void lut_init(void)
{
    u8 x, y;
    for (y = 0; y < 48; ++y) {
        for (x = 0; x < 64; ++x) {
            u16 off = 0;
            u8  a   = 1;
            u8  odd = (u8)(x & 1);
            u8  d;
            u8  yy  = (u8)(y + 1);

            while (1) {
                d = 3;
                a = 1;
                while (d--) {
                    --yy;
                    if (!yy) break;
                    a = (u8)(a << 2);
                }
                if (!yy) break;
                off = (u16)(off + 32);
            }

            off = (u16)(off + (x >> 1));
            if (odd) a = (u8)(a << 1);

            pix_off[y][x]  = off;
            pix_mask[y][x] = a;
        }
    }
}

static void pset(u8 x, u8 y)
{
    volatile unsigned char *p = (volatile unsigned char*)(0x2800u + pix_off[y][x]);
    unsigned char v = *p;
    unsigned char a = pix_mask[y][x];
    if (v & 0x80) *p = (unsigned char)(v | a);
    else *p = (unsigned char)(0xC0 | a);
}

static void pclr(u8 x, u8 y)
{
    volatile unsigned char *p = (volatile unsigned char*)(0x2800u + pix_off[y][x]);
    unsigned char v = *p;
    unsigned char a = pix_mask[y][x];
    if (!(v & 0x80)) v = 0xC0;
    v = (unsigned char)(v & (unsigned char)~a);
    *p = v;
}

/* =================== 64x48 => 16x12 tiles =================== */
#define W 16
#define H 12

static const u8 walls[H][W] = {
 {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
 {1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1},
 {1,0,1,1,0,1,0,0,0,0,1,0,1,1,0,1},
 {1,0,0,0,0,1,0,1,1,0,1,0,0,0,0,1},
 {1,0,1,0,0,0,0,1,1,0,0,0,0,1,0,1},
 {1,0,1,0,1,1,0,0,0,0,1,1,0,1,0,1},
 {1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1},
 {1,0,1,1,0,1,0,1,1,0,1,0,1,1,0,1},
 {1,0,0,1,0,0,0,0,0,0,0,0,1,0,0,1},
 {1,0,0,0,0,1,0,1,1,0,1,0,0,0,0,1},
 {1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1},
 {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
};

static u8 pellets[H][W];

/* =================== RNG + small delay =================== */
static u8 rnd = 0xA7;
static u8 tick = 0;

static u8 rnd8(void)
{
    rnd ^= (u8)(rnd << 3);
    rnd ^= (u8)(rnd >> 5);
    rnd ^= (u8)(rnd << 1);
    return rnd;
}

static void delay_small(void)
{
    volatile u16 i;
    for (i = 0; i < 40; ++i) {}
}

/* =================== SPEED CONTROL =================== */
#define PAC_DIV 4

/* Base ghost speed divider (smaller = faster). We will start at 4x slower. */
#define GHOST_DIV_BASE 12
#define GHOST_DIV_MIN  6   /* don’t go faster than this */
static u8 ghost_div;        /* current divider (starts at BASE*4 then decreases by level) */

/* =================== tile drawing =================== */
static void tile_fill4(u8 tx, u8 ty)
{
    u8 x0 = (u8)(tx << 2), y0 = (u8)(ty << 2);
    for (u8 dy=0; dy<4; ++dy)
        for (u8 dx=0; dx<4; ++dx)
            pset((u8)(x0+dx), (u8)(y0+dy));
}

static void tile_clear4(u8 tx, u8 ty)
{
    u8 x0 = (u8)(tx << 2), y0 = (u8)(ty << 2);
    for (u8 dy=0; dy<4; ++dy)
        for (u8 dx=0; dx<4; ++dx)
            pclr((u8)(x0+dx), (u8)(y0+dy));
}

static void tile_pellet(u8 tx, u8 ty)
{
    pset((u8)((tx<<2)+2), (u8)((ty<<2)+2));
}

static void redraw_background_tile(u8 tx, u8 ty)
{
    if (walls[ty][tx]) tile_fill4(tx,ty);
    else {
        tile_clear4(tx,ty);
        if (pellets[ty][tx]) tile_pellet(tx,ty);
    }
}

/* =================== actors =================== */
static void pac_draw(u8 tx, u8 ty)
{
    u8 x = (u8)((tx<<2)+1);
    u8 y = (u8)((ty<<2)+1);
    pset(x,y); pset((u8)(x+1),y);
    pset(x,(u8)(y+1)); pset((u8)(x+1),(u8)(y+1));
}

/* ghost: 2x2 with top-left missing */
static void ghost_draw(u8 tx, u8 ty)
{
    u8 x = (u8)((tx<<2)+1);
    u8 y = (u8)((ty<<2)+1);
    pset((u8)(x+1),y);
    pset(x,(u8)(y+1));
    pset((u8)(x+1),(u8)(y+1));
}

static void move_actor_incremental(u8 oldx, u8 oldy, u8 newx, u8 newy, u8 is_ghost)
{
    if (oldx == newx && oldy == newy) return;
    redraw_background_tile(oldx, oldy);
    if (is_ghost) ghost_draw(newx, newy);
    else pac_draw(newx, newy);
}

/* =================== LIFE LOST ANIMATION =================== */
static void life_lost_anim(u8 tx, u8 ty)
{
    for (u8 i=0; i<6; ++i) {
        if (i & 1) {
            tile_fill4(tx, ty);
        } else {
            redraw_background_tile(tx, ty);
            pac_draw(tx, ty);
        }
        delay_msish(6);
    }
    redraw_background_tile(tx, ty);
}

/* =================== HUD (ASCII) =================== */
static u16 score = 0;
static u8  lives = 3;
static u16 last_score = 65535u;
static u8  last_lives = 255u;

#define HUD_BG ((unsigned char)0xFF)

static void hud_update_if_changed(void)
{
    if (score == last_score && lives == last_lives) return;

    u16 s = score;
    if (s > 9999u) s = 9999u;

    u8 d3 = (u8)(s % 10u); s /= 10u;
    u8 d2 = (u8)(s % 10u); s /= 10u;
    u8 d1 = (u8)(s % 10u); s /= 10u;
    u8 d0 = (u8)(s % 10u);

    scr_putc(2,0,(unsigned char)('0'+d0));
    scr_putc(3,0,(unsigned char)('0'+d1));
    scr_putc(4,0,(unsigned char)('0'+d2));
    scr_putc(5,0,(unsigned char)('0'+d3));

    scr_putc(8,0,(lives>=1)?(unsigned char)'*':HUD_BG);
    scr_putc(9,0,(lives>=2)?(unsigned char)'*':HUD_BG);
    scr_putc(10,0,(lives>=3)?(unsigned char)'*':HUD_BG);

    last_score = score;
    last_lives = lives;
}

/* =================== keyboard matrix (HW stable) =================== */
#define KBD_BASE ((volatile unsigned char*)0x2000)
#define KBD_SIZE 0x0800

static void delay_key(void)
{
    volatile u16 i;
    for (i = 0; i < 300; ++i) {}
}

static u16 kbd_scan_pressed_offset(void)
{
    for (u16 i=0; i<KBD_SIZE; ++i) {
        unsigned char v = KBD_BASE[i];
        if ((v & 1) == 0) return i;
    }
    return 0xFFFF;
}

static u16 kbd_wait_key(void)
{
    u16 off;
    do { off = kbd_scan_pressed_offset(); } while (off == 0xFFFF);
    delay_key();
    while (kbd_scan_pressed_offset() != 0xFFFF) {}
    delay_key();
    return off;
}

static u8 kbd_is_pressed(u16 off)
{
    return ((KBD_BASE[off] & 1) == 0) ? 1 : 0;
}

static u16 K_UP=0xFFFF, K_DOWN=0xFFFF, K_LEFT=0xFFFF, K_RIGHT=0xFFFF, K_QUIT=0xFFFF;
static u16 K_ENTER=0xFFFF;

static void show_text_only(const char *line1, const char *line2)
{
    scr_clear_all((unsigned char)' ');
    if (line1) scr_puts(1, 7, line1);
    if (line2) scr_puts(1, 8, line2);
}

static void calibrate_keys(void)
{
    show_text_only("PRITISNITE TASTER ZA LEVO", 0);
    K_LEFT = kbd_wait_key();

    show_text_only("PRITISNITE TASTER ZA DESNO", 0);
    K_RIGHT = kbd_wait_key();

    show_text_only("PRITISNITE TASTER ZA GORE", 0);
    K_UP = kbd_wait_key();

    show_text_only("PRITISNITE TASTER ZA DOLE", 0);
    K_DOWN = kbd_wait_key();

    show_text_only("PRITISNITE TASTER ZA IZLAZ", 0);
    K_QUIT = kbd_wait_key();

    show_text_only("PRITISNITE ENTER", "ZA NOVU IGRU");
    K_ENTER = kbd_wait_key();
}

static u8 read_dir_hw(void)
{
    if (kbd_is_pressed(K_UP))    return 1;
    if (kbd_is_pressed(K_DOWN))  return 2;
    if (kbd_is_pressed(K_LEFT))  return 3;
    if (kbd_is_pressed(K_RIGHT)) return 4;
    return 0;
}

/* =================== movement/game =================== */
static u8 px,py,g1x,g1y,g2x,g2y;
static u8 dir = 4;
static u8 grace = 90;

static u8 is_wall(u8 x, u8 y)
{
    if (x>=W || y>=H) return 1;
    return walls[y][x];
}

static void move_try(u8 *x, u8 *y, u8 d)
{
    u8 nx=*x, ny=*y;
    if (d==1) { if (ny) --ny; }
    else if (d==2) { ++ny; }
    else if (d==3) { if (nx) --nx; }
    else if (d==4) { ++nx; }
    if (!is_wall(nx,ny)) { *x=nx; *y=ny; }
}

static void ghost_step(u8 *gx, u8 *gy)
{
    u8 d1=0, d2=0;

    if (*gx < px) d1=4; else if (*gx > px) d1=3;
    if (*gy < py) d2=2; else if (*gy > py) d2=1;

    if (d1) { u8 ox=*gx, oy=*gy; move_try(gx,gy,d1); if (*gx!=ox || *gy!=oy) return; }
    if (d2) { u8 ox=*gx, oy=*gy; move_try(gx,gy,d2); if (*gx!=ox || *gy!=oy) return; }

    for (u8 t=0; t<3; ++t) {
        u8 d3 = (u8)((rnd8() & 3) + 1);
        u8 ox=*gx, oy=*gy;
        move_try(gx,gy,d3);
        if (*gx!=ox || *gy!=oy) return;
    }
}

/* pellets setup for NEW MAZE */
static void reset_pellets(void)
{
    for (u8 y=0;y<H;y++)
        for (u8 x=0;x<W;x++)
            pellets[y][x] = walls[y][x] ? 0 : 1;

    pellets[6][1]   = 0;
    pellets[10][14] = 0;
    pellets[10][1]  = 0;
}

static void draw_maze_once(void)
{
    clg();
    for (u8 y=0;y<H;y++)
        for (u8 x=0;x<W;x++)
            redraw_background_tile(x,y);

    hud_update_if_changed();
}

static void respawn_positions(void)
{
    px=1;  py=6;
    g1x=14; g1y=10;
    g2x=1;  g2y=10;
    dir=4;
    grace=90;
}

/* check if any pellet remains */
static u8 pellets_remaining(void)
{
    for (u8 y=0; y<H; ++y)
        for (u8 x=0; x<W; ++x)
            if (pellets[y][x]) return 1;
    return 0;
}

/* Game over screen, wait ENTER */
static void game_over_wait_enter(void)
{
    show_text_only("KRAJ. ENTER ZA NOVU IGRU.", 0);
    while (!kbd_is_pressed(K_ENTER)) {}
    delay_key();
    while (kbd_is_pressed(K_ENTER)) {}
    delay_key();
}

/* Show BRAVO briefly (text-only) */
static void show_bravo(void)
{
    show_text_only("BRAVO", 0);
    delay_msish(25);
}

/* Start a fresh maze but KEEP score/lives; increase ghost speed slightly */
static void next_level(void)
{
    /* speed up a bit: decrease divider by 1 each level, down to min */
    if (ghost_div > GHOST_DIV_MIN) ghost_div--;

    reset_pellets();
    draw_maze_once();
    respawn_positions();

    pac_draw(px,py);
    ghost_draw(g1x,g1y);
    ghost_draw(g2x,g2y);

    hud_update_if_changed();
}

/* Full new game from scratch */
static void new_game(void)
{
    score = 0;
    lives = 3;
    last_score = 65535u;
    last_lives = 255u;

    /* 4x slower initially */
    ghost_div = (u8)(GHOST_DIV_BASE * 4);

    reset_pellets();
    draw_maze_once();
    respawn_positions();

    pac_draw(px,py);
    ghost_draw(g1x,g1y);
    ghost_draw(g2x,g2y);

    hud_update_if_changed();
}

int main(void)
{
    /* FIRST: border + UČITAVANJE */
    show_loading_first();

    /* init LUT + keyboard mapping */
    lut_init();
    calibrate_keys();

    /* start game */
    new_game();

    u8 pac_ctr = 0;
    u8 ghost_ctr = 0;

    while (1) {
        if (kbd_is_pressed(K_QUIT)) { clg(); for(;;){} }

        u8 nd = read_dir_hw();
        if (nd) dir = nd;

        u8 opx=px, opy=py, og1x=g1x, og1y=g1y, og2x=g2x, og2y=g2y;

        /* move pac slower */
        ++pac_ctr;
        if (pac_ctr >= PAC_DIV) {
            pac_ctr = 0;
            move_try(&px,&py,dir);

            if (!walls[py][px] && pellets[py][px]) {
                pellets[py][px] = 0;
                score = (u16)(score + 1u);
                redraw_background_tile(px, py);
                hud_update_if_changed();
            }
        }

        /* move ghosts using current divider (starts 4x slower) */
        ++tick;
        ++ghost_ctr;
        if (ghost_ctr >= ghost_div) {
            ghost_ctr = 0;
            if ((rnd8() & 3) != 0) {
                ghost_step(&g1x,&g1y);
                ghost_step(&g2x,&g2y);
            }
        }

        /* WIN: all dots eaten */
        if (!pellets_remaining()) {
            show_bravo();
            next_level();
            pac_ctr = 0; ghost_ctr = 0;
            continue;
        }

        /* collision */
        if (grace) --grace;
        else if ((px==g1x && py==g1y) || (px==g2x && py==g2y)) {

            life_lost_anim(px, py);

            if (lives) --lives;
            hud_update_if_changed();

            if (lives == 0) {
                game_over_wait_enter();
                new_game();             /* restart from scratch (ghosts slow again) */
                pac_ctr = 0; ghost_ctr = 0;
                continue;
            } else {
                /* keep pellets as-is, only respawn actors */
                redraw_background_tile(opx,opy);
                redraw_background_tile(og1x,og1y);
                redraw_background_tile(og2x,og2y);

                respawn_positions();
                pac_draw(px,py);
                ghost_draw(g1x,g1y);
                ghost_draw(g2x,g2y);

                pac_ctr = 0; ghost_ctr = 0;
                delay_small();
                continue;
            }
        }

        /* incremental redraw only when moved */
        move_actor_incremental(opx,opy,px,py,0);
        move_actor_incremental(og1x,og1y,g1x,g1y,1);
        move_actor_incremental(og2x,og2y,g2x,g2y,1);

        delay_small();
    }

    return 0;
}
