alex-ong / NESTrisOCR

OCR for statistics in NESTris
24 stars 7 forks source link

Add more digit patterns #17

Closed timotheeg closed 4 years ago

timotheeg commented 4 years ago

I'm going to try to submit a few PRs for some of the stuff I've had dormant for a while. It's quite hard to extract changesets that are nicely self contained (and will reduce my pain in resolving code conflicts later šŸ˜…)

So, starting with a simple one here, hopefully straight forward.

There are are some minor optimizations that can be done in OCR. If you OCR das with Das Trainer, values are from 00 to 16, so we can introduce a digit pattern of B to denote [0, 1]. Pattern B is not yet used in the code, but added anyway :)

Similarly, for the vast majority of human players, level will not reach beyond 29. Tetris renders level >= 30 with hex characters, so to cover everything, the pattern needs to be AA, but for most players for whom level 29 is kill screen, that's overkill to compare against. So I introduce another set T (for triplet) for [0, 1, 2], such that players can use the pattern TD for level.

I didn't know if you'd want to keep AA for level as default or use TD as default, so I made that one change in a single commit that can easily be removed if needed :)

Cheers!

timotheeg commented 4 years ago

The B or T pattern could be used for the piece stats as well šŸ¤”.

One of the longest game of tetris I've seen (Jake's Marathon to PAL maxout) played 1008 pieces in total, and each type had less than 200 count in the end.

2020-04-04_2328

Of course random being random, it is possible to have piece distribution as (1000, 0, 0, 0, 0, 0, 0), but that's super unlikely, and would probably be death before reaching 200 or 300 piece of that one kind.

alex-ong commented 4 years ago

Right; I can see why TD is superior to AA for the general case. If you also instead wrap it as a config setting (Boolean allow levels past 29) that would be swell and then we can make TD the default.

Wrt to pieces, BDD seems to be a sensible default, however we technically only need the last digit.

The next optimization we actually want is to update text only once per piece, by scanning top of field. I think I discussed this previously. This would give a more than 1000x speed increase imo. We can also optionally queue text read over several frames; this helps for peasant PCs.

I.e. every frame scan field (or top of it)

When new piece is detected, scan score.

If it changes, scan level and lines.

Scan last digit of piece distribution only, checking for += 1.

If you can guarantee no frame drops, I already have a text-free piece distribution method. This is 1000s of times faster than doing ocr.

timotheeg commented 4 years ago

Right; I can see why TD is superior to AA for the general case. If you can instead wrap it as a config setting (Boolean allow levels past 29) that would be swell and then we can make TD the default.

I'll do that for now. Commit coming soon(-ish).

alex-ong commented 4 years ago

Right; I can see why TD is superior to AA for the general case. If you can instead wrap it as a config setting (Boolean allow levels past 29) that would be swell and then we can make TD the default.

I'll do that for now. Commit coming soon(-ish).

Also we need not scan level either; we can scan level at begininng of game, then increment level every 10 lines.

We need not scan lines either; we can scan it once at beginning to check for 0, and then scan just the last digit to check for lines cleared.

Atm we check for new game as

if last.lines== invalid and last.score == invalid and notlevel.isvalid:
  if (lines == 00 && score == 00 && level.isValid): newgame

That might have to be tweaked.

Lots of places to improve performance vastly!

alex-ong commented 4 years ago

let me know when you're done and i will press merge :D

timotheeg commented 4 years ago

Done! (I just forced push a minor change to one of the comment)

Should be clean now :)

timotheeg commented 4 years ago

Your suggestions for improvements are all great! šŸ‘

I'll see if I can do some of it. To be honest though, as long as things are working well enough on my box, I lose interest a bit šŸ˜…, and try to focus on the next part of the pipeline instead (rendering, vs. mode, etc.)

BTW, As you mentioned, I always have NESTris OCR do full field scanning, and I use that to figure out when new piece is released without worrying of missing a frame at the top of the field. Basically, as long as I see a +4 between current count and past count, we know a new piece has appeared.

I liked that +4 approach, because it works even if I drop some frames when the piece is at the top. But to be really reliable, ideally no frame drop is best no matter what of course.

My code looks like this:

// assume able to set up initial block count and self correct
// assume no frame drop

let
    pending_piece,
    pending_single,
    last_block_count,
    clear_animation_remaining_frames;

function process_game_frame() {
    if (pending_piece) {
        onPiece(); // reads what needs to be read (preview, das, etc.)
        pending_piece = false;
    }

    if (clear_animation_remaining_frames-- > 0) return;

    const block_diff = cur_block_count - last_block_count;

    if (block_diff === 4) {
        // new piece has appeared, read all the data at next frame
        last_block_count = cur_block_count;
        pending_piece = true;
    }
    else {
        // assuming we aren't dropping any frame, the number of blocks only reduces when the
        // line animation starts, the diff is twice the number of lines being cleared.
        switch(block_diff) {
            case -8:
                onTetris();
            case -6:
                // indicate animation for triples and tetris
                clear_animation_remaining_frames = 6;
                last_block_count += (block_diff * 5); // equivalent to fast forward on how many blocks will have gone after the animation

                break;

            case -4:
                if (pending_single) {
                    // verified single (second frame of clear animation)
                    clear_animation_remaining_frames = 6 - 1;
                    last_block_count -= 10;
                }
                else
                {
                    // genuine double
                    clear_animation_remaining_frames = 6;
                    last_block_count -= 20;
                }

                pending_single = false;
                break;

            case -2:
                // -2 can happen on the first clear animation frame of a single
                // -2 can also happen when the piece is at the top of the field and gets rotated and is then partially off field
                // to differentiate the 2, we must wait for the next frame, if it goes to -4, then it is the clear animation
                pending_single = true;
                break;

            default:
                pending_single = false;
        }
}

Oh, If block count is 200, that's a game end event too.

In term of the optimization of not reading level. Maybe do that one last? For Das Trainer, I use color references, rather than reading color 1 and color 2 from the piece stats (coz there's no piece stats in das trainer). But so to scan the field and know which colors to check against, I need the level first (bit of chicken and egg with what you mentioned above šŸ˜…)

alex-ong commented 4 years ago

Oh right; all my suggestions were aspirational; i lost interest a long time ago due to 1) it runs fine on my box 2) no one seems to be using it except people who can already dev.

Yes that code looks good in terms of not reading letters/score every frame which cuts out 99% of the heavy lifting. Merging!

timotheeg commented 4 years ago

Thanks!