Skip to content

Instantly share code, notes, and snippets.

@fractal161
Last active April 14, 2025 16:40
Show Gist options
  • Save fractal161/b644413f71d09a178df13b9957b77367 to your computer and use it in GitHub Desktop.
Save fractal161/b644413f71d09a178df13b9957b77367 to your computer and use it in GitHub Desktop.
Passing Tetris in 7 Minutes
--[=[
PLANNING
- how to control piece sequence?
- information per execution unit:
- handler at the start of frame
- optionally controls piece movement according to another table
- handler right at end of piecegen
- special handler for first tetris because it does piecegen differently
- "extra" handlers, specify function and memory address
- have destructor that deregisters callbacks
- try and use closures when possible to keep variables nice and local
- storing piece movements:
- have a string of inputs per row? so can lookup pieceY/gravity to decide
what inputs to make
--]=]
-- change this to start on later tetrises. may break for some cases because of
-- weird dependencies
local START_SCENE = 1
-- luals type annotations
---@class (exact) Callback
---@field addr number
---@field fn fun(addr: number, val: number)
---@alias Piece string
-- each row maps to an input, so we can have gaps
-- first tuple entry is dpad, second is rotations
---@alias Input [string, string]
---@alias Inputs Input[]
-- resting x, y, orientation
---@alias LockPosition [number, number, number]
---@alias Move [Piece, LockPosition, Input]
---@class (exact) Scene
---@field description string
---@field moves Move[]
---@field callbacks? Callback[]
-- rom memory addresses
local SPAWN_ID_ADDR = 0x19
local PIECE_X_ADDR = 0x40
-- will be zero-indexed
local PIECE_Y_ADDR = 0x41
local CURRENT_PIECE_ADDR = 0x42
local LEVEL_NUMBER_ADDR = 0x44
-- cycles from 1 to gravity number, so it's already 1-indexed!
local FALL_TIMER_ADDR = 0x45
local PLAY_STATE_ADDR = 0x048
local NEXT_PIECE_ADDR = 0xBF
local GAME_MODE_ADDR = 0xC0 -- if 3 to 4, then reset state
local NEW_BUTTONS_ADDR = 0xB5
local HELD_BUTTONS_ADDR = 0xB6
local GAME_LOOP_START_ADDR = 0x81CF
local GAME_LOOP_END_ADDR = 0x81D8
-- local SPAWN_PIECE_END_ADDR = 0x98EA -- update counters here?
local PICK_PIECE_END_ADDR = 0x993A
local PIECE_ID_TABLE = {
-- normal pieces
["T"] = 0x02,
["J"] = 0x07,
["Z"] = 0x08,
["O"] = 0x0A,
["S"] = 0x0B,
["L"] = 0x0E,
["I"] = 0x12,
-- special pieces (given arbitrary names)
-- big mode
["Jb"] = 0x07,
["Zb"] = 0x08,
["Lb"] = 0x0E,
["Ib"] = 0x12,
}
-- holy shit meatfighter i love you so much
local PIECE_COORDS = {
{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, -1 }, }, -- 00: T up
{ { 0, -1 }, { 0, 0 }, { 1, 0 }, { 0, 1 }, }, -- 01: T right
{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, 1 }, }, -- 02: T down (spawn)
{ { 0, -1 }, { -1, 0 }, { 0, 0 }, { 0, 1 }, }, -- 03: T left
{ { 0, -1 }, { 0, 0 }, { -1, 1 }, { 0, 1 }, }, -- 04: J left
{ { -1, -1 }, { -1, 0 }, { 0, 0 }, { 1, 0 }, }, -- 05: J up
{ { 0, -1 }, { 1, -1 }, { 0, 0 }, { 0, 1 }, }, -- 06: J right
{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { 1, 1 }, }, -- 07: J down (spawn)
{ { -1, 0 }, { 0, 0 }, { 0, 1 }, { 1, 1 }, }, -- 08: Z horizontal (spawn)
{ { 1, -1 }, { 0, 0 }, { 1, 0 }, { 0, 1 }, }, -- 09: Z vertical
{ { -1, 0 }, { 0, 0 }, { -1, 1 }, { 0, 1 }, }, -- 0A: O (spawn)
{ { 0, 0 }, { 1, 0 }, { -1, 1 }, { 0, 1 }, }, -- 0B: S horizontal (spawn)
{ { 0, -1 }, { 0, 0 }, { 1, 0 }, { 1, 1 }, }, -- 0C: S vertical
{ { 0, -1 }, { 0, 0 }, { 0, 1 }, { 1, 1 }, }, -- 0D: L right
{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { -1, 1 }, }, -- 0E: L down (spawn)
{ { -1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 }, }, -- 0F: L left
{ { 1, -1 }, { -1, 0 }, { 0, 0 }, { 1, 0 }, }, -- 10: L up
{ { 0, -2 }, { 0, -1 }, { 0, 0 }, { 0, 1 }, }, -- 11: I vertical
{ { -2, 0 }, { -1, 0 }, { 0, 0 }, { 1, 0 }, }, -- 12: I horizontal (spawn)
}
-- some helpers for working with the game board
local function getBoardCell(x, y)
return emu.read(0x400+10*y+x, emu.memType.nesDebug)
end
local function setBoardCell(x, y, b)
emu.write(0x400+10*y+x, b, emu.memType.nesDebug)
end
-- PENTO SPAWNS (x is block, o is center)
-- V: xxx
-- ox
-- x
-- Y: xxox
-- x
-- F: x
-- xox
-- x
-- U: xox
-- x x
-- P: xox
-- xx
-- T: xox
-- x
-- x
-- N: xox
-- xx
-- W: xx
-- ox
-- x
-- Z: x
-- xox
-- x
-- I: xxoxx
local pentos = {
{ {-1, -1}, {0, -1}, {1, -1}, {1, 0}, {1, 1} },
{ {-2, 0}, {-1, 0}, {0,0}, {1,0}, {0,1} },
{ {0,-1}, {-1,0}, {0,0}, {1,0}, {1,1} },
{ {-1,0}, {0,0}, {1,0}, {-1,1}, {1,1} },
{ {-1,0}, {0,0}, {1,0}, {0,1}, {1,1} },
{ {-1,-1}, {0,-1}, {0,0}, {1, 0}, {1,1} },
{ {-1,-1}, {-1,0}, {0,0}, {1,0}, {1,1} },
{ {-1,0}, {0,0}, {1,0}, {1,1}, {2,1} },
{ {-1,0}, {0,0}, {1,0}, {0,1}, {0,2} },
{ {-2,0}, {-1,0}, {0, 0}, {1,0}, {2,0} },
}
local pentoKinds = { 1, 3, 3, 1, 2, 1, 2, 2, 1, 1 }
-- configuration states
local autoInput = false
local gameMode = emu.read(GAME_MODE_ADDR, emu.memType.nesDebug)
-- should correspond to the active piece. we initialize to -1 because there
-- isn't an active piece at the start
local pieceIndex = -1
-- tracks how long the current piece has been active
local pieceFrame = 1
-- callbacks!
local function scene9_midair(addr, val)
local pieceX = emu.read(PIECE_X_ADDR, emu.memType.nesDebug)
local pieceY = emu.read(PIECE_Y_ADDR, emu.memType.nesDebug)
local fallTimer = emu.read(FALL_TIMER_ADDR, emu.memType.nesDebug)
if pieceIndex == 103 and pieceX == 3 and pieceY == 17 and fallTimer == 0 then
local state = emu.getState()
state["cpu.ps"] = state["cpu.ps"] - 0x02 -- the code that actually locks
emu.setState(state)
end
end
-- force a pause if not done by the player
local function scene14_forcePause(addr, val)
local currentPiece = emu.read(CURRENT_PIECE_ADDR, emu.memType.nesDebug)
if pieceIndex == 160 and pieceFrame == 24 and currentPiece ~= 0x11 then
-- not NEW_BUTTONS because player 1 buttons are specifically checked
local buttons = emu.read(0xF5, emu.memType.nesDebug)
buttons = buttons + 0x10 -- start
emu.write(0xF5, buttons, emu.memType.nesDebug)
end
end
local function scene14_swapBar(addr, val)
emu.write(PIECE_X_ADDR, 9, emu.memType.nesDebug)
emu.write(CURRENT_PIECE_ADDR, 0x11, emu.memType.nesDebug)
emu.write(PIECE_X_ADDR+0x20, 9, emu.memType.nesDebug)
emu.write(CURRENT_PIECE_ADDR+0x20, 0x11, emu.memType.nesDebug)
end
local function scene19_rotatedBar(addr, val)
if pieceIndex == 209 then
emu.write(CURRENT_PIECE_ADDR, 0x11, emu.memType.nesDebug)
end
-- more prep for scene 20
if pieceIndex == 210 then
emu.write(CURRENT_PIECE_ADDR, 0x10, emu.memType.nesDebug)
end
end
local function scene20_prerotations(addr, val)
-- emu.log(tostring(pieceIndex))
-- emu.breakExecution()
local orients = { 0x10, 0x0D, 0x0A, 0x04, 0x09, 0x0E, 0x00, 0x06, 0x03, 0x11 }
local index = pieceIndex - 209
if index <= 10 then
emu.write(CURRENT_PIECE_ADDR, orients[index], emu.memType.nesDebug)
end
end
local function scene21_deepDrop(addr, val)
local pieceY = emu.read(PIECE_Y_ADDR, emu.memType.nesDebug)
if pieceIndex == 229 and pieceY < 17 then
local state = emu.getState()
state["cpu.ps"] = state["cpu.ps"] | 0x02
emu.setState(state)
end
end
local function scene22_quadTuck(addr, val)
local pieceY = emu.read(PIECE_Y_ADDR, emu.memType.nesDebug)
if pieceIndex == 236 and pieceY == 19 then
emu.write(0xAF, 12, emu.memType.nesDebug)
end
end
local function scene23_srs(addr, val)
local pieceY = emu.read(PIECE_Y_ADDR, emu.memType.nesDebug)
local fallTimer = emu.read(FALL_TIMER_ADDR, emu.memType.nesDebug)
if pieceIndex == 244 and pieceY == 17 and fallTimer == 3 then
-- piecex = 7, piecey = 18, falltimer = 0, sfx (0x6f1 gets 0x05
emu.write(PIECE_X_ADDR, 6, emu.memType.nesDebug)
emu.write(PIECE_Y_ADDR, 18, emu.memType.nesDebug)
emu.write(FALL_TIMER_ADDR, 0, emu.memType.nesDebug)
emu.write(CURRENT_PIECE_ADDR, 0x0B, emu.memType.nesDebug)
emu.write(0x6F1, 5, emu.memType.nesDebug)
elseif pieceIndex == 245 and pieceY == 18 and fallTimer == 3 then
-- piecex = 7, piecey = 18, falltimer = 0, sfx (0x6f1 gets 0x05
emu.write(PIECE_X_ADDR, 1, emu.memType.nesDebug)
emu.write(PIECE_Y_ADDR, 19, emu.memType.nesDebug)
emu.write(FALL_TIMER_ADDR, 0, emu.memType.nesDebug)
emu.write(CURRENT_PIECE_ADDR, 0x05, emu.memType.nesDebug)
emu.write(0x6F1, 5, emu.memType.nesDebug)
elseif pieceIndex == 249 and pieceY == 15 and fallTimer == 3 then
-- piecex = 7, piecey = 18, falltimer = 0, sfx (0x6f1 gets 0x05
emu.write(PIECE_X_ADDR, 5, emu.memType.nesDebug)
emu.write(PIECE_Y_ADDR, 16, emu.memType.nesDebug)
emu.write(FALL_TIMER_ADDR, 0, emu.memType.nesDebug)
emu.write(CURRENT_PIECE_ADDR, 0x08, emu.memType.nesDebug)
emu.write(0x6F1, 5, emu.memType.nesDebug)
else
return
end
-- update pc because the frame has been processed 0x81D8
local state = emu.getState()
state["cpu.pc"] = 0x81D8
emu.setState(state)
end
local function scene23_preArs(addr, val)
if pieceIndex == 250 then
emu.write(PIECE_X_ADDR, 4, emu.memType.nesDebug)
emu.write(PIECE_Y_ADDR, 18, emu.memType.nesDebug)
emu.write(CURRENT_PIECE_ADDR, 4, emu.memType.nesDebug)
end
end
-- faking ars/spawn
local function scene24_ars(addr, val)
local xs = { 4, 5, 4, 5, 5, 4, 4, 4, 4, 4 }
local ys = { 18, 15, 15, 15, 15, 15, 14, 14, 14, 14 }
local orients = { 0x04, 0x0A, 0x0B, 0x09, 0x0A, 0x0B, 0x06, 0x0E, 0x07, 0x11 }
local index = pieceIndex - 249
if index <= 10 then
emu.write(PIECE_X_ADDR, xs[index], emu.memType.nesDebug)
emu.write(PIECE_Y_ADDR, ys[index], emu.memType.nesDebug)
emu.write(CURRENT_PIECE_ADDR, orients[index], emu.memType.nesDebug)
end
end
local function scene24_tgm(addr, val)
-- basically take over the game loop
-- after a shift, drop as far as possible
local buttons = emu.read(NEW_BUTTONS_ADDR, emu.memType.nesDebug)
-- handle shifts generically
if buttons & 0x02 > 0 then
local x = emu.read(PIECE_X_ADDR, emu.memType.nesDebug)
if x > 0 then
x = x - 1
emu.write(PIECE_X_ADDR, x, emu.memType.nesDebug)
emu.write(0x6F1, 3, emu.memType.nesDebug)
end
elseif buttons & 0x01 > 0 then
local x = emu.read(PIECE_X_ADDR, emu.memType.nesDebug)
if x < 9 then
x = x + 1
emu.write(PIECE_X_ADDR, x, emu.memType.nesDebug)
emu.write(0x6F1, 3, emu.memType.nesDebug)
end
end
-- special cases!
local pieceX = emu.read(PIECE_X_ADDR, emu.memType.nesDebug)
local pieceY = emu.read(PIECE_Y_ADDR, emu.memType.nesDebug)
local dropPositions = {
{ 252, 6, 15, 18 },
{ 253, 3, 15, 16 },
{ 253, 2, 16, 18 },
{ 254, 3, 15, 16 },
{ 254, 1, 16, 17 },
{ 254, 0, 17, 18 },
{ 255, 6, 15, 16 },
{ 255, 8, 16, 18 },
{ 256, 3, 15, 16 },
{ 257, 5, 14, 16 },
{ 258, 3, 14, 15 },
{ 258, 1, 15, 16 },
{ 259, 5, 14, 15 },
{ 259, 7, 15, 16 },
{ 260, 9, 14, 18 },
}
for _, p in ipairs(dropPositions) do
if pieceIndex == p[1] and pieceX == p[2] and pieceY == p[3] then
emu.write(PIECE_Y_ADDR, p[4], emu.memType.nesDebug)
end
end
-- special kick override
if pieceIndex == 257 and buttons == 0x40 then
emu.write(PIECE_Y_ADDR, 17, emu.memType.nesDebug)
emu.write(PIECE_X_ADDR, 6, emu.memType.nesDebug)
emu.write(CURRENT_PIECE_ADDR, 5, emu.memType.nesDebug)
emu.write(0x6F1, 5, emu.memType.nesDebug)
end
-- skip game loop
local state = emu.getState()
state["cpu.pc"] = 0x81D8
-- just be stupid and lock on 30th frame each time
if pieceFrame == 30 then
state["cpu.pc"] = 0x896B
end
emu.setState(state)
end
-- SCENE 25 (ZONE)
-- yea the big one lmfao
-- TODO: parse up as hard drop (0x81D8)
local function scene25_hardDrop(addr, val)
if pieceIndex < 295 then
return
end
local buttons = emu.read(NEW_BUTTONS_ADDR, emu.memType.nesDebug)
local currentPiece = emu.read(CURRENT_PIECE_ADDR, emu.memType.nesDebug)
local x = emu.read(PIECE_X_ADDR, emu.memType.nesDebug)
local y = emu.read(PIECE_Y_ADDR, emu.memType.nesDebug)
local index = pieceIndex - 294
if buttons & 0x08 == 0 then
return
end
-- TODO: drop piece a lot
local coords = PIECE_COORDS[currentPiece + 1]
local done = false
while not done do
y = y + 1
for _, coord in ipairs(coords) do
local xp = x + coord[1]
local yp = y + coord[2]
if xp < 0 or xp > 9 or yp > 19 then
y = y - 1
done = true
break
end
if yp >= 0 and getBoardCell(xp, yp) ~= 0xEF then
y = y - 1
done = true
break
end
end
end
emu.write(PIECE_Y_ADDR, y, emu.memType.nesDebug)
-- lock piece
emu.write(0x48, 2, emu.memType.nesDebug)
-- update vram
emu.write(0x49, math.max(y-2, 0), emu.memType.nesDebug)
end
-- TODO: get rid of line clear/tetris soundeffects. lots of places
-- 0x9695
-- 0x9A9B
-- 0x9AF2
-- 0x9BF2
local function scene25_disableSfx(addr, val)
if pieceIndex < 295 then
return
end
local state = emu.getState()
state["cpu.pc"] = state["cpu.pc"] + 3
emu.setState(state)
end
-- TODO: disable gravity at certain index (0x8984)
local function scene25_zeroGravity(addr, val)
if pieceIndex < 295 then
return
end
local state = emu.getState()
state["cpu.ps"] = state["cpu.ps"] | 0x80
emu.setState(state)
end
-- TODO: line clears with white blocks (0x97D3, 0x97E8)
local function scene25_lineClearBlocks(addr, val)
local state = emu.getState()
state["cpu.a"] = 0xFE
emu.setState(state)
end
-- TODO: hijack part that moves rows down, move them up instead (skip the line clear during this part)
-- TODO: ideally keep the white rows
-- 9A74
-- may be broken for high up pieces? unclear
local function scene25_zoneClears(addr, val)
local pieceY = emu.read(PIECE_Y_ADDR, emu.memType.nesDebug)
local lineIndex = emu.read(0x57, emu.memType.nesDebug)
local y = pieceY + 1 - lineIndex
local fullLine = false
if y < 0 then
goto finish
end
fullLine = true
for x=0,9 do
if getBoardCell(x, y) == 0xEF then
fullLine = false
break
end
end
if not fullLine then
goto finish
end
do
local completedLines = emu.read(0x56, emu.memType.nesDebug)
emu.write(0x56, completedLines + 1, emu.memType.nesDebug)
emu.write(CURRENT_PIECE_ADDR, 0x13, emu.memType.nesDebug)
end
-- move entire playfield up
for i=y,18 do
for x=0,9 do
local b = getBoardCell(x, i+1)
setBoardCell(x, i, b)
end
end
for x=0,9 do
setBoardCell(x, 19, 0xFE)
end
::finish::
-- update completedrow array
if fullLine then
emu.write(0x4A+lineIndex, y, emu.memType.nesDebug)
else
emu.write(0x4A+lineIndex, 0, emu.memType.nesDebug)
end
local state = emu.getState()
state["cpu.pc"] = 0x9AD2
emu.setState(state)
end
-- TODO: disable score updates until the end
local function scene25_hideScore(addr, val)
end
-- TODO: clean everything up after score is done
local function scene25_clean(addr, val)
if pieceIndex ~= 310 then
return
end
for i=1,200 do
emu.write(0x3FF+i, 0xEF, emu.memType.nesDebug)
end
-- write score
emu.write(0x55, 0x70, emu.memType.nesDebug)
emu.write(0x54, 0x68, emu.memType.nesDebug)
emu.write(0x53, 0x00, emu.memType.nesDebug)
end
-- for the zone effect (register at 0x8005)
local function scene25_invert(t)
local buffer = emu.getScreenBuffer()
if t >= 1 then
for i = 1, #buffer do
buffer[i] = 0xFFFFFF - buffer[i]
end
else
for i = 1, #buffer do
r = buffer[i] >> 16
g = (buffer[i] >> 8) & 0xFF
b = buffer[i] & 0xFF
r2 = math.floor(t*0xFF + (1-2*t)* r+0.5)
g2 = math.floor(t*0xFF + (1-2*t)* g+0.5)
b2 = math.floor(t*0xFF + (1-2*t)* b+0.5)
buffer[i] = (r2 << 16)+ (g2 << 8) + b2
end
end
emu.setScreenBuffer(buffer)
end
local scene25_invertTimer = 0
local function scene25_inverter(addr, val)
if pieceIndex < 302 then
return
end
scene25_invertTimer = scene25_invertTimer + 1
local t = scene25_invertTimer / 30
scene25_invert(t)
end
-- SCENE 26 PLANNING
-- ok collisions is weird so we're just hijacking it all
-- at 948B
-- jump to 94E9 when we actually want to lock
-- callback at 99A2 to skip to 9A0E to disable locking
-- 9a6b to force checkforcompletedrows to continue
-- TODO: add callback to undo invert from zone
local function scene26_noCollision(addr, val)
local index = pieceIndex - 310
if not (index == 7 or index == 9 or index == 10 or index == 13) then
return
end
-- if pieceY is too low
local state = emu.getState()
if emu.read(PIECE_Y_ADDR, emu.memType.nesDebug) == 27 then
state["cpu.pc"] = 0x94E9
else
state["cpu.pc"] = 0x94E4
-- emu.breakExecution()
end
emu.setState(state)
end
local function scene26_noLock(addr, val)
local index = pieceIndex - 310
if index == 7 or index == 9 or index == 10 or index == 13 then
local state = emu.getState()
state["cpu.pc"] = 0x9A0E
emu.setState(state)
end
end
local function scene26_hack(addr, val)
local index = pieceIndex - 310
if index == 7 or index == 9 or index == 10 or index == 13 then
emu.write(0x49, 0x20, emu.memType.nesDebug)
end
end
local function scene27_garbage(addr, val)
if pieceIndex ~= 324 then
return
end
if addr == 0x9B05 then
-- bypass two player check
local state = emu.getState()
state["cpu.a"] = 2
emu.setState(state)
end
if addr == 0x9B09 then
-- set pending garbage to 4
emu.write(0xBB, 4, emu.memType.nesDebug)
-- set well column
emu.write(0x5A, 2, emu.memType.nesDebug)
end
end
-- 0x993A to set orientation/spawn
-- 0x81CF to capture game logic (see tgm functions for how to end)
-- 0x8A0A for current piece sprite (just draw rectangles)
-- 0x8BCE for next piece sprite (but not necessary here?)
-- takes board coordinates
local function drawBigMino(x, y, kind)
if x < 0 or y < 0 then
return
end
local color1 = 0xFE6ECC
local color2 = 0x88D800
local color3 = 0xFE8170
if kind == 1 then
-- color1 and color3
emu.drawRectangle(96+8*x, 48+8*y, 14, 14, color1, true)
emu.drawRectangle(96+8*x, 48+8*y, 2, 2, color3, true)
emu.drawRectangle(96+2+8*x, 48+2+8*y, 10, 10, color3, true)
elseif kind == 2 then
-- color2 and color3
emu.drawRectangle(96+8*x, 48+8*y, 14, 14, color2, true)
emu.drawRectangle(96+8*x, 48+8*y, 2, 2, color3, true)
emu.drawRectangle(96+2+8*x, 48+2+8*y, 4, 4, color3, true)
emu.drawRectangle(96+4+8*x, 48+4+8*y, 2, 2, color2, true)
elseif kind == 3 then
-- color1 and color3
emu.drawRectangle(96+8*x, 48+8*y, 14, 14, color1, true)
emu.drawRectangle(96+8*x, 48+8*y, 2, 2, color3, true)
emu.drawRectangle(96+2+8*x, 48+2+8*y, 4, 4, color3, true)
emu.drawRectangle(96+4+8*x, 48+4+8*y, 2, 2, color1, true)
end
end
local function scene29_currentPiece(addr, val)
local x = emu.read(PIECE_X_ADDR, emu.memType.nesDebug)
local y = emu.read(PIECE_Y_ADDR, emu.memType.nesDebug)
local currentPiece = emu.read(CURRENT_PIECE_ADDR, emu.memType.nesDebug)
if currentPiece == 0x12 then
-- i flat
drawBigMino(x-5, y, 1)
drawBigMino(x-3, y, 1)
drawBigMino(x-1, y, 1)
drawBigMino(x+1, y, 1)
elseif currentPiece == 0x11 then
-- i vert
drawBigMino(x-1, y-4, 1)
drawBigMino(x-1, y-2, 1)
drawBigMino(x-1, y-0, 1)
drawBigMino(x-1, y+2, 1)
elseif currentPiece == 0x07 then
-- j down
drawBigMino(x-3, y, 3)
drawBigMino(x-1, y, 3)
drawBigMino(x+1, y, 3)
drawBigMino(x+1, y+2, 3)
elseif currentPiece == 0x0E then
-- l down
drawBigMino(x-3, y, 2)
drawBigMino(x-1, y, 2)
drawBigMino(x+1, y, 2)
drawBigMino(x-3, y+2, 2)
elseif currentPiece == 0x0D then
-- l rirhgt
drawBigMino(x-1, y-2, 2)
drawBigMino(x-1, y, 2)
drawBigMino(x-1, y+2, 2)
drawBigMino(x+1, y+2, 2)
elseif currentPiece == 0x08 then
-- z flat
drawBigMino(x-3, y, 2)
drawBigMino(x-1, y, 2)
drawBigMino(x-1, y+2, 2)
drawBigMino(x+1, y+2, 2)
end
local state = emu.getState()
state["cpu.pc"] = 0x8A9B
emu.setState(state)
end
-- 0x89F2
local function scene29_shiftLeft(addr, val)
local x = emu.read(PIECE_X_ADDR, emu.memType.nesDebug)
emu.write(PIECE_X_ADDR, x-1, emu.memType.nesDebug)
end
-- 0x89DD
local function scene29_shiftRight(addr, val)
local x = emu.read(PIECE_X_ADDR, emu.memType.nesDebug)
emu.write(PIECE_X_ADDR, x+1, emu.memType.nesDebug)
end
-- 0x8960
local function scene29_drop(addr, val)
local index = pieceIndex - 335
local y = emu.read(PIECE_Y_ADDR, emu.memType.nesDebug)
-- emu.log(y)
-- emu.breakExecution()
local ys = { 18, 14, 14, 12, 16 }
if y == ys[index] then
-- update play state
emu.write(0x48, 2, emu.memType.nesDebug)
-- update vram
if index == 5 then
emu.write(0x49, 12, emu.memType.nesDebug)
else
emu.write(0x49, y-2, emu.memType.nesDebug)
end
else
emu.write(PIECE_Y_ADDR, y+2, emu.memType.nesDebug)
end
local state = emu.getState()
state["cpu.pc"] = 0x8972
emu.setState(state)
end
local function scene29_lock(addr, val)
local index = pieceIndex - 335
-- map of board memory address to mino
-- c0 c1 d0 d1, +2*type
local kinds = { 1, 2, 2, 3, 1 }
local kind = kinds[index]
local positions = {
{ 180, 182, 184, 186 },
{ 120, 140, 160, 162 },
{ 142, 144, 164, 166 },
{ 122, 124, 126, 146 },
{ 128, 148, 168, 188 },
}
for _, pos in ipairs(positions[index]) do
emu.write(0x400+pos, 0xC0+2*(kind - 1), emu.memType.nesDebug)
emu.write(0x401+pos, 0xC1+2*(kind - 1), emu.memType.nesDebug)
emu.write(0x40A+pos, 0xD0+2*(kind - 1), emu.memType.nesDebug)
emu.write(0x40B+pos, 0xD1+2*(kind - 1), emu.memType.nesDebug)
end
local state = emu.getState()
state["cpu.pc"] = 0x9A04
emu.setState(state)
end
local function scene29_lines(addr, val)
if pieceIndex ~= 340 then
return
end
emu.write(0x56, 8, emu.memType.nesDebug)
end
local function scene29_clean(addr, val)
if pieceIndex ~= 340 then
return
end
for i=1,200 do
emu.write(0x3FF+i, 0xEF, emu.memType.nesDebug)
end
-- write score
emu.write(0x55, 0x82, emu.memType.nesDebug)
emu.write(0x54, 0x80, emu.memType.nesDebug)
emu.write(0x53, 0x00, emu.memType.nesDebug)
end
local function drawMino(x, y, kind)
if x < 0 or y < 0 then
return
end
local color1 = 0xFFFEFF
local color2 = 0xA01ACC
local color3 = 0x0B4800
if kind == 1 then
-- color1 and color3
emu.drawRectangle(96+8*x, 48+8*y, 7, 7, color1, true)
emu.drawRectangle(96+8*x, 48+8*y, 1, 1, color3, true)
emu.drawRectangle(96+1+8*x, 48+1+8*y, 5, 5, color3, true)
elseif kind == 2 then
-- color2 and color3
emu.drawRectangle(96+8*x, 48+8*y, 7, 7, color2, true)
emu.drawRectangle(96+8*x, 48+8*y, 1, 1, color3, true)
emu.drawRectangle(96+1+8*x, 48+1+8*y, 2, 2, color3, true)
emu.drawRectangle(96+2+8*x, 48+2+8*y, 1, 1, color2, true)
elseif kind == 3 then
-- color1 and color3
emu.drawRectangle(96+8*x, 48+8*y, 7, 7, color1, true)
emu.drawRectangle(96+8*x, 48+8*y, 1, 1, color3, true)
emu.drawRectangle(96+1+8*x, 48+1+8*y, 2, 2, color3, true)
emu.drawRectangle(96+2+8*x, 48+2+8*y, 1, 1, color1, true)
end
end
local rotationMatrices = {
{1, 0, 0, 1},
{0, -1, 1, 0},
{-1, 0, 0, -1},
{0, 1, -1, 0},
}
local scene30_pentoOrientation = 1
local function scene30_resetOrient(addr, val)
scene30_pentoOrientation = 1
end
local function scene30_currentPiece(addr, val)
-- first check to see if piece has been rotated
local buttons = emu.read(NEW_BUTTONS_ADDR, emu.memType.nesDebug)
if buttons & 0x80 > 0 then
scene30_pentoOrientation = scene30_pentoOrientation + 1
if scene30_pentoOrientation == 5 then
scene30_pentoOrientation = 1
end
elseif buttons & 0x40 > 0 then
scene30_pentoOrientation = scene30_pentoOrientation - 1
if scene30_pentoOrientation == 0 then
scene30_pentoOrientation = 4
end
end
local x = emu.read(PIECE_X_ADDR, emu.memType.nesDebug)
local y = emu.read(PIECE_Y_ADDR, emu.memType.nesDebug)
local index = pieceIndex - 340
local mat = rotationMatrices[scene30_pentoOrientation]
-- TODO: check current piece and orientation
for _, pos in ipairs(pentos[index]) do
local xp = mat[1]*pos[1]+mat[2]*pos[2]
local yp = mat[3]*pos[1]+mat[4]*pos[2]
drawMino(x+xp, y+yp, pentoKinds[index])
end
end
local function scene30_drop(addr, val)
local index = pieceIndex - 340
local y = emu.read(PIECE_Y_ADDR, emu.memType.nesDebug)
local ys = { 18, 19, 18, 18, 17, 16, 16, 15, 15, 17 }
if y == ys[index] then
-- update play state
emu.write(0x48, 2, emu.memType.nesDebug)
-- update vram
emu.write(0x49, y-2, emu.memType.nesDebug)
else
emu.write(PIECE_Y_ADDR, y+1, emu.memType.nesDebug)
end
local state = emu.getState()
state["cpu.pc"] = 0x8972
emu.setState(state)
end
local function scene30_lock(addr, val)
local index = pieceIndex - 340
local positions = {
{ 170, 180, 190, 191, 192 },
{ 186, 195, 196, 197, 198 },
{ 174, 184, 185, 193, 194 },
{ 171, 173, 181, 182, 183 },
{ 168, 177, 178, 187, 188 },
{ 157, 158, 166, 167, 176 },
{ 150, 160, 161, 162, 172 },
{ 151, 152, 153, 163, 164 },
{ 154, 155, 156, 165, 175 },
{ 159, 169, 179, 189, 199 },
}
for _, pos in ipairs(positions[index]) do
emu.write(0x400+pos, 0x7A+pentoKinds[index], emu.memType.nesDebug)
end
local state = emu.getState()
state["cpu.pc"] = 0x9A04
emu.setState(state)
end
local function scene30_lines(addr, val)
if pieceIndex ~= 350 then
return
end
emu.write(0x56, 5, emu.memType.nesDebug)
end
local function scene30_clean(addr, val)
if pieceIndex ~= 350 then
return
end
for i=1,200 do
emu.write(0x3FF+i, 0xEF, emu.memType.nesDebug)
end
-- write score
emu.write(0x55, 0x85, emu.memType.nesDebug)
emu.write(0x54, 0x32, emu.memType.nesDebug)
emu.write(0x53, 0x00, emu.memType.nesDebug)
end
local scene31_rot = 0
local function scene31_drawBig()
local buttons = emu.read(NEW_BUTTONS_ADDR, emu.memType.nesDebug)
local y = emu.read(PIECE_Y_ADDR, emu.memType.nesDebug)
if buttons & 0x80 > 0 then
scene31_rot = 1 - scene31_rot
emu.write(0x6F1, 5, emu.memType.nesDebug)
end
if scene31_rot == 0 then
for i=y-2,y+1 do
for x=0,9 do
drawMino(x, i, 1)
end
end
else
for i=y-5,y+4 do
for x=3,6 do
drawMino(x, i, 1)
end
end
end
end
local function scene31_aeppoz(addr, val)
local state = emu.getState()
state["cpu.ps"] = state["cpu.ps"] & 0xFD
emu.setState(state)
end
local function scene33_noise(addr, val)
if pieceIndex < 365 then
return
end
local rng = math.random(7)
if rng == 1 then
emu.write(0x6F1, 0, emu.memType.nesDebug)
elseif rng <= 5 then
emu.write(0x6F1, 3, emu.memType.nesDebug)
else
emu.write(0x6F1, 5, emu.memType.nesDebug)
end
end
local function scene34_aeppoz(addr, val)
if pieceIndex ~= 380 then
return
end
local state = emu.getState()
state["cpu.ps"] = state["cpu.ps"] & 0xFD
emu.setState(state)
end
local function scene35_satan(addr, val)
if pieceIndex ~= 390 then
return
end
local state = emu.getState()
state["cpu.pc"] = 0xAC1C
state["cpu.a"] = 0xAA
state["cpu.x"] = 0xFF
state["cpu.y"] = 0
emu.setState(state)
end
local function scene36_score(addr, val)
if pieceIndex ~= 393 then
return
end
emu.write(0x55, 0x99, emu.memType.nesDebug)
emu.write(0x54, 0x99, emu.memType.nesDebug)
emu.write(0x53, 0x99, emu.memType.nesDebug)
end
local function scene36_crash(addr, val)
-- just set the pc to an $02 somewhere
if pieceIndex ~= 393 then
return
end
emu.breakExecution()
local state = emu.getState()
state["cpu.pc"] = 0x00
emu.setState(state)
end
local sceneIndex = START_SCENE
---@type Scene[]
local scenes = {
{ -- 0-12 lines
description = "seed 0 sequence, b3b tetris into pc",
moves = {
{ "J", { 1, 19, 2 }, { "L.L.L.L", "A.A" } },
{ "Z", { 1, 17, 0 }, { "L.L.L.L", "" } },
{ "O", { 4, 18, 0 }, { "L", "" } },
{ "S", { 6, 18, 0 }, { "R", "" } },
{ "L", { 6, 17, 0 }, { "R", "" } },
{ "I", { 0, 15, 1 }, { "L.L.L.L.L", "...A" } },
{ "S", { 4, 16, 0 }, { "L", "" } },
{ "T", { 2, 16, 0 }, { "L.L.L", "" } },
{ "J", { 8, 18, 1 }, { "R.R.R.R18L", ".A" } },
{ "Z", { 6, 15, 0 }, { "R", "" } },
{ "O", { 2, 14, 0 }, { "L.L.L", "" } },
{ "S", { 7, 15, 1 }, { "R.R", "A" } },
{ "L", { 3, 14, 3 }, { "L.L", ".B" } },
{ "I", { 8, 13, 1 }, { "R.R.R.R12..L", "...A" } },
{ "S", { 6, 13, 0 }, { "R", "" } },
{ "T", { 4, 13, 3 }, { "L", "...B" } },
{ "J", { 7, 11, 3 }, { ".R..R", "B" } },
{ "Z", { 1, 12, 0 }, { "L.L.L.L", "" } },
{ "O", { 6, 11, 0 }, { "...R", "" } },
{ "S", { 3, 11, 0 }, { "L.L", "" } },
{ "L", { 1, 10, 3 }, { "L.L.L.L.L10.R", "....B" } },
{ "I", { 0, 10, 1 }, { "L.L.L..L.L", "..A" } },
{ "S", { 6, 9, 0 }, { "..R", "" } },
{ "T", { 3, 10, 2 }, { "L.L", ".A.A" } },
{ "O", { 5, 8, 0 }, { "", "" } },
{ "T", { 2, 8, 0 }, { "L.L.L", "" } },
{ "J", { 7, 8, 0 }, { "6R.R", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "...A" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "...A" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "...A" } },
},
},
{ -- 12-16 lines
description = "standard pc 1",
moves = {
{ "O", { 1, 18, 0 }, { "L.L.L.L", "" } },
{ "L", { 2, 18, 3 }, { "L.L.L", "B" } },
{ "Z", { 4, 18, 0 }, { "L", "" } },
{ "O", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "L", { 7, 19, 2 }, { "R.R", "B.B" } },
{ "Z", { 7, 17 ,1 }, { "R.R", "A" } },
{ "J", { 6, 17, 3 }, { "R", "B" } },
{ "T", { 5, 17, 1 }, { "", "A" } },
{ "T", { 3, 16, 0 }, { "L.L", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "...A" } },
},
},
{ -- 16-20 lines
description = "standard pc 2",
moves = {
{ "J", { 1, 19, 2 }, { "L.L.L.L", ".A.A" } },
{ "Z", { 3, 18, 0 }, { "L.L", "" } },
{ "L", { 6, 19, 2 }, { "R", "B..B" } },
{ "T", { 5, 18, 2 }, { "", ".B.B" } },
{ "S", { 0, 17, 1 }, { "L.L.L.L.L", "....A" } },
{ "Z", { 2, 16, 0 }, { "L.L.L", "" } },
{ "L", { 8, 18, 1 }, { "R.R.R", "A" } },
{ "T", { 4, 16, 0 }, { "L", "" } },
{ "L", { 7, 16, 0 }, { "R.R", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "...A" } },
},
},
{ -- 20-24 lines
description = "start being lazy with controller",
moves = {
{ "O", { 1, 18, 0 }, { "L.L.L.L", "" } },
{ "O", { 3, 18, 0 }, { "L.L", "" } },
{ "Z", { 4, 18, 1 }, { "L", "A" } },
{ "S", { 6, 18, 0 }, { "R.R18L", "" } },
{ "J", { 8, 18, 1 }, { "R.R.R.R18L", "A" } },
{ "J", { 1, 17, 2 }, { "L.L.L.L", "A.A" } },
{ "S", { 7, 16, 0 }, { "R.R", "" } },
{ "L", { 5, 16, 0 }, { "", "" } },
{ "J", { 2, 16, 0 }, { "L.L.L", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "...A" } },
},
},
{ -- 24-28 lines
description = "start pizza",
moves = {
{ "O", { 1, 18, 0 }, { "L..L...L..L", "" } },
{ "T", { 3, 19, 2 }, { "L..L", ".A.A" } },
{ "Z", { 5, 18, 0 }, { "", "" } },
{ "T", { 6, 17, 2 }, { "R", "B...B" } },
{ "Z", { 7, 18, 0 }, { "R.R..R18L", ".B18A" } },
{ "S", { 4, 16, 0 }, { "L", "" } },
{ "O", { 1, 16, 0 }, { "L.L..L..L", "" } },
{ "J", { 2, 17, 3 }, { "L..L..L", "B" } },
{ "L", { 8, 17, 1 }, { "R..R..R", "A" } },
{ "I", { 9, 18, 1 }, { "R.R..R.R", "...A" } },
},
},
{ -- 28-32 lines
description = "pco",
moves = {
{ "T", { 9, 18, 1 }, { "R..R..R..R", "A" } },
{ "J", { 1, 19, 2 }, { "L..L..L.L", "A.A" } },
{ "S", { 8, 16, 0 }, { "R..R..R", "" } },
{ "L", { 1, 16, 0 }, { "L..L..L..L", "" } },
{ "Z", { 7, 18, 0 }, { "R16L...R.R", "A16A...A" } },
{ "O", { 2, 17, 0 }, { "L17L.L", "" } },
{ "I", { 3, 18, 1 }, { "L.L", "A" } },
{ "L", { 5, 18, 3 }, { "L18R", "B" } },
{ "T", { 6, 16, 0 }, { "R", "" } },
{ "I", { 4, 18, 1 }, { "L", "A" } },
},
},
{ -- 32-36 lines
description = "lucy hands?",
moves = {
{ "J", { 3, 19, 2 }, { "L.L", "A.A" } },
{ "O", { 1, 18, 0 }, { "L.L.L.L", "" } },
{ "S", { 2, 17, 1 }, { "L.L.L", "A" } },
{ "Z", { 5, 19, 0 }, { "", "" } },
{ "S", { 6, 18, 1 }, { "R", "A" } },
{ "Z", { 4, 16, 0 }, { "L", "" } },
{ "L", { 8, 18, 1 }, { "R.R.R", "A" } },
{ "I", { 7, 16, 0 }, { "R.R", "" } },
{ "O", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "...A" } },
},
},
{ -- 36-40 lines (NOTE: could double tuck?)
description = "7-bag again?",
moves = {
{ "T", { 5, 18, 3 }, { "", "B" } },
{ "J", { 1, 19, 2 }, { "L.L.L.L", "A.A" } },
{ "O", { 4, 18, 0 }, { "L", "" } },
{ "S", { 2, 17, 0 }, { "L.L.L", "" } },
{ "L", { 7, 19, 2 }, { "R.R.R19L", "B.B" } },
{ "Z", { 8, 17, 1 }, { "R.R", "A" } },
{ "J", { 3, 16, 0 }, { "L.L", "" } },
{ "T", { 6, 16, 0 }, { "R", "" } },
{ "O", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
},
{ -- 40-44 lines
description = "last 7-bag, midair drop",
moves = {
{ "Z", { 0, 18, 1 }, { "L.L.L.L.L", "A" } },
{ "L", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "S", { 3, 16, 0 }, { "L.L", "A16.A" } },
{ "T", { 2, 19, 2 }, { "18L..L.L", "B18B" } },
{ "Z", { 4, 18, 0 }, { "R17L..L", "A18A" } },
{ "T", { 5, 17, 1 }, { "R17L", "A" } },
{ "L", { 6, 18, 3 }, { "R", "B" } },
{ "S", { 7, 18, 1 }, { "R.R", "A" } },
{ "J", { 7, 16, 0 }, { "R.R", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
callbacks = {
{ addr = 0x8965, fn = scene9_midair },
},
},
{ -- 44-48 lines
description = "repeated setup 1",
moves = {
{ "I", { 4, 18, 1 }, { "L", "A" } },
{ "L", { 0, 18, 3 }, { "L.L.L.L.L", ".B" } },
{ "J", { 8, 18, 1 }, { "R.R.R", ".A" } },
{ "Z", { 2, 18, 0 }, { "L.L.L", "" } },
{ "S", { 6, 18, 0 }, { "R", "" } },
{ "Z", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "S", { 7, 16, 0 }, { "R.R", "" } },
{ "L", { 3, 17, 1 }, { "L.L", ".A" } },
{ "J", { 5, 17, 3 }, { "", "B" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", ".A" } },
},
},
{ -- 48-52 lines
description = "repeated setup 2",
moves = {
{ "I", { 4, 18, 1 }, { "L", "A" } },
{ "L", { 0, 18, 3 }, { "L.L.L.L.L", "B" } },
{ "J", { 8, 18, 1 }, { "R.R.R", "A" } },
{ "Z", { 2, 18, 0 }, { "L.L.L", "" } },
{ "S", { 6, 18, 0 }, { "R", "" } },
{ "Z", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "S", { 7, 16, 0 }, { "R.R", "" } },
{ "L", { 3, 17, 1 }, { "L.L", "A" } },
{ "J", { 5, 17, 3 }, { "", "B" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", ".A" } },
},
},
{ -- 52-56 lines
description = "repeated setup 3",
moves = {
{ "I", { 4, 18, 1 }, { "L", "A" } },
{ "L", { 0, 18, 3 }, { "L.L.L.L.L", ".B" } },
{ "J", { 8, 18, 1 }, { "R.R.R", ".A" } },
{ "Z", { 2, 18, 0 }, { "L.L.L", "" } },
{ "S", { 6, 18, 0 }, { "R", "" } },
{ "Z", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "S", { 7, 16, 0 }, { "R.R", "" } },
{ "L", { 3, 17, 1 }, { "L.L", ".A" } },
{ "J", { 5, 17, 3 }, { "", "B" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", ".A" } },
},
},
{ -- 56-60 lines
description = "funny sequence",
moves = {
{ "O", { 6, 18, 0 }, { "R", "" } },
{ "O", { 1, 18, 0 }, { "L.L.L.L", "" } },
{ "O", { 6, 16, 0 }, { "R", "" } },
{ "I", { 2, 18, 1 }, { "L.L.L", "A" } },
{ "O", { 4, 18, 0 }, { "L", "" } },
{ "O", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "O", { 8, 18, 0 }, { "R.R.R", "" } },
{ "O", { 4, 16, 0 }, { "L", "" } },
{ "O", { 8, 16, 0 }, { "R.R.R", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
},
{ -- 60-64 lines (pause and replace piece with bar)
description = "pausing replaces 10th piece with a longbar",
moves = {
{ "T", { 6, 18, 0 }, { "R", "" } },
{ "J", { 0, 18, 3 }, { "L.L.L.L.L", "B" } },
{ "Z", { 5, 16, 0 }, { "", "" } },
{ "O", { 2, 18, 0 }, { "L.L18L", "" } },
{ "J", { 8, 18, 1 }, { "R.R.R.R18L", "A" } },
{ "L", { 4, 18, 3 }, { "L.L.L16R18R", "B" } },
{ "T", { 7, 16, 0 }, { "R.R", "" } },
{ "L", { 3, 18, 1 }, { "L.L", "A" } },
{ "I", { 2, 16, 0 }, { "L.L.L", "" } },
{ "O", { 9, 18, 0 }, { "R.R.R.R", "" } },
},
callbacks = {
{ addr = 0xA37F, fn = scene14_forcePause },
{ addr = 0xA3DF, fn = scene14_swapBar },
}
},
{ -- 64-68 lines
description = "keyboard tap",
moves = {
{ "S", { 1, 18, 0 }, { "L.L.L.L", "" } },
{ "I", { 4, 19, 0 }, { "19L", "" } },
{ "O", { 4, 17, 0 }, { "L", "" } },
{ "T", { 0, 17, 3 }, { "L.L.L.L.L", "B" } },
{ "T", { 2, 16, 0 }, { "L.L.L", "" } },
{ "T", { 6, 18, 0 }, { "R", "" } },
{ "J", { 8, 18, 1 }, { "R.R.R.R18L", "A" } },
{ "Z", { 5, 16, 0 }, { "", "" } },
{ "T", { 7, 16, 0 }, { "R.R", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
},
{ -- 68-72 lines
-- TODO: video editing
description = "keyboard roll/upside down",
moves = {
{ "J", { 1, 19, 2 }, { "L.L.L.L", "A.A" } },
{ "I", { 5, 19, 0 }, { "", "" } },
{ "O", { 8, 18, 0 }, { "R.R.R", "" } },
{ "S", { 6, 17, 0 }, { "R", "" } },
{ "O", { 3, 17, 0 }, { "L.L", "" } },
{ "Z", { 4, 17, 1 }, { "L", "A" } },
{ "S", { 0, 17, 1 }, { "L.L.L.L.L", "A" } },
{ "I", { 3, 16, 0 }, { "L.L", "" } },
{ "J", { 7, 16, 0 }, { "R.R", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
},
{ -- 72-76 lines
description = "keyboard spam???",
moves = {
{ "S", { 1, 18, 0 }, { "L.L.L.L", "" } },
{ "S", { 2, 16, 0 }, { "L.L.L", "" } },
{ "S", { 3, 18, 0 }, { "17..L.L", "" } },
{ "S", { 4, 16, 0 }, { "16L", "" } },
{ "S", { 5, 18, 0 }, { "R.R17.L..L", "" } },
{ "S", { 6, 16, 0 }, { "R.R16L", "" } },
{ "J", { 7, 18, 1 }, { "R.R.R.R17L..L", "A" } },
{ "I", { 8, 18, 1 }, { "R.R.R", "A" } },
{ "J", { 0, 17, 3 }, { "L.L.L.L.L", "B" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
},
{ -- 76-80 lines
description = "go back to controller",
moves = {
{ "I", { 0, 18, 1 }, { "L.L.L.L.L", "A" } },
{ "I", { 1, 18, 1 }, { "L.L.L.L", "A" } },
{ "I", { 2, 18, 1 }, { "L.L.L", "A" } },
{ "I", { 8, 18, 1 }, { "R.R.R", "A" } },
{ "I", { 7, 18, 1 }, { "R.R", "A" } },
{ "I", { 5, 19, 0 }, { "", "" } },
{ "I", { 5, 18, 0 }, { "", "" } },
{ "I", { 5, 17, 0 }, { "", "" } },
{ "I", { 5, 16, 0 }, { "", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
},
{ -- 80-84 lines
description = "normal noro",
moves = {
{ "O", { 1, 18, 0 }, { "L.L.L.L", "" } },
{ "L", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "T", { 2, 17, 0 }, { "L17L.L", "" } },
{ "S", { 4, 18, 0 }, { "17.L..L", "" } },
{ "I", { 6, 19, 0 }, { "R.R19L", "" } },
{ "T", { 5, 17, 0 }, { "", "" } },
{ "I", { 5, 16, 0 }, { "", "" } },
{ "J", { 7, 18, 0 }, { "R.R.R18L", "" } },
{ "O", { 8, 16, 0 }, { "R.R.R", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "" } },
},
callbacks = {
{ addr = 0x98D1, fn = scene19_rotatedBar },
}
},
{ -- 84-88 lines
description = "funny noro",
moves = {
{ "L", { 1, 19, 2 }, { "L.L.L.L", "" } },
{ "L", { 0, 17, 3 }, { "L.L.L.L.L", "" } },
{ "O", { 2, 16, 0 }, { "L.L.L", "" } },
{ "J", { 5, 18, 1 }, { "", "" } },
{ "Z", { 3, 18, 1 }, { "L.L", "" } },
{ "L", { 4, 16, 0 }, { "L", "" } },
{ "T", { 7, 19, 2 }, { "R.R", "" } },
{ "J", { 6, 17, 3 }, { "R", "" } },
{ "T", { 8, 17, 1 }, { "R.R.R.R17L", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "" } },
},
callbacks = {
{ addr = 0x98D1, fn = scene20_prerotations },
}
},
{ -- 88-92 lines (deep drop)
description = "deep dropped O piece",
moves = {
{ "I", { 7, 19, 0 }, { "R.R", "" } },
{ "Z", { 0, 18, 1 }, { "L.L.L.L.L", "A" } },
{ "T", { 2, 19, 2 }, { "L.L19L", "A.A" } },
{ "L", { 8, 17, 1 }, { "R.R.R", "A" } },
{ "T", { 4, 18, 1 }, { "L", "A" } },
{ "S", { 3, 16, 0 }, { "L.L", "" } },
{ "L", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "J", { 5, 17, 3}, { "", "B" } },
{ "O", { 7, 17, 0 }, { "R.R", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
callbacks = {
{ addr = 0x8965, fn = scene21_deepDrop },
},
},
{ -- 92-96 lines
description = "quad tuck",
moves = {
{ "O", { 1, 18, 0 }, { "L.L.L.L", "" } },
{ "L", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "Z", { 2, 17, 0 }, { "L.L17L", "A17A" } },
{ "Z", { 4, 17, 0 }, { "L", "" } },
{ "I", { 5, 16, 0 }, { "", "" } },
{ "I", { 4, 19, 0 }, { "R.R.R19L.L.L......L", "A17A" } },
{ "L", { 7, 19, 2 }, { "R.R", "B18B" } },
{ "Z", { 6, 17, 0 }, { "R.R17L", "A17A" } },
{ "O", { 8, 16, 0 }, { "R.R.R", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
callbacks = {
{ addr = 0x8980, fn = scene22_quadTuck },
},
},
{ -- 96-100 lines (srs)
description = "srs spins",
-- not including the final spin because callbacks handle that
moves = {
{ "J", { 8, 18, 1 }, { "R.R.R", "A" } },
{ "T", { 4, 18, 3 }, { "L", "B" } },
{ "J", { 2, 18, 0 }, { "L.L.L", "" } },
{ "S", { 6, 18, 0 }, { "", "A17.A" } },
{ "J", { 1, 19, 2 }, { "L.L.L.L.L", "B" } },
{ "O", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "T", { 7, 16, 0 }, { "R.R", "" } },
{ "O", { 3, 16, 0 }, { "L.L", "" } },
{ "Z", { 5, 16, 0 }, { "", "A" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
callbacks = {
{ addr = 0x81CF, fn = scene23_srs },
{ addr = 0x98D1, fn = scene23_preArs },
},
},
{ -- 100-104 lines (tgm)
description = "tgm mechanics",
-- inputs will be handled by callbacks
moves = {
{ "J", { 4, 18, 1 }, { "", "" } }, --pre-rotate a
{ "O", { 6, 18, 0 }, { ".......R", "" } },
{ "S", { 2, 18, 0 }, { "LLL..R", "" } },
{ "Z", { 0, 18, 1 }, { "LLLLL", "" } }, -- pre-rotate A, nudge left
{ "O", { 8, 18, 0 }, { "RRRR..L", "" } },
{ "S", { 3, 16, 0 }, { "L.", "" } },
{ "J", { 6, 17, 2 }, { "...R", "......B" } }, -- pre-rotate B, nudge left
{ "L", { 1, 16, 0 }, { "LLL", "" } },
{ "J", { 7, 16, 0 }, { "RRRR....L", "" } },
{ "I", { 9, 18, 1 }, { "RRRRR", "" } }, -- pre-rotate A
},
-- TODO: add rotation sfx for tgm (along with piece sfx)
callbacks = {
{ addr = 0x98D1, fn = scene24_ars },
{ addr = 0x81CF, fn = scene24_tgm },
}
},
{ -- 104-124 lines (tetris effect zone)
description = "tetris effect zone",
moves = {
{ "T", { 8, 19, 2 }, { "R.R.R", "B.B" } },
{ "O", { 1, 18, 0 }, { "L.L.L.L", "" } },
{ "I", { 4, 19, 0 }, { "L", "" } },
{ "T", { 9, 17, 1 }, { "R.R.R.R", "A" } },
{ "O", { 5, 17, 0 }, { "", "" } },
{ "Z", { 2, 17, 0 }, { "L.L.L", "" } },
{ "L", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "L", { 4, 16, 0 }, { "L", "" } },
{ "J", { 7, 17, 3 }, { "R.R", "B" } },
{ "I", { 2, 15, 0 }, { "R12L.L.L.L", "A11A" } },
{ "L", { 8, 15, 2 }, { "R.R.R", "B.B" } },
{ "Z", { 4, 14, 0 }, { "L", "" } },
{ "O", { 8, 13, 0 }, { "R.R.R", "" } },
{ "T", { 9, 12, 1 }, { "R.R.R.R", "A" } },
{ "T", { 7, 11, 3 }, { "R.R", "B" } },
{ "S", { 2, 13, 0 }, { "L.L.L", "" } },
{ "Z", { 0, 13, 1 }, { "L.L.L.L.L", "A" } },
{ "T", { 5, 13, 1 }, { "", "A" } },
{ "L", { 3, 12, 2 }, { "L.L", "A.A" } },
{ "J", { 9, 9, 1 }, { "R.R.R.R", "A" } },
{ "T", { 2, 11, 2 }, { "L.L.L", "A.A" } },
{ "I", { 0, 11, 1 }, { "L.L.L.L.L", "A" } },
{ "O", { 4, 9, 0 }, { "L", "" } },
{ "L", { 7, 8, 3 }, { "R.R", "B" } },
{ "Z", { 8, 7, 1 }, { "R.R.R", "A" } },
{ "Z", { 1, 9, 1 }, { "L.L.L.L", "A" } },
{ "J", { 4, 7, 1 }, { "L", "A" } },
{ "S", { 8, 4, 0 }, { "R.R.R", "" } },
{ "L", { 8, 4, 0 }, { "R.R.R", "" } },
{ "S", { 1, 7, 0 }, { "L.L.L.L", "" } },
{ "T", { 0, 6, 3 }, { "L.L.L.L.L", "B" } },
{ "S", { 2, 6, 1 }, { "L.L.L", "A" } },
{ "S", { 0, 4, 1 }, { "L.L.L.L.L", "A" } },
{ "Z", { 3, 4, 0 }, { "L.L", "" } },
{ "O", { 6, 10, 0 }, { "R.............................U", "" } }, -- activate zone, disable gravity. double
{ "I", { 6, 18, 1 }, { "R.....U", "A" } }, -- tetris
{ "L", { 5, 8, 3 }, { "......U", "B" } }, -- single
{ "T", { 2, 3, 2 }, { "L.L.L.......U", "A.A" } },
{ "O", { 9, 2, 0 }, { "R.R.R.R....U", "" } },
{ "O", { 9, 0, 0 }, { "R.R.R.R......U", "" } },
{ "I", { 6, 14, 1 }, { "R........U", "A" } }, -- tetris
{ "L", { 6, 7, 1 }, { "R........U", "A" } }, -- triple
{ "I", { 7, 2, 1 }, { "R.R.........U", "A" } },
{ "J", { 6, 4, 1 }, { "....R...........U", "A" } }, -- single
{ "L", { 4, 3, 3 }, { "L..........U", "B" } }, -- single
{ "L", { 0, 1, 3 }, { "L.L.L.L.L............U", "...B" } },
{ "O", { 2, 0, 0 }, { "L.L.L..............U", "" } },
{ "Z", { 5, 2, 1 }, { "...........................U", "A" } },
{ "J", { 3, 1, 3 }, { "..............L............L......U", "....B" } }, -- single
{ "S", { 5, 0, 0 }, { ".................................U", "" } }, -- double
},
-- TODO: callbacks
callbacks = {
{ addr = 0x8984, fn = scene25_zeroGravity },
{ addr = 0x97D3, fn = scene25_lineClearBlocks },
{ addr = 0x97E8, fn = scene25_lineClearBlocks },
{ addr = 0x81D8, fn = scene25_hardDrop },
{ addr = 0x9695, fn = scene25_disableSfx },
{ addr = 0x9A9B, fn = scene25_disableSfx },
{ addr = 0x9AF2, fn = scene25_disableSfx },
{ addr = 0x9BF2, fn = scene25_disableSfx },
{ addr = 0x9A74, fn = scene25_zoneClears },
{ addr = 0x9CA4, fn = scene25_clean },
{ addr = 0x8005, fn = scene25_inverter },
},
-- TODO: video editing
-- TODO: don't update score
},
{ -- 124-128 lines (move bad pieces out)
description = "move bad pieces outside the playing field",
moves = {
{ "T", { 5, 19, 2 }, { "", "A.A" } },
{ "O", { 7, 17, 0 }, { "R.R", "" } },
{ "I", { 2, 19, 0 }, { "L.L.L", "" } },
{ "J", { 8, 18, 1 }, { "R.R.R.R18L", "A" } },
{ "I", { 7, 16, 0 }, { "R.R", "" } },
{ "L", { 0, 17, 3 }, { "L.L.L.L.L", "B" } },
{ "O", { -2, 19, 0 }, { "L.L.L.L.L.L.L", "" } }, -- bad
{ "S", { 4, 17, 0 }, { "L.L17R", "" } },
{ "O", { -1, 19, 0 }, { "L.L.L.L.L.L", "" } }, -- bad
{ "J", { -2, 19, 0 }, { "L.L.L.L.L6L.L.L.L", "" } }, -- bad
{ "S", { 1, 17, 1 }, { "L.L.L.L", "A" } },
{ "T", { 3, 16, 0 }, { "L.L", "" } },
{ "Z", { 10, 19, 1 }, { "R.R.R.R.R7R.R", "A" } }, -- bad
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
callbacks = {
{ addr = 0x948B, fn = scene26_noCollision },
{ addr = 0x99A2, fn = scene26_noLock },
{ addr = 0x9A6B, fn = scene26_hack },
{ addr = 0x9B05, fn = scene27_garbage },
{ addr = 0x9B09, fn = scene27_garbage },
},
},
{ -- 128-132 lines (garbage)
description = "4 lines of garbage",
moves = {
{ "I", { 2, 18, 1 }, { "L.L.L", "A" } },
},
},
{ -- 132-136 lines (watermelon)
description = "watermelon",
moves = {
{ "T", { 1, 19, 2 }, { "L.L.L.L", "A.A" } },
{ "I", { 5, 19, 0 }, { "", "" } },
{ "J", { 0, 17, 3 }, { "L.L.L.L.L", "B" } },
{ "Z", { 2, 17, 0 }, { "L.L17L", "" } },
{ "T", { 5, 18, 2 }, { "", "B.B" } },
{ "O", { 8, 18, 0 }, { "R.R.R", "" } },
{ "Z", { 3, 16, 0 }, { "L.L", "" } },
{ "O", { 8, 16, 0 }, { "R.R.R", "" } },
{ "J", { 5, 16, 0 }, { "", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
},
-- strategy: use a 10x5 board, edit score/lines via callback
{ -- 136-144 lines (big pieces)
description = "big pieces",
moves = {
{ "Ib", { 2, 19, 0 }, { "", "" } },
{ "Lb", { 0, 17, 3 }, { "L......L", "...B" } },
{ "Zb", { 2, 17, 0 }, { "", "" } },
{ "Jb", { }, { "", "" } },
{ "Ib", { 4, 18, 1 }, { "R.......R", "....A" } },
},
callbacks = {
{ addr = 0x8A0A, fn = scene29_currentPiece },
{ addr = 0x89F2, fn = scene29_shiftLeft },
{ addr = 0x89DD, fn = scene29_shiftRight },
{ addr = 0x8960, fn = scene29_drop },
{ addr = 0x99BE, fn = scene29_lock },
{ addr = 0x9BA6, fn = scene29_lines },
{ addr = 0x9CA4, fn = scene29_clean },
},
-- TODO: fix visual bugs with video editing
},
-- PENTO STRATEGY
-- hijack stage next piece/current piece sprite routines
-- use video editing for the first preview
-- callback to fix piece stats
-- probably a callback to fake the line clear anim somehow
{ -- 144-149 lines (pentos)
description = "pentos",
moves = {
{ "Vp", { 1, 18, 2 }, { "L.L.L.L", "A.A" } },
{ "Yp", { 6, 19, 2 }, { "R", "B..B" } },
{ "Fp", { 4, 18, 1 }, { "L", "A" } },
{ "Up", { 2, 18, 2 }, { "L.L.L", "A.A" } },
{ "Pp", { 8, 17, 1 }, { "R.R.R", "A" } },
{ "Wp", { 7, 16, 3 }, { "R.R", "B" } },
{ "Zp", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "Np", { 2, 15, 0 }, { "L.L.L", "" } },
{ "Tp", { 5, 15, 0 }, { "", "" } },
{ "Ip", { 9, 17, 1 }, { "R.R.R.R", "A" } },
},
callbacks = {
{ addr = PICK_PIECE_END_ADDR, fn = scene30_resetOrient },
{ addr = 0x8A0A, fn = scene30_currentPiece },
{ addr = 0x8960, fn = scene30_drop },
{ addr = 0x99BE, fn = scene30_lock },
{ addr = 0x9B58, fn = scene30_lines },
{ addr = 0x9CA4, fn = scene30_clean },
},
-- TODO: fix visual bugs with video editing
-- TODO: use existing callbacks for staging current/next sprites
-- hijack collisions or smth, no piece stats
},
-- strategy: have aeppoz equivalent, add minos in post
-- would be really funny to rotate the piece lmao
{ -- 149-153 lines
description = "4x10 piece",
moves = {
{ "O", { 5, 18, 0 }, { "", "9A.....A" } },
},
-- TODO: video editing (rotate it twice)
callbacks = {
{ addr = 0x9A93, fn = scene31_aeppoz },
{ addr = 0x81D8, fn = scene31_drawBig },
},
},
{ -- 153-157 lines (charcoal, normal?)
description = "charcoal",
moves = {
{ "I", { 2, 19, 0 }, { "L.L.L", "" } },
{ "Z", { 4, 18, 0 }, { "L", "" } },
{ "T", { 6, 18, 0 }, { "R", "" } },
{ "J", { 8, 18, 1 }, { "R.R.R.R18L", "A" } },
{ "S", { 7, 16, 0 }, { "R.R", "" } },
{ "S", { 5, 16, 0 }, { "", "" } },
{ "L", { 1, 18, 2 }, { "L.L.L.L", "A.A" } },
{ "T", { 3, 16, 0 }, { "L.L", "" } },
{ "O", { 1, 16, 0 }, { "L.L.L.L", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
},
{ -- 157-161 lines (everything invisible)
description = "everything is invisible",
moves = {
{ "I", { 0, 18, 1 }, { "L.L.L.L.L", "A" } },
{ "I", { 1, 18, 1 }, { "L.L.L.L", "A" } },
{ "I", { 2, 18, 1 }, { "L.L.L", "A" } },
{ "I", { 8, 18, 1 }, { "R.R.R", "A" } },
{ "I", { 7, 18, 1 }, { "R.R", "A" } },
{ "I", { 5, 19, 0 }, { "", "" } },
{ "I", { 5, 18, 0 }, { "", "" } },
{ "I", { 5, 17, 0 }, { "", "" } },
{ "I", { 5, 16, 0 }, { "", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
callbacks = {
{ addr = 0x81D8, fn = scene33_noise },
},
},
{ -- 161-165 lines (8 wide)
description = "set up an 8-wide, then clear a tetris somehow",
moves = {
{ "T", { 1, 19, 2 }, { "L.L.L.L", "A.A" } },
{ "T", { 4, 19, 2 }, { "L", "A.A" } },
{ "O", { 7, 18, 0 }, { "R.R", "" } },
{ "Z", { 2, 17, 0 }, { "L.L.L", "" } },
{ "Z", { 3, 16, 0 }, { "L.L", "" } },
{ "J", { 0, 17, 3 }, { "L.L.L.L.L", "B" } },
{ "L", { 6, 17, 0 }, { "R", "" } },
{ "I", { 6, 16, 0 }, { "R", "" } },
{ "S", { 8, 18, 1 }, { "R.R.R", "A" } },
},
callbacks = {
{ addr = 0x9A93, fn = scene34_aeppoz },
},
},
{ -- 165-169 lines (satan spawn)
description = "trigger satan spawn after clearing",
moves = {
{ "O", { 4, 18, 0 }, { "L", "" } },
{ "J", { 1, 19, 2 }, { "L.L.L.L", "A.A" } },
{ "Z", { 1, 17, 0 }, { "L.L.L.L", "" } },
{ "L", { 7, 19, 2 }, { "R.R", "B.B" } },
{ "L", { 6, 18, 0 }, { "R", "" } },
{ "I", { 4, 17, 0 }, { "L", "" } },
{ "O", { 8, 16, 0 }, { "R.R.R", "" } },
{ "I", { 2, 16, 0 }, { "L.L.L", "" } },
{ "J", { 5, 16, 0 }, { "", "" } },
{ "I", { 9, 18, 1 }, { "R.R.R.R", "A" } },
},
callbacks = {
{ addr = 0x9CA4, fn = scene35_satan },
},
},
{ -- 169-170 lines (game crash)
description = "game crash!",
moves = {
{ "J", { 1, 19, 2 }, { "L.L.L.L", "A.A" } },
{ "L", { 8, 19, 2 }, { "R.R.R", "B.B" } },
{ "I", { 5, 19, 0 }, { "", "" } },
{ "S", { 5, 18, 0 }, { "", "" } },
},
callbacks = {
{ addr = 0x9CA4, fn = scene36_score },
{ addr = 0x9B03, fn = scene36_crash },
},
},
}
local function addToInputs(inputs, frame, val)
if inputs[frame] then
inputs[frame] = inputs[frame] + val
else
inputs[frame] = val
end
end
-- strategy: can use same logic for both shifts and rotates
-- think state machine
-- start row 0 in "input mode", handle non-numbers normally
-- if number received, switch to "row mode"
-- if non-number receieved in row mode, set row, then process as normal
---@param inputs [string, string]
---@param gravity number
---@return table<number, number> a map from frame number to input value
local function parseInputs(inputs, gravity)
local dpad = inputs[1]
local ab = inputs[2]
local inputMap = {}
local frame = 1
-- if row mode, keep parsing numbers
local rowMode = false
local row = 1
for i = 1, #dpad do
local c = dpad:sub(i, i)
if rowMode then
if tonumber(c) then
row = 10*row + tonumber(c)
else
rowMode = false
frame = row*gravity + 1
-- check as normal
if c == "L" then
addToInputs(inputMap, frame, 0x02)
elseif c == "R" then
addToInputs(inputMap, frame, 0x01)
elseif c == "U" then
addToInputs(inputMap, frame, 0x08)
elseif c == "D" then
addToInputs(inputMap, frame, 0x04)
end
end
else
local d = tonumber(c)
if d then
rowMode = true
row = d
elseif c == "L" then
addToInputs(inputMap, frame, 0x02)
elseif c == "R" then
addToInputs(inputMap, frame, 0x01)
elseif c == "U" then
addToInputs(inputMap, frame, 0x08)
elseif c == "D" then
addToInputs(inputMap, frame, 0x04)
end
end
frame = frame + 1
end
frame = 1
for i = 1, #ab do
local c = ab:sub(i, i)
if rowMode then
if tonumber(c) then
row = 10*row + tonumber(c)
else
rowMode = false
frame = row*gravity + 1
-- check as normal
if c == "A" then
addToInputs(inputMap, frame, 0x80)
elseif c == "B" then
addToInputs(inputMap, frame, 0x40)
end
end
else
local d = tonumber(c)
if d then
rowMode = true
row = d
elseif c == "A" then
addToInputs(inputMap, frame, 0x80)
elseif c == "B" then
addToInputs(inputMap, frame, 0x40)
end
end
frame = frame + 1
end
return inputMap
end
-- how to use: maintain a counter that's reset at the start of each piece
-- build derived structures from scenes
---@type Move[]
local moveSequence = {}
for _, scene in ipairs(scenes) do
for _, move in ipairs(scene.moves) do
table.insert(moveSequence, move)
end
end
-- basically if piece_index > this number, increment the scene index
local sceneLastPieceIndexes = {}
local tmpScenePieceIndex = 0
for i, scene in ipairs(scenes) do
tmpScenePieceIndex = tmpScenePieceIndex + #scene.moves
sceneLastPieceIndexes[i] = tmpScenePieceIndex
end
-- cache input lookup for tas features
---@type table<number, number>
local pieceInputs = {}
-- global callback management
local activeCallbacks = {}
---@param callbacks Callback[]
local function addSceneCallbacks(callbacks)
for _, cb in ipairs(callbacks) do
local id = emu.addMemoryCallback(cb.fn, emu.callbackType.exec, cb.addr)
activeCallbacks[id] = cb.addr
end
end
local function removeSceneCallbacks()
for id, addr in pairs(activeCallbacks) do
emu.removeMemoryCallback(id, emu.callbackType.exec, addr)
end
activeCallbacks = {}
end
-- init code
local glitchedColors = {
0x69, 0xC9, 0x14, 0x30, -- neon night
0xC9, 0x00, 0xD0, 0x0E, -- charcoal
0xA5, 0xA4, 0xC9, 0x1C, -- squid
0xD0, 0x30, 0xA9, 0x00, -- lime factory?
}
for i, b in ipairs(glitchedColors) do
emu.write(0x984C-1+i, b, emu.memType.nesDebug)
end
-- write in watermelon at the end
local watermelon = { 0x60, 0xE6, 0x69, 0xA5 }
for i, b in ipairs(watermelon) do
emu.write(0x984C-1+4*9+i, b, emu.memType.nesDebug)
end
-- callbacks
-- resets state when starting a new game
local function updateGameMode()
local newGameMode = emu.read(GAME_MODE_ADDR, emu.memType.nesDebug)
if newGameMode == 4 and gameMode == 3 then
sceneIndex = START_SCENE
pieceIndex = -1
if sceneIndex > 1 then
pieceIndex = sceneLastPieceIndexes[sceneIndex - 1] - 1
end
pieceFrame = 1
removeSceneCallbacks()
if scenes[sceneIndex].callbacks ~= nil then
addSceneCallbacks(scenes[sceneIndex].callbacks)
end
end
gameMode = newGameMode
end
local function printStatus()
if gameMode ~= 4 then
return
end
emu.drawString(0, 0, "Scene " .. sceneIndex, 0xFFFFFF, 0x000000)
emu.drawString(0, 8, "Piece " .. pieceIndex, 0xFFFFFF, 0x000000)
emu.drawString(0, 16, "Next " .. sceneLastPieceIndexes[sceneIndex], 0xFFFFFF, 0x000000)
emu.drawString(0, 24, "Frame " .. pieceFrame, 0xFFFFFF, 0x000000)
emu.drawString(48, 0, scenes[sceneIndex].description, 0xFFFFFF, 0x000000)
emu.drawString(40, 216, "NOW", 0xFFFFFF, 0x000000)
emu.drawString(40, 224, moveSequence[pieceIndex][3][1],
0xFFFFFF, 0x000000)
emu.drawString(40, 232, moveSequence[pieceIndex][3][2],
0xFFFFFF, 0x000000)
if pieceIndex < #moveSequence then
emu.drawString(120, 216, "NEXT", 0xFFFFFF, 0x000000)
emu.drawString(120, 224, moveSequence[pieceIndex+1][3][1],
0xFFFFFF, 0x000000)
emu.drawString(120, 232, moveSequence[pieceIndex+1][3][2],
0xFFFFFF, 0x000000)
end
end
emu.addEventCallback(updateGameMode, emu.eventType.nmi)
emu.addEventCallback(printStatus, emu.eventType.nmi)
local function getGravity()
local levelNumber = emu.read(LEVEL_NUMBER_ADDR, emu.memType.nesDebug)
if levelNumber == 18 then
return 3
end
return 2
end
-- sets piece sequence
local function setPiece(addr, val)
local newPieceIndex = pieceIndex + 2
emu.log("generating piece " .. newPieceIndex)
local piece = moveSequence[newPieceIndex][1]
if piece ~= "" then
local pieceId = PIECE_ID_TABLE[piece]
if pieceId == nil then
pieceId = 0x13
end
local state = emu.getState()
state["cpu.a"] = pieceId
emu.setState(state)
end
pieceIndex = pieceIndex + 1
pieceFrame = 1
-- update inputmap
if pieceIndex > 0 then
emu.log("setting inputs for piece " .. pieceIndex)
pieceInputs = parseInputs(moveSequence[pieceIndex][3], getGravity())
end
-- callback management
if pieceIndex > sceneLastPieceIndexes[sceneIndex] then
removeSceneCallbacks()
sceneIndex = sceneIndex + 1
if scenes[sceneIndex] == nil then
return
end
if scenes[sceneIndex].callbacks ~= nil then
addSceneCallbacks(scenes[sceneIndex].callbacks)
end
end
end
-- TODO: add callback at 98D1 to jump to 98D4 if piece is illegal
local function skipIncrement(addr, val)
local piece = moveSequence[pieceIndex][1]
if piece ~= "" then
local pieceId = PIECE_ID_TABLE[piece]
if pieceId == nil then
local state = emu.getState()
state["cpu.pc"] = 0x98D4
emu.setState(state)
end
end
end
local function emptyPiecePatch(addr, val)
local state = emu.getState()
local currentPiece = emu.read(CURRENT_PIECE_ADDR, emu.memType.nesDebug)
if addr == 0x98CF then
if state["cpu.x"] == 0x13 then
state["cpu.a"] = 0x13
end
elseif currentPiece == 0x13 then
state["cpu.a"] = 0x13
end
emu.setState(state)
end
emu.addMemoryCallback(
setPiece,
emu.callbackType.exec,
PICK_PIECE_END_ADDR
)
emu.addMemoryCallback(skipIncrement, emu.callbackType.exec, 0x98D1)
emu.addMemoryCallback(emptyPiecePatch, emu.callbackType.exec, 0x98CF)
-- fix rotations
emu.addMemoryCallback(emptyPiecePatch, emu.callbackType.exec, 0x88C0)
emu.addMemoryCallback(emptyPiecePatch, emu.callbackType.exec, 0x88DA)
local function updateInputs(addr, val)
-- TODO: disable if player has control
local buttons = pieceInputs[pieceFrame]
if buttons == nil then
buttons = 0
end
if sceneIndex > 0 then
emu.write(NEW_BUTTONS_ADDR, buttons, emu.memType.nesDebug)
emu.write(HELD_BUTTONS_ADDR, buttons, emu.memType.nesDebug)
end
pieceFrame = pieceFrame + 1
end
emu.addMemoryCallback(
updateInputs,
emu.callbackType.exec,
GAME_LOOP_START_ADDR
)
-- local function print()
-- emu.drawString(0, 0, "test", 0xFFFFFF, 0x000000)
-- end
--
-- emu.addEventCallback(print, emu.eventType.nmi)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment