diff --git a/app/nose/bin/tetris.ts b/app/nose/bin/tetris.ts
new file mode 100644
index 0000000..daca53d
--- /dev/null
+++ b/app/nose/bin/tetris.ts
@@ -0,0 +1,278 @@
+///
+export const game = true
+
+import type { GameContext, InputState } from "@/shared/game"
+import { randomElement, randomIndex } from "@/shared/utils.ts"
+
+const COLS = 10
+const ROWS = 20
+const CELL = 25
+const DOWN_TICK = 10
+const MOVE_TICK = 3
+
+let player: { x: number, y: number, shape: string, rotation: number }
+let grid: string[][] = []
+let dead = false
+let downTick = 0
+let moveTick = 0
+
+const COLORS: Record = {
+ I: "cyan",
+ O: "yellow",
+ T: "purple",
+ S: "green",
+ Z: "red",
+ J: "blue",
+ L: "orange",
+}
+
+// I, O, T, L, J, S, Z
+const SHAPES: Record = {
+ I: [
+ [[1, 1, 1, 1]],
+ [[1], [1], [1], [1]]
+ ],
+
+ O: [[
+ [1, 1],
+ [1, 1]
+ ]],
+
+ T: [
+ [
+ [0, 1, 0],
+ [1, 1, 1],
+ ],
+ [
+ [1, 0],
+ [1, 1],
+ [1, 0]
+ ],
+ [
+ [1, 1, 1],
+ [0, 1, 0],
+ ],
+ [
+ [0, 1],
+ [1, 1],
+ [0, 1]
+ ]
+ ],
+
+ L: [
+ [
+ [1, 0],
+ [1, 0],
+ [1, 1]
+ ],
+ [
+ [1, 1, 1],
+ [1, 0, 0]
+ ],
+ [
+ [1, 1],
+ [0, 1],
+ [0, 1]
+ ],
+ [
+ [0, 0, 1],
+ [1, 1, 1]
+ ]
+ ],
+
+ J: [
+ [
+ [0, 1],
+ [0, 1],
+ [1, 1]
+ ],
+ [
+ [1, 0, 0],
+ [1, 1, 1]
+ ],
+ [
+ [1, 1],
+ [1, 0],
+ [1, 0]
+ ],
+ [
+ [1, 1, 1],
+ [0, 0, 1]
+ ]
+ ],
+
+ S: [
+ [
+ [0, 1, 1],
+ [1, 1, 0]
+ ],
+ [
+ [1, 0],
+ [1, 1],
+ [0, 1]
+ ],
+ [
+ [0, 1, 1],
+ [1, 1, 0],
+ ],
+ [
+ [1, 0],
+ [1, 1],
+ [0, 1]
+ ]
+ ],
+
+ Z: [
+ [
+ [1, 1, 0],
+ [0, 1, 1]
+ ],
+ [
+ [0, 1],
+ [1, 1],
+ [1, 0]
+ ],
+ [
+ [1, 1, 0],
+ [0, 1, 1]
+ ],
+ [
+ [0, 1],
+ [1, 1],
+ [1, 0]
+ ]
+ ],
+}
+
+export function init() {
+ dead = false
+ downTick = 0
+ moveTick = 0
+
+ player = newShape()
+}
+
+export function update(_delta: number, input: InputState) {
+ const keys = input.pressed
+
+ if (input.justPressed.has(" ")) {
+ player.rotation += 1
+ if (player.rotation === SHAPES[player.shape]!.length)
+ player.rotation = 0
+ }
+
+ // save to grid
+ const shape = SHAPES[player.shape]![player.rotation]!
+ if (shape) {
+ let hit = false
+ outer: for (const row in shape) {
+ for (const col in shape[row]!) {
+ const filled = shape[row][col]
+ if (!filled) continue
+
+ const x = player.x + Number(col)
+ const y = player.y + Number(row)
+
+ grid[y + 1] ??= []
+
+ if ((y + 1) >= ROWS || grid[y + 1]![x]) {
+ hit = true
+ break outer
+ }
+ }
+ }
+
+ if (hit) {
+ const shape = SHAPES[player.shape]![player.rotation]!
+ for (const row in shape) {
+ for (const col in shape[row]!) {
+ const filled = shape[row][col]
+ if (!filled) continue
+
+ const x = player.x + Number(col)
+ const y = player.y + Number(row)
+
+ grid[y] ??= []
+ grid[y][x] = COLORS[player.shape]!
+ }
+ }
+
+
+ player = newShape()
+ }
+ }
+
+ if (++moveTick % MOVE_TICK === 0) {
+ if (keys.has("ArrowLeft") || keys.has("a")) player.x = Math.max(0, player.x - 1)
+ if (keys.has("ArrowRight") || keys.has("d")) player.x = Math.min(COLS, player.x + 1)
+ if (keys.has("ArrowDown") || keys.has("s")) { player.y += 1 }
+ }
+
+ if (++downTick % DOWN_TICK === 0) {
+ player.y += 1
+ }
+}
+
+export function draw(game: GameContext) {
+ // game.clear("#6C6FF6")
+ game.clear("#654321")
+
+ const boardW = COLS * CELL
+ const boardH = ROWS * CELL
+ const offsetX = (game.width - boardW) / 2
+ const offsetY = (game.height - boardH) / 2
+
+ const c = game.ctx
+ c.save()
+ c.translate(offsetX, offsetY)
+
+ // background
+ game.rectfill(0, 0, boardW, boardH, "black")
+
+ // draw board
+ for (let row = 0; row < ROWS; row++) {
+ for (let col = 0; col < COLS; col++) {
+ drawBlock(game, col, row, grid[row]?.[col] || "gray")
+ }
+ }
+
+ // player shape
+ const shape = SHAPES[player.shape]![player.rotation]!
+ for (const row in shape) {
+ for (const col in shape[row]!) {
+ const filled = shape[row][col]
+ if (!filled) continue
+
+ const x = player.x + Number(col)
+ const y = player.y + Number(row)
+
+ drawBlock(game, x, y, COLORS[player.shape]!)
+ }
+ }
+
+ c.restore()
+
+ // ya dead
+ if (dead) {
+ game.centerText("GAME OVER", "red", 24)
+ }
+}
+
+function newShape() {
+ const shape = randomElement(Object.keys(SHAPES))!
+ const newShape = { x: 3, y: 0, shape, rotation: randomIndex(SHAPES[shape]!)! }
+ // const shape = SHAPES["O"]
+ // const newShape = { x: 3, y: 0, shape, rotation: randomIndex(SHAPES[shape]!)! }
+ console.log("new shape", newShape)
+ return newShape
+}
+
+function drawBlock(game: GameContext, x: number, y: number, color: string) {
+ game.rectfill(
+ x * CELL,
+ y * CELL,
+ ((x + 1) * CELL) - .5,
+ ((y + 1) * CELL) - .5,
+ color
+ )
+}
\ No newline at end of file