import {
  BIT,
  DC,
  DR,
  Dir,
  E,
  N,
  OPP,
  PieceKind,
  Playable,
  S,
  W,
  axisOf,
  canEnter,
  exitDir,
  isOneway,
  isReservoir,
  openings,
  pickPiece,
  segmentCost,
  wrapIndex,
  type PieceWeights,
} from "./pieces";

export type { PieceWeights };
import { mulberry32 } from "./rng";

export type Mode = "campaign" | "endless" | "daily" | "bonus";
export type Phase =
  | "countdown"
  | "flowing"
  | "won"
  | "lost"
  | "results";

export interface Cell {
  piece: PieceKind | null;
  port: Dir;
  locked: boolean;
  bonus: boolean;
  /** 0 = NS/primary, 1 = EW/secondary (cross). 0 empty, (0,1] filling/filled. */
  fill: [number, number];
  from: [Dir | null, Dir | null];
  tank: number;
}

export interface FlowHead {
  r: number;
  c: number;
  enter: Dir;
  progress: number;
  axis: 0 | 1;
}

export interface Floater {
  r: number;
  c: number;
  text: string;
  kind: "score" | "loop" | "cross" | "warn" | "bonus";
  age: number;
}

export interface GameEvent {
  type:
    | "place"
    | "replace"
    | "dump"
    | "release"
    | "enter"
    | "loop"
    | "cross"
    | "reservoir"
    | "spill"
    | "win"
    | "lose"
    | "slide"
    | "tick"
    | "warn";
  r?: number;
  c?: number;
  dir?: Dir;
  text?: string;
  n?: number;
}

export interface LevelSpec {
  id: number;
  name: string;
  subtitle: string;
  password?: string;
  cols: number;
  rows: number;
  min: number;
  delay: number;
  speed: number;
  wrap: boolean;
  start: { r: number; c: number; out: Dir };
  end?: { r: number; c: number; inn: Dir };
  weights: PieceWeights;
  map?: string[];
  bonusSlide?: boolean;
  endless?: boolean;
}

export function makeCell(): Cell {
  return {
    piece: null,
    port: E,
    locked: false,
    bonus: false,
    fill: [0, 0],
    from: [null, null],
    tank: 0,
  };
}

function key(r: number, c: number): string {
  return r + "," + c;
}

export class Game {
  spec: LevelSpec;
  mode: Mode;
  cols: number;
  rows: number;
  board: Cell[][];
  queue: Playable[];
  phase: Phase = "countdown";
  delayLeft: number;
  speed: number;
  score = 0;
  combo = 0;
  distance = 0;
  min: number;
  crossings = 0;
  loops = 0;
  unusedPenalty = 0;
  extraBonus = 0;
  reservoirs = 0;
  replaced = 0;
  dumped = 0;
  earlyRelease = false;
  earlyBonus = 0;
  replaceLock = 0;
  flow: FlowHead | null = null;
  path: { r: number; c: number }[] = [];
  events: GameEvent[] = [];
  floaters: Floater[] = [];
  wrap: boolean;
  hole: { r: number; c: number } | null = null;
  scoreMult: number;
  rand: () => number;
  wonBy: "spill" | "drain" | "none" = "none";
  spillAt: { r: number; c: number; dir: Dir } | null = null;
  time = 0;
  flowingTime = 0;
  perfect = false;
  endlessLevel = 1;
  pipesPlaced = 0;

  constructor(
    spec: LevelSpec,
    mode: Mode,
    rand: () => number,
    scoreMult = 1,
  ) {
    this.spec = spec;
    this.mode = mode;
    this.cols = spec.cols;
    this.rows = spec.rows;
    this.wrap = spec.wrap;
    this.min = spec.min;
    this.delayLeft = spec.delay;
    this.speed = spec.speed;
    this.scoreMult = scoreMult;
    this.rand = rand;
    this.board = Array.from({ length: spec.rows }, () =>
      Array.from({ length: spec.cols }, () => makeCell()),
    );
    this.queue = [];
    this.applyMap();
    if (spec.bonusSlide) {
      this.buildSlide();
      this.phase = "countdown";
    } else {
      for (let i = 0; i < 5; i++) this.queue.push(this.nextPiece());
    }
  }

  emit(e: GameEvent): void {
    this.events.push(e);
  }

  popEvents(): GameEvent[] {
    const e = this.events;
    this.events = [];
    return e;
  }

  private applyMap(): void {
    const { start, end, map } = this.spec;
    const sc = this.cell(start.r, start.c);
    sc.piece = "start";
    sc.port = start.out;
    sc.locked = true;

    if (end) {
      const ec = this.cell(end.r, end.c);
      ec.piece = "end";
      ec.port = end.inn;
      ec.locked = true;
    }

    if (!map) return;
    for (let r = 0; r < this.rows; r++) {
      const row = map[r] ?? "";
      for (let c = 0; c < this.cols; c++) {
        const ch = row[c] ?? ".";
        const cell = this.cell(r, c);
        if (cell.piece === "start" || cell.piece === "end") continue;
        switch (ch) {
          case "#":
            cell.piece = "block";
            cell.locked = true;
            break;
          case "H":
            cell.piece = "h";
            cell.locked = true;
            break;
          case "V":
            cell.piece = "v";
            cell.locked = true;
            break;
          case "A":
            cell.piece = "ne";
            cell.locked = true;
            break;
          case "B":
            cell.piece = "se";
            cell.locked = true;
            break;
          case "C":
            cell.piece = "sw";
            cell.locked = true;
            break;
          case "D":
            cell.piece = "nw";
            cell.locked = true;
            break;
          case "X":
            cell.piece = "cross";
            cell.locked = true;
            break;
          case "R":
            cell.piece = "res_h";
            cell.locked = true;
            break;
          case "r":
            cell.piece = "res_v";
            cell.locked = true;
            break;
          case "*":
            cell.piece = this.rand() < 0.5 ? "h" : "v";
            cell.locked = true;
            cell.bonus = true;
            break;
          case "+":
            cell.piece = "cross";
            cell.locked = true;
            cell.bonus = true;
            break;
          case "g":
            cell.piece = pickPiece(this.rand, {
              straight: 20,
              elbow: 60,
              cross: 20,
              reservoir: 0,
              oneway: 0,
            });
            cell.bonus = true;
            cell.locked = true;
            break;
          default:
            break;
        }
      }
    }
  }

  private buildSlide(): void {
    const { cols, rows, start } = this.spec;
    const path: { r: number; c: number }[] = [];
    let r = start.r;
    let c = start.c;
    let dir: Dir = start.out;
    path.push({ r, c });
    const visited = new Set<string>([key(r, c)]);
    const target = Math.min(cols * rows - 2, this.min + 8);

    for (let i = 0; i < target; i++) {
      const nr = r + DR[dir];
      const nc = c + DC[dir];
      if (nr < 0 || nc < 0 || nr >= rows || nc >= cols) break;
      if (visited.has(key(nr, nc))) {
        const turns: Dir[] = [((dir + 1) % 4) as Dir, ((dir + 3) % 4) as Dir];
        let turned = false;
        for (const t of turns) {
          const tr = r + DR[t];
          const tc = c + DC[t];
          if (
            tr >= 0 &&
            tc >= 0 &&
            tr < rows &&
            tc < cols &&
            !visited.has(key(tr, tc))
          ) {
            this.forceConnect(r, c, dir, t);
            dir = t;
            r = tr;
            c = tc;
            visited.add(key(r, c));
            path.push({ r, c });
            turned = true;
            break;
          }
        }
        if (!turned) break;
        continue;
      }
      r = nr;
      c = nc;
      visited.add(key(r, c));
      path.push({ r, c });
    }

    for (let i = 1; i < path.length; i++) {
      const a = path[i - 1]!;
      const b = path[i]!;
      const d = this.dirBetween(a, b);
      if (i === 1) {
        /* start already set */
      }
      const prev = i >= 2 ? this.dirBetween(path[i - 1]!, path[i]!) : d;
      void prev;
      const enter = OPP[d];
      let leave: Dir = d;
      if (i + 1 < path.length) {
        leave = this.dirBetween(b, path[i + 1]!);
      } else {
        leave = d;
      }
      this.cell(b.r, b.c).piece = this.pieceFrom(enter, leave);
      this.cell(b.r, b.c).locked = false;
    }

    for (let rr = 0; rr < rows; rr++) {
      for (let cc = 0; cc < cols; cc++) {
        const cell = this.cell(rr, cc);
        if (!cell.piece) {
          cell.piece = pickPiece(this.rand, {
            straight: 25,
            elbow: 55,
            cross: 20,
            reservoir: 0,
            oneway: 0,
          });
          cell.locked = false;
        }
      }
    }

    const last = path[path.length - 1]!;
    this.hole = { r: last.r, c: last.c };
    this.cell(last.r, last.c).piece = null;

    const holeside = this.hole;
    for (let i = 0; i < 70; i++) {
      const neigh: { r: number; c: number }[] = [];
      for (let d = 0; d < 4; d++) {
        const nr = holeside.r + DR[d];
        const nc = holeside.c + DC[d];
        if (nr >= 0 && nc >= 0 && nr < rows && nc < cols) {
          const cell = this.cell(nr, nc);
          if (cell.piece && cell.piece !== "start" && cell.piece !== "end") {
            neigh.push({ r: nr, c: nc });
          }
        }
      }
      if (!neigh.length) break;
      const pick = neigh[Math.floor(this.rand() * neigh.length)]!;
      this.slideSwap(pick.r, pick.c);
    }
  }

  private dirBetween(
    a: { r: number; c: number },
    b: { r: number; c: number },
  ): Dir {
    if (b.r < a.r) return N;
    if (b.r > a.r) return S;
    if (b.c > a.c) return E;
    return W;
  }

  private pieceFrom(enter: Dir, leave: Dir): Playable {
    const mask = BIT[enter] | BIT[leave];
    if (mask === (BIT[E] | BIT[W])) return "h";
    if (mask === (BIT[N] | BIT[S])) return "v";
    if (mask === (BIT[N] | BIT[E])) return "ne";
    if (mask === (BIT[S] | BIT[E])) return "se";
    if (mask === (BIT[S] | BIT[W])) return "sw";
    if (mask === (BIT[N] | BIT[W])) return "nw";
    return "cross";
  }

  private forceConnect(r: number, c: number, from: Dir, to: Dir): void {
    const cell = this.cell(r, c);
    if (cell.piece === "start") return;
    cell.piece = this.pieceFrom(OPP[from], to);
  }

  cell(r: number, c: number): Cell {
    return this.board[r]![c]!;
  }

  inBounds(r: number, c: number): boolean {
    return r >= 0 && c >= 0 && r < this.rows && c < this.cols;
  }

  neighbor(r: number, c: number, d: Dir): { r: number; c: number } | null {
    let nr = r + DR[d];
    let nc = c + DC[d];
    if (this.wrap) {
      nr = wrapIndex(nr, this.rows);
      nc = wrapIndex(nc, this.cols);
      return { r: nr, c: nc };
    }
    if (!this.inBounds(nr, nc)) return null;
    return { r: nr, c: nc };
  }

  nextPiece(): Playable {
    let w = { ...this.spec.weights };
    if (this.mode === "endless") {
      if (this.endlessLevel >= 3) w.cross = Math.max(w.cross, 16);
      if (this.endlessLevel >= 5) w.reservoir = Math.max(w.reservoir, 8);
      if (this.endlessLevel >= 8) w.oneway = Math.max(w.oneway, 8);
    }
    return pickPiece(this.rand, w);
  }

  get next(): Playable | null {
    return this.queue[0] ?? null;
  }

  private addScore(n: number, text: string, r?: number, c?: number, kind: Floater["kind"] = "score"): void {
    const v = Math.round(n * this.scoreMult);
    this.score = Math.max(0, this.score + v);
    if (r !== undefined && c !== undefined) {
      this.floaters.push({ r, c, text: (v >= 0 ? "+" : "") + v, kind, age: 0 });
      if (text && kind !== "score") {
        this.floaters.push({ r, c, text, kind, age: 0 });
      }
    }
  }

  place(r: number, c: number): boolean {
    if (this.phase === "won" || this.phase === "lost" || this.phase === "results")
      return false;
    if (this.spec.bonusSlide) return this.trySlide(r, c);
    if (this.replaceLock > 0) return false;
    if (!this.inBounds(r, c)) return false;
    const cell = this.cell(r, c);
    if (!this.queue.length) return false;
    if (cell.locked) return false;
    if (cell.piece === "start" || cell.piece === "end" || cell.piece === "block")
      return false;
    if (cell.fill[0] > 0 || cell.fill[1] > 0) return false;

    const piece = this.queue.shift()!;
    this.queue.push(this.nextPiece());
    this.pipesPlaced++;

    if (cell.piece) {
      cell.piece = piece;
      cell.bonus = false;
      this.replaced++;
      this.replaceLock = 0.38;
      this.addScore(-50, "REPLACE", r, c);
      this.emit({ type: "replace", r, c });
      return true;
    }
    cell.piece = piece;
    this.emit({ type: "place", r, c });
    return true;
  }

  dump(): boolean {
    if (this.spec.bonusSlide) return false;
    if (this.phase === "won" || this.phase === "lost" || this.phase === "results")
      return false;
    if (!this.queue.length) return false;
    this.queue.shift();
    this.queue.push(this.nextPiece());
    this.dumped++;
    this.addScore(-20, "DUMP");
    this.emit({ type: "dump" });
    return true;
  }

  release(): boolean {
    if (this.phase !== "countdown") return false;
    this.earlyRelease = true;
    this.earlyBonus = Math.round(this.delayLeft * 18);
    this.beginFlow();
    this.emit({ type: "release" });
    return true;
  }

  private trySlide(r: number, c: number): boolean {
    if (!this.hole) return false;
    if (this.phase === "won" || this.phase === "lost") return false;
    const cell = this.cell(r, c);
    if (!cell.piece || cell.piece === "start" || cell.piece === "end") return false;
    if (cell.fill[0] > 0 || cell.fill[1] > 0) return false;
    const dist = Math.abs(r - this.hole.r) + Math.abs(c - this.hole.c);
    if (dist !== 1) return false;
    this.slideSwap(r, c);
    this.emit({ type: "slide", r, c });
    return true;
  }

  private slideSwap(r: number, c: number): void {
    if (!this.hole) return;
    const a = this.cell(r, c);
    const b = this.cell(this.hole.r, this.hole.c);
    const tmp = { ...a };
    a.piece = b.piece;
    a.port = b.port;
    a.locked = b.locked;
    a.bonus = b.bonus;
    a.fill = [0, 0];
    a.from = [null, null];
    a.tank = 0;
    b.piece = tmp.piece;
    b.port = tmp.port;
    b.locked = tmp.locked;
    b.bonus = tmp.bonus;
    b.fill = [0, 0];
    b.from = [null, null];
    b.tank = 0;
    this.hole = { r, c };
  }

  beginFlow(): void {
    if (this.phase !== "countdown") return;
    this.phase = "flowing";
    const { r, c, out } = this.spec.start;
    const cell = this.cell(r, c);
    const ax = axisOf(out);
    cell.fill[ax] = 0.001;
    cell.from[ax] = OPP[out];
    this.flow = { r, c, enter: OPP[out], progress: 0, axis: ax };
    this.path = [{ r, c }];
    this.delayLeft = 0;
  }

  tick(dt: number): void {
    this.time += dt;
    for (const f of this.floaters) f.age += dt;
    this.floaters = this.floaters.filter((f) => f.age < 1.15);
    this.replaceLock = Math.max(0, this.replaceLock - dt);

    if (this.phase === "countdown") {
      this.delayLeft -= dt;
      if (this.delayLeft <= 0) this.beginFlow();
      return;
    }
    if (this.phase !== "flowing" || !this.flow) return;
    this.flowingTime += dt;

    let budget = dt * this.speed;
    let guard = 0;
    while (budget > 0 && this.phase === "flowing" && this.flow && guard++ < 12) {
      const head = this.flow;
      const cell = this.cell(head.r, head.c);
      const kind = cell.piece!;
      const cost = segmentCost(kind);
      const remain = (1 - head.progress) * cost;
      if (budget < remain) {
        head.progress += budget / cost;
        cell.fill[head.axis] = head.progress;
        if (isReservoir(kind)) cell.tank = Math.min(1, head.progress);
        budget = 0;
      } else {
        budget -= remain;
        head.progress = 1;
        cell.fill[head.axis] = 1;
        if (isReservoir(kind)) {
          cell.tank = 1;
          this.reservoirs++;
          this.addScore(300, "CISTERN", head.r, head.c, "bonus");
          this.emit({ type: "reservoir", r: head.r, c: head.c });
        }
        this.advance();
      }
    }
  }

  private advance(): void {
    const head = this.flow!;
    const cell = this.cell(head.r, head.c);
    const kind = cell.piece!;

    if (kind === "end") {
      this.reachEnd(head.r, head.c);
      return;
    }

    const leave = exitDir(kind, head.enter, cell.port);
    if (leave === null) {
      this.spill(head.r, head.c, head.enter);
      return;
    }

    this.distance += 1;
    const base = 50 + Math.min(40, this.combo * 4);
    this.combo++;
    this.addScore(base, "", head.r, head.c);
    if (cell.bonus) {
      this.addScore(400, "GOLD", head.r, head.c, "bonus");
    }

    if (kind === "cross") {
      const other = head.axis === 0 ? 1 : 0;
      if (cell.fill[other] >= 1) {
        this.crossings++;
        this.addScore(500, "CROSS", head.r, head.c, "cross");
        this.emit({ type: "cross", r: head.r, c: head.c, n: this.crossings });
        if (this.crossings === 5) {
          this.addScore(5000, "FIVE-CROSS", head.r, head.c, "bonus");
        }
      }
    }

    const already = this.path.findIndex((p) => p.r === head.r && p.c === head.c);
    if (already >= 0 && already < this.path.length - 1) {
      this.loops++;
      const pts = 300 + (this.loops - 1) * 200;
      this.addScore(pts, "LOOP", head.r, head.c, "loop");
      this.emit({ type: "loop", r: head.r, c: head.c, n: this.loops });
    } else {
      this.path.push({ r: head.r, c: head.c });
    }

    this.emit({ type: "enter", r: head.r, c: head.c, dir: leave });

    const next = this.neighbor(head.r, head.c, leave);
    if (!next) {
      this.spill(head.r, head.c, leave);
      return;
    }
    const nc = this.cell(next.r, next.c);
    if (!nc.piece) {
      this.spill(head.r, head.c, leave);
      return;
    }
    const enter = OPP[leave];
    if (!canEnter(nc.piece, enter, nc.port)) {
      this.spill(head.r, head.c, leave);
      return;
    }
    const ax = nc.piece === "cross" ? axisOf(enter) : 0;
    if (nc.fill[ax] > 0 && nc.piece !== "cross") {
      this.spill(head.r, head.c, leave);
      return;
    }
    if (nc.piece === "cross" && nc.fill[ax] > 0) {
      this.spill(head.r, head.c, leave);
      return;
    }
    nc.fill[ax] = 0.001;
    nc.from[ax] = enter;
    this.flow = { r: next.r, c: next.c, enter, progress: 0, axis: ax };
  }

  private unusedCount(): number {
    let n = 0;
    for (let r = 0; r < this.rows; r++) {
      for (let c = 0; c < this.cols; c++) {
        const cell = this.cell(r, c);
        if (
          cell.piece &&
          cell.piece !== "block" &&
          cell.piece !== "start" &&
          cell.piece !== "end" &&
          cell.fill[0] <= 0 &&
          cell.fill[1] <= 0
        ) {
          n++;
        }
      }
    }
    return n;
  }

  private finishScoring(): void {
    const extra = Math.max(0, this.distance - this.min);
    this.extraBonus = extra * 100;
    this.addScore(this.extraBonus, "OVERTIME");
    const unused = this.unusedCount();
    this.unusedPenalty = unused * 50;
    this.addScore(-this.unusedPenalty, "UNUSED");
    if (this.earlyRelease) this.addScore(this.earlyBonus, "FAST VALVE");
    if (unused === 0 && this.pipesPlaced > 0) {
      this.perfect = true;
      this.addScore(1000, "CLEAN RIG");
    }
    if (this.mode === "endless") {
      /* no end bonus besides distance */
    }
  }

  private spill(r: number, c: number, dir: Dir): void {
    this.spillAt = { r, c, dir };
    this.emit({ type: "spill", r, c, dir });
    if (this.spec.end && this.wonBy !== "drain") {
      this.lose();
      return;
    }
    if (this.distance >= this.min) this.win("spill");
    else this.lose();
  }

  private reachEnd(r: number, c: number): void {
    this.distance += 1;
    this.addScore(50, "", r, c);
    if (this.distance >= this.min) this.win("drain");
    else this.lose();
  }

  private win(how: "spill" | "drain"): void {
    this.wonBy = how;
    this.flow = null;
    if (this.mode === "endless") {
      const extra = Math.max(0, this.distance - this.min);
      this.addScore(extra * 80 + 250, "WAVE");
      this.emit({ type: "win" });
      this.endlessAdvance();
      return;
    }
    this.finishScoring();
    this.phase = "won";
    this.emit({ type: "win" });
  }

  private lose(): void {
    this.finishScoring();
    this.phase = "lost";
    this.flow = null;
    this.emit({ type: "lose" });
  }

  private endlessAdvance(): void {
    this.endlessLevel++;
    this.min += 3;
    this.speed = Math.min(1.6, this.speed + 0.08);
    this.phase = "countdown";
    this.delayLeft = Math.max(3.2, 10 - this.endlessLevel * 0.35);
    this.combo = 0;
    this.wonBy = "none";
    this.spillAt = null;
    const { r, c, out } = this.spec.start;
    for (let rr = 0; rr < this.rows; rr++) {
      for (let cc = 0; cc < this.cols; cc++) {
        const cell = this.cell(rr, cc);
        if (cell.piece === "start") {
          cell.fill = [0, 0];
          continue;
        }
        if (cell.fill[0] >= 1 || cell.fill[1] >= 1) {
          cell.locked = true;
        }
      }
    }
    this.cell(r, c).fill = [0, 0];
    this.path = [];
    this.distance = 0;
    this.crossings = 0;
    this.loops = 0;
    void c;
    void out;
  }

  /** Predicted remaining sealed cells before an open end. */
  forecast(): { cells: { r: number; c: number }[]; remaining: number; danger: boolean } {
    const cells: { r: number; c: number }[] = [];
    let r = this.spec.start.r;
    let c = this.spec.start.c;
    let dir: Dir = this.spec.start.out;
    let enter: Dir = OPP[dir];
    if (this.flow) {
      r = this.flow.r;
      c = this.flow.c;
      const cell = this.cell(r, c);
      const leave = exitDir(cell.piece ?? "block", this.flow.enter, cell.port);
      if (leave === null) return { cells, remaining: 0, danger: true };
      const n = this.neighbor(r, c, leave);
      if (!n) return { cells, remaining: 0, danger: true };
      r = n.r;
      c = n.c;
      enter = OPP[leave];
    } else {
      const n = this.neighbor(r, c, dir);
      if (!n) return { cells, remaining: 0, danger: true };
      r = n.r;
      c = n.c;
      enter = OPP[dir];
    }
    const seen = new Set<string>();
    for (let i = 0; i < 80; i++) {
      if (!this.inBounds(r, c)) break;
      const cell = this.cell(r, c);
      if (!cell.piece) break;
      if (!canEnter(cell.piece, enter, cell.port)) break;
      const k = key(r, c) + ":" + enter;
      if (seen.has(k)) break;
      seen.add(k);
      cells.push({ r, c });
      if (cell.piece === "end") break;
      const leave = exitDir(cell.piece, enter, cell.port);
      if (leave === null) break;
      const n = this.neighbor(r, c, leave);
      if (!n) break;
      r = n.r;
      c = n.c;
      enter = OPP[leave];
    }
    const remaining = cells.length;
    const need = Math.max(0, this.min - this.distance);
    const danger = remaining <= 3 && (this.phase === "flowing" || this.delayLeft < 4);
    void need;
    return { cells, remaining, danger: danger && remaining > 0 && this.distance + remaining < this.min || (danger && remaining <= 2) };
  }

  resultsReady(): void {
    if (this.phase === "won" || this.phase === "lost") this.phase = "results";
  }
}

export function makeEndlessSpec(): LevelSpec {
  return {
    id: 99,
    name: "ENDLESS RIG",
    subtitle: "The valve never rests",
    cols: 10,
    rows: 7,
    min: 6,
    delay: 12,
    speed: 0.34,
    wrap: false,
    start: { r: 3, c: 0, out: E },
    weights: { straight: 22, elbow: 54, cross: 14, reservoir: 0, oneway: 0 },
    endless: true,
  };
}

export function makeDailySpec(dateKey: string): LevelSpec {
  const rand = mulberry32(
    [...dateKey].reduce((a, ch) => (a * 33 + ch.charCodeAt(0)) | 0, 7),
  );
  const wrap = rand() > 0.65;
  const hasEnd = rand() > 0.45;
  const startC = 0;
  const startR = 1 + Math.floor(rand() * 5);
  const spec: LevelSpec = {
    id: 100,
    name: "DAILY CONDUIT",
    subtitle: dateKey,
    cols: 10,
    rows: 7,
    min: 10 + Math.floor(rand() * 6),
    delay: 8 + Math.floor(rand() * 5),
    speed: 0.42 + rand() * 0.28,
    wrap,
    start: { r: startR, c: startC, out: E },
    weights: {
      straight: 20,
      elbow: 50,
      cross: 14,
      reservoir: rand() > 0.4 ? 8 : 0,
      oneway: rand() > 0.5 ? 8 : 0,
    },
  };
  const map = Array.from({ length: 7 }, () => ".".repeat(10).split(""));
  const blocks = 3 + Math.floor(rand() * 5);
  for (let i = 0; i < blocks; i++) {
    const r = Math.floor(rand() * 7);
    const c = 2 + Math.floor(rand() * 7);
    if (r === startR && c === startC) continue;
    map[r]![c] = "#";
  }
  if (hasEnd) {
    const er = 1 + Math.floor(rand() * 5);
    const ec = 9;
    map[er]![ec] = ".";
    spec.end = { r: er, c: ec, inn: W };
  }
  spec.map = map.map((row) => row.join(""));
  return spec;
}
