Here is my 10th (and the 12th overall) TAS of NES Ghosts 'n Goblins. It is 257 frames faster than my previous TAS, which I submitted exactly 3141 days ago. My first attempt was this site's first submission.
This game is notorious for its extreme difficulty, and it is just as challenging to TAS it optimally. It is like playing a game of chess against an opponent so unforgiving that you always have to calculate seven moves ahead.
I documented in great detail almost every aspect of this game/TAS in the forum thread, but I'll summarize the improvements below. Thank you to everyone who followed my progress. A special thank you to AngerFist for encouraging me to try this game one more time.
I performed all of the input, but Alyosha provided some tips that were essential to the run. Before I started the run, Alyosha and I realized that the currently published version, which I made in FCE Ultra 0.98.16, starts from a reset instead of power-on. By starting from a reset, that version gained a significant advantage by manipulating the item-drop order on stage 1. It is not possible to get the torch or knife early in stage 1 when starting from power-on.
Stage 1: 21 frames faster. Optimized graveyard puzzle, improved pixel position, and a smoother boss fight.
Stage 2: 33 frames faster. Improved on the rising platforms and especially on the boss fight.
Stage 3: 6 frames faster. Small improvements throughout the level. If we don't manipulate the dragon's spawn, it will cost a few seconds, not just frames. I spent a long time looking for the best way until Alyosha suggested using the left-right move, which cost only 6 frames.
Stage 4: 6 frames faster. Slightly faster on the boss. Taking damage from the devil at the end is an intentional and necessary pixel-optimization move to save 3 frames.
Stage 5: 174 frames faster. The biggest improvement in the run is the optimization of the timing of the moving platform. I did a lot of research here, and Alyosha assisted with the last piece of the puzzle so that I could get perfect timing. Without manipulating the timing, this part could take up to 7 seconds longer.
Stage 6: 18 frames faster. Slight improvement in the zig-zag. Although I improved the boss fight by only 15 frames, it was a major breakthrough. At first, I struggled just to match my previous attempt, which I had thought was perfect. I've spent a lot of time analyzing this battle and it takes precise movement to make it work so beautifully.
Stage 7: 0 frames faster. I think the Astaroth fight is optimal. There's no better option than to wait for the first fireball to pass.

Nach: I've been watching these since the site started, and it's always nice to see the progress. This looks wonderful. Nicely done. Accepting.
fsvgm777: Processing.


Arc
Editor, Experienced player (826)
Joined: 3/8/2004
Posts: 534
Location: Arizona
The platform can take you out-of-bounds to the left because of wrapping. If you quickly jump up to the platform that's moving right, you can get back in bounds. If you jump off the platform before the crash, you're stuck out-of-bounds. (You can walk to the left and right limits of the out-of-bounds area.) When the platform tries to take you too far left, the game crashes.
Site Admin, Skilled player (1254)
Joined: 4/17/2010
Posts: 11475
Location: Lake Char­gogg­a­gogg­man­chaugg­a­gogg­chau­bun­a­gung­a­maugg
I was testing this yesterday, it's because camera speed gets confused: it's 2 byte for whatever purpose. And these 2 bytes must be set accordingly, like whenever one is negative, the other must also be negative. But when you warp around the screen, one of them gets positive, another gets negative, resulting in value of -250 and such, and when the cam needs to actually scroll (when you arrive to the middle of the screen from the other side), it uses that speed value and goes way to the left, so that camera position is also around -250. Jump to contents of $22 happens when a new object is about to be spawned by scrolling (I guess?), but it gets written with values of $24, and that is what breaks I think. I was about to trace this for a few frames, but got tired. Hold on. Download GnG.lua
Language: lua

function stuff() Xcam = memory.readwordsigned(0x64) DXcam = memory.readwordsigned(0x5d) Objects() --Sprites() gui.text(30, 0, DXcam) -- don't crop 8 scanlines to see gui.text(30, 10, Xcam) end function Objects() for i = 0, 0x15 do local color = "#ffaa00ff" local id = memory.readbyte(0x4f0+i) local y = memory.readbyte(0x509+i) local spr = memory.readbyte(0x522+i) local pal = memory.readbyte(0x53b+i) local x = memory.readbyte(0x554+i) if id<80 then color = "#22ccccff" end -- offscreen if id> 0 then gui.text(x,y,string.format("\n%X",id),color,"#000000ff") end end end function Sprites() local base0 = 0x480 for i = 0, 16 do local base = base0 + i*4 local y = memory.readbyte(base+0) if y==0xff then break end local spr = memory.readbyte(base+1) local pal = memory.readbyte(base+2) local x = memory.readbyte(base+3) gui.text(x,y,i,"#ffaa00ff") end end gui.register(stuff)
Warning: When making decisions, I try to collect as much data as possible before actually deciding. I try to abstract away and see the principles behind real world events and people's opinions. I try to generalize them and turn into something clear and reusable. I hate depending on unpredictable and having to make lottery guesses. Any problem can be solved by systems thinking and acting.
Site Admin, Skilled player (1254)
Joined: 4/17/2010
Posts: 11475
Location: Lake Char­gogg­a­gogg­man­chaugg­a­gogg­chau­bun­a­gung­a­maugg
And I doubt we can get any use of it. There's 3 ways to change the result: duck while you go to illegal area, jump, or be at the bottom of the screen. Can't go there while on top because there's no room for scrolling up there. Jump by $22 contents is related to object spawn and is determined by scrolling. $C1 contains object config offset ( I think), it looks like 2-byte. It is used to formulate the resulting jump pointer that is then stored in $22. So if you're at the bottom, scrolling goes downwards as well. It results in $C1 value that lets it formulate a somewhat legit pointer, so nothing special occures, even though $C1 value is still out of global bounds, the game only uses values up to $D02, and $C1 is $F01. We end up in random garbage that gets loaded as a map, and from my tests, there's nowhere to go there, you can't get back in bounds since there's some block on the very right, you can't go left since there's a pit with a wall on the left as well. If we're in the middle, the jump happens using $C1 value $F00, and it leads execution to RAM, however it's only the same address all the time, since it's what we get with $C1=$F00. It is sprite ram, that would work perfectly if not the fact that all old sprites get erased and some random garbage is loaded as well. Now, depending on your movement, garbage sprites start from 3 possible places, all the above (up to our broken jump address) is filled with $FF. $FF is an undefined opcode INS that takes 3 bytes, so PC goes in steps of 3, every time eventually getting to sequence $D0550280 in sprite RAM, but at different offsets. If we're standing still on a platform that moves us to the left, or running there, PC gets to $02 which is halt. If we're ducking, PC gets to $5502 which is EOR $02,X, and it eventually goes to reset vector (?). If we're jumping, PC gets to $D055 which is BNE $03F3, and it also leads to reset in the end.
Warning: When making decisions, I try to collect as much data as possible before actually deciding. I try to abstract away and see the principles behind real world events and people's opinions. I try to generalize them and turn into something clear and reusable. I hate depending on unpredictable and having to make lottery guesses. Any problem can be solved by systems thinking and acting.