Last active
April 14, 2025 16:40
-
-
Save fractal161/b644413f71d09a178df13b9957b77367 to your computer and use it in GitHub Desktop.
Passing Tetris in 7 Minutes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[=[ | |
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