I won’t call anyone a moron because it took me time to figure it out too.
For the buffer, you add the new inputs to it on each frame right when you read them, you always want it to be up to date so you’ll always be checking for the latest move.
For the underlying structure, it’ll be some sort of array to which you must be able to always add, but there’s plenty of room for the specifics, I’m gonna mention three main approaches.
-you can just do it with one big array, preferrably one that can resize when it’s gonna overflow (so, std::vector in C++, arraylist in java, list in c#, some similar name in other languages) or you do the resizing yourself (rule of thumb: double size when it’s full). You always add the new input at the end, and keep track of where the end is. The drawback is the array may become quite large, but supposing a 10 buttons layout that’d be stored with 1 byte per button (more efficient would be just one bit but let’s go with the simple inefficient one), you’d get 36K per minute, which is still reasonable. The perk here is if you restart the match with the same start data, and just go through this array instead of reading input from the player you get a full replay! So even if you don’t use that one keep it in mind for the day you need a replay.
-you can do a smaller array that’s just size X and on each frame you shift all the content to the left ((for i=0; i<arraylength-1;i++) array[ i ]=array[i+1];) and write the new input at the end (or shift everything to the right and add at the beginning depending which way you prefer, that doesn’t matter). The big pro is the array is a constant size, the con is you rewrite it all on each frame (but since it’s small it won’t be a real problem). However…
-that’s actually a common problem so there is a data structure to avoid that rewrite! It’s called a ring buffer. Long story short (the one on wiki is actually a bit complex for my taste), when you reach the end you just go back to the beginning, so it’s always writing over itself, and you keep track of where you last wrote, then you just need some simple arithmetic ((current index - how many frames behind you want to read) mod array length iirc) to read it like the array above. Only drawback is figuring out the right calculation in the first place, then it’s smooth sailing.
I’ll come back to the magic function in a bit, that one requires a bit more thinking.
EDIT:
So, magic function. Let’s go back to mugen for a bit, 'cause it’s handy for formalizing the notation. In Mugen the commands are a series of presses separated by commas. So a LP hadoken would be something like 2,3,6,LP. So it’s a series of button presses separated by commas, and we can represent it by an array of button presses. For the move to be accepted each of those individual items in the array must be true in turn, with the last press occurring on the current frame. Let’s assume our buffer, no matter how it is implemented, has a method bool ButtonWasDownAtFrame(button, howManyFramesAgo) that can tell us if the button was pressed. ButtonWasDownAtFrame(2,4) would check if down was down 2 frames ago, ButtonWasDownAtFrame(LP, 0) would tell us if the LP button is down at the current frame, etc. This is abstraction: no matter what kind of buffer you chose, at this level it’s already not a problem anymore.
So, to check if LP was pressed at the current frame, your check would be ButtonWasDownAtFrame(LP, 0) && !ButtonWasDownAtFrame(LP,1), ie the button was down this frame but not the previous one. If you want to check a negative edge or button release X frames ago you do !ButtonWasDownAtFrame(LP, X) && ButtonWasDownAtFrame(LP,1), you want (for a charge move) to know if a button was pressed for Y frames before frame X, you loop over the whole range ButtonWasDownAtFrame(LP, X) to ButtonWasDownAtFrame(LP, X+Y) and they must all be true. So with just these basic combinations you can build slightly higher level methods, let’s say ButtonWasPressedAtFrame(button, frame), ButtonWasReleasedAtFrame(button, frame) and ButtonWasHeldAtFrame(button, frame, howLong).
So, now, let’s say you want to know if a LP hadoken (2,3,6,LP) has happened in the last 20 frames. As I said we start from the end, so it goes like this:
-check if LP button was pressed at frame 0 (ie right now)
-if that’s true, go back through frames, and for each frame check if button 6 was pressed until you either find it or you’ve reached frame 20 (ie 20 frames from frame 0), in which case you decide the hadoken was not performed.
-if you found the 6, from the frame right before that press go back through frames, and for each frame check if button 3 was pressed until you either find it or you’ve reached frame 20 (ie 20 frames from frame 0, not the current point from which you started), in which case you decide the hadoken was not performed.
-if you found the 3, from the frame right before that press go back through frames, and for each frame check if button 2 was pressed until you either find it or you’ve reached frame 20 (ie 20 frames from frame 0, not the current point from which you started), in which case you decide the hadoken was not performed.
-if you found the 2, your command is deemed legit.
You can see the loop emerging, right? Your command is a time window and a series of button presses. Check for the last press, then for each previous press go back through your input history searching for a match until you’ve either found them all (in which case the move is valid) or reached the end of the time window the player had to make the move (or of your buffer, for that matter). That can be turned into a while loop with a little work.
Note this is a very lenient method: for the hadoken example it’ll also be true for 2,3,4,6,LP or 2,LP,3,6,LP because it won’t check if buttons that don’t matter to our move were pressed. So depending on the result you may want a stricter check (say, when you go back one frame after you check for the button you’re interested in you also check if the input has changed at all. If so, you’ve got an extra press and may stop because you want the move to be exact). This is a basic framework, you may then tweak and expand it to add functionality as you find its limitations.