Code Sketch
yoii
Category: Programming
// ============================================================
// HAUNTED HOUSE ESCAPE 3D - Kojo (Scala) first-person horror escape
// Goal: find 3 golden keys, then reach the green exit door (bottom right)
// Avoid the Warden! He hunts you through the maze.
// Controls: UP/DOWN = walk, LEFT/RIGHT = turn, ENTER = restart
// ============================================================
cleari()
drawStage(black)
setRefreshRate(30)
val W = 640.0; val H = 400.0; val N = 128
val CW = W / N
val FOV = math.Pi / 3
val MAP = Array(
"1111111111111111",
"1000000100000001",
"1011110101111101",
"1010000000000101",
"1010111111110101",
"1000100000010001",
"1110101111010111",
"1000001001000001",
"1011111001111101",
"1000000000000101",
"1011110111100101",
"1010000100000101",
"1010111101111101",
"1010000001000001",
"1000111101011101",
"1111111111111121")
def cell(x: Int, y: Int): Char =
if (x < 0 || y < 0 || x >= 16 || y >= 16) '1' else MAP(y)(x)
val rnd = new scala.util.Random()
var px = 1.5; var py = 1.5; var ang = 0.0
var ex = 7.5; var ey = 9.5
var keys = List[(Double, Double)]()
var got = 0
var turnL = false; var turnR = false; var fwd = false; var back = false
var gameOver = false; var won = false
var frame = List[Picture](); var tick = 0
var shake = 0.0
// ---------- BFS distance map from a cell ----------
def distMap(tx: Int, ty: Int): Array[Array[Int]] = {
val d = Array.fill(16, 16)(-1)
var q = List((tx, ty)); d(ty)(tx) = 0
while (q.nonEmpty) {
val (cx, cy) = q.head; q = q.tail
for ((dx, dy) <- List((1, 0), (-1, 0), (0, 1), (0, -1))) {
val nx = cx + dx; val ny = cy + dy
if (cell(nx, ny) == '0' && d(ny)(nx) == -1) { d(ny)(nx) = d(cy)(cx) + 1; q = q :+ ((nx, ny)) }
}
}
d
}
def reset() {
px = 1.5; py = 1.5; ang = 0.0; ex = 7.5; ey = 9.5
got = 0; gameOver = false; won = false; tick = 0
val d = distMap(1, 1)
val far = (for (y <- 0 until 16; x <- 0 until 16 if d(y)(x) > 9 && !(x == 7 && y == 9)) yield (x + 0.5, y + 0.5)).toList
keys = rnd.shuffle(far).take(3)
}
// ---------- raycaster (DDA) ----------
def cast(a: Double): (Double, Int, Char) = {
val dx = math.cos(a); val dy = math.sin(a)
var mx = px.toInt; var my = py.toInt
val ddx = if (dx == 0) 1e30 else math.abs(1 / dx)
val ddy = if (dy == 0) 1e30 else math.abs(1 / dy)
val stepX = if (dx < 0) -1 else 1
val stepY = if (dy < 0) -1 else 1
var sdx = if (dx < 0) (px - mx) * ddx else (mx + 1 - px) * ddx
var sdy = if (dy < 0) (py - my) * ddy else (my + 1 - py) * ddy
var side = 0; var hit = false; var c = '1'; var guard = 0
while (!hit && guard < 40) {
guard += 1
if (sdx < sdy) { sdx += ddx; mx += stepX; side = 0 } else { sdy += ddy; my += stepY; side = 1 }
val ch = cell(mx, my)
if (ch != '0') { hit = true; c = ch }
}
(if (side == 0) sdx - ddx else sdy - ddy, side, c)
}
def walkable(x: Double, y: Double) = cell(x.toInt, y.toInt) == '0'
def shade(r: Int, g: Int, b: Int, dist: Double): Color = {
val k = math.max(0.03, 1.0 - dist / 7.5) // flashlight / darkness
Color((r * k).toInt, (g * k).toInt, (b * k).toInt)
}
def add(p: Picture) { draw(p); frame = p :: frame }
// ---------- sprite projection: returns (screenX, size, dist) if visible ----------
def project(wx: Double, wy: Double, depth: Array[Double]): Option[(Double, Double, Double)] = {
val dx = wx - px; val dy = wy - py
val dist = math.sqrt(dx * dx + dy * dy)
var rel = math.atan2(dy, dx) - ang
while (rel > math.Pi) rel -= 2 * math.Pi
while (rel < -math.Pi) rel += 2 * math.Pi
if (math.abs(rel) > FOV / 2 + 0.1 || dist < 0.2) None
else {
val col = ((rel / FOV + 0.5) * N).toInt
val ok = col >= 0 && col < N && depth(col) > dist * math.cos(rel)
if (ok) Some(((rel / FOV + 0.5) * W - W / 2, H / dist * 0.9, dist)) else None
}
}
def update() {
tick += 1
if (gameOver || won) return
if (turnL) ang -= 0.075
if (turnR) ang += 0.075
val sp = 0.085
var mvx = 0.0; var mvy = 0.0
if (fwd) { mvx = math.cos(ang) * sp; mvy = math.sin(ang) * sp }
if (back) { mvx = -math.cos(ang) * sp * 0.7; mvy = -math.sin(ang) * sp * 0.7 }
if (walkable(px + mvx * 3, py)) px += mvx
if (walkable(px, py + mvy * 3)) py += mvy
// keys pickup
keys.foreach { k => if (math.hypot(k._1 - px, k._2 - py) < 0.55) got += 1 }
keys = keys.filter(k => math.hypot(k._1 - px, k._2 - py) >= 0.55)
// win
if (px.toInt == 14 && py.toInt == 14 && got >= 3) won = true
// Warden AI: BFS chase, gets faster with every key
val d = distMap(px.toInt, py.toInt)
val cx = ex.toInt; val cy = ey.toInt
var best = (cx, cy); var bd = d(cy)(cx)
for ((dx, dy) <- List((1, 0), (-1, 0), (0, 1), (0, -1))) {
val nx = cx + dx; val ny = cy + dy
if (cell(nx, ny) == '0' && d(ny)(nx) != -1 && (bd == -1 || d(ny)(nx) < bd)) { bd = d(ny)(nx); best = (nx, ny) }
}
val tx = best._1 + 0.5; val ty = best._2 + 0.5
val es = 0.032 + 0.012 * got
val len = math.hypot(tx - ex, ty - ey)
if (len > 0.02) { ex += (tx - ex) / len * es; ey += (ty - ey) / len * es }
val dist = math.hypot(ex - px, ey - py)
shake = if (dist < 4) (4 - dist) * 2.5 else 0
if (dist < 0.6) gameOver = true
}
def render() {
frame.foreach(_.erase()); frame = Nil
val depth = new Array[Double](N)
val sx = if (shake > 0) (rnd.nextDouble - 0.5) * shake else 0.0
val sy = if (shake > 0) (rnd.nextDouble - 0.5) * shake else 0.0
// walls
for (i <- 0 until N) {
val a = ang - FOV / 2 + i * FOV / N
val (dRaw, side, c) = cast(a)
val dist = math.max(0.05, dRaw * math.cos(a - ang))
depth(i) = dist
val lineH = math.min(H * 1.4, H / dist)
val flick = if (tick % 47 < 2) 0.6 else 1.0 // flickering light
val base = if (c == '2') (if (got >= 3) (40, 255, 80) else (180, 40, 40))
else if (side == 0) (150, 110, 80) else (110, 80, 60)
val col = shade((base._1 * flick).toInt, (base._2 * flick).toInt, (base._3 * flick).toInt, dist)
val r = Picture.rectangle(CW + 1, lineH)
r.setFillColor(col); r.setPenColor(noColor)
r.setPosition(-W / 2 + i * CW + sx, -lineH / 2 + sy)
add(r)
}
// keys (golden diamonds)
keys.sortBy(k => -math.hypot(k._1 - px, k._2 - py)).foreach { k =>
project(k._1, k._2, depth).foreach { case (x, size, dist) =>
val s = size * 0.12
val bob = math.sin(tick * 0.15) * s * 0.4
val key = Picture.circle(s)
key.setFillColor(shade(255, 215, 0, dist * 0.5)); key.setPenColor(Color(255, 255, 150))
key.setPosition(x + sx, -size * 0.2 + bob + sy); add(key)
}
}
// the Warden (original character: hooded pale figure with glowing eyes)
project(ex, ey, depth).foreach { case (x, size, dist) =>
val bw = size * 0.38; val bh = size * 0.75
val body = Picture.rectangle(bw, bh)
body.setFillColor(shade(70, 70, 90, dist * 0.6)); body.setPenColor(noColor)
body.setPosition(x - bw / 2 + sx, -size * 0.45 + sy); add(body)
val head = Picture.circle(bw * 0.38)
head.setFillColor(shade(220, 215, 200, dist * 0.6)); head.setPenColor(noColor)
head.setPosition(x + sx, -size * 0.45 + bh + sy); add(head)
for (ox <- Seq(-0.12, 0.12)) {
val eye = Picture.circle(bw * 0.07)
eye.setFillColor(Color(255, 20, 20)); eye.setPenColor(noColor)
eye.setPosition(x + ox * bw + sx, -size * 0.45 + bh + bw * 0.08 + sy); add(eye)
}
}
// vignette bars + HUD
val hud = Picture.text(s"KEYS: $got / 3 ${if (got >= 3) "EXIT IS OPEN - RUN TO THE GREEN DOOR!" else "Find all keys..."}", 18)
hud.setPenColor(Color(255, 220, 120)); hud.setPosition(-300, 175); add(hud)
if (gameOver) {
val ov = Picture.rectangle(W, H); ov.setFillColor(Color(120, 0, 0, 170)); ov.setPenColor(noColor)
ov.setPosition(-W / 2, -H / 2); add(ov)
val g = Picture.text("THE WARDEN CAUGHT YOU! - ENTER = restart", 26)
g.setPenColor(white); g.setPosition(-290, 0); add(g)
}
if (won) {
val g = Picture.text("YOU ESCAPED! - ENTER = play again", 30)
g.setPenColor(Color(60, 255, 100)); g.setPosition(-250, 0); add(g)
}
}
onKeyPress { k =>
k match {
case Kc.VK_LEFT => turnL = true
case Kc.VK_RIGHT => turnR = true
case Kc.VK_UP => fwd = true
case Kc.VK_DOWN => back = true
case Kc.VK_ENTER => if (gameOver || won) reset()
case _ =>
}
}
onKeyRelease { k =>
k match {
case Kc.VK_LEFT => turnL = false
case Kc.VK_RIGHT => turnR = false
case Kc.VK_UP => fwd = false
case Kc.VK_DOWN => back = false
case _ =>
}
}
// floor and ceiling (drawn once, behind everything)
val ceil = Picture.rectangle(W, H / 2); ceil.setFillColor(Color(12, 10, 18)); ceil.setPenColor(noColor)
ceil.setPosition(-W / 2, 0); draw(ceil)
val floor = Picture.rectangle(W, H / 2); floor.setFillColor(Color(28, 20, 16)); floor.setPenColor(noColor)
floor.setPosition(-W / 2, -H / 2); draw(floor)
reset()
animate {
update()
render()
}
activateCanvas()