Calculating the max-out level+0.00 lines — explanation needed

Thread in 'Research & Development' started by furious programming, 18 May 2020.

  1. Currently I'm working on an application called Richtris, which will be a tool for visualizing my games. In addition to HD rendering of game elements, it will also have many additional features known from CTWC. After the game is over, detailed statistics will be visible and one value in them is to be the maxout level in the form of the level number and the fraction of the line. I am talking about this:

    But I don't really understand how it is calculated.

    @Kitaru: can you explain where this result comes from, what the formula for calculating it looks like and what is what in this formula? I would be very grateful for help in understanding this issue.
     
  2. It's more or less just line intersection.

    Take the pre-max score + final line clear score - 999999 to get the amount of excess points above a max-out. (Excess score / final line clear score) gives you the percentage of the final line clear that overshot 999999. Or, you can work from the opposite direction using 1 - (excess score / final line clear score) to give the percentage of the final line clear that was needed to exactly reach 999999. In either case, multiply by the number of lines in the final clear by the percentage needed/overshot in order to get the fractional lines needed/overshot and either add to pre-max lines or subtract from final line count as needed.

    In the example given, 991000 + 34800 - 999999 = 25801 points in excess of max. 25801 / 34800 ≈ 74.14% of the final line score in excess of max, or conversely, ≈ 25.86% of final line score needed to achieve max. 4 lines in a tetris * 25.86% of a tetris needed to max = 1.03 fractional lines. (Or vice versa -- 4 lines in a tetris * 74.14% ≈ 2.97 lines overshot, and 224 total lines - 2.97 overshot = 221.03, or Level 28 + 1.03.)
     
    furious programming likes this.
  3. @Kitaru: thank you very much for the clarification — now it is clear. I knew it was a line faction, but I didn't find a way to get "1.03" at the end, based on sample data.

    Ok, I prepared a function in Free Pascal to produce the final maxout result string:

    Code:
    {
      parameter   sample   meaning
    
      ALevel      28       pre-max level id
      AScore      991.000  pre-max score
      AGainLines  4        final line clear count
      AGainScore  34.800   final line clear score
    }
    function GetMaxOutResult(ALevel, AScore, AGainLines, AGainScore: Integer): String;
    var
      ScoreTotal, ScoreOvershot: Integer;
      PercentOvershot, FractionalLines: Single;
    begin
      ScoreTotal := AScore + AGainScore;    // 1.025.800 points
      ScoreOvershot := ScoreTotal - 999999; //    25.801 points
    
      PercentOvershot := ScoreOvershot / AGainScore;                // 74.14 percent
      FractionalLines := AGainLines - AGainLines * PercentOvershot; //  1.03 lines
    
      Result := '%.2d+%1.2f'.Format([ALevel, FractionalLines]); // 28+1.03 lines
    end;
    Simplified version, with a few auxiliary variables and comment lines so that you can see what is happening in the code. By using this function as follows (https://ideone.com/Vio13B):

    Code:
    ResultString := GetMaxOutResult(28, 991000, 4, 34800);
    the value returned is "28+1.03", so it seems to work correctly.
     
    Last edited: 19 May 2020
  4. Extra question — what if the player reach the max out by using push down points? ;)
     
  5. That would just be the number of lines they're at at the time, since it doesn't change on soft drop.

    I'd like to add a reminder that the score calculation in NES Tetris uses the level number *after* the line clear - something that is easy to forget if you can't see the score directly. Did you make sure that the formula works properly when crossing level boundaries? (I didn't see it in what you coded.)
     
    furious programming likes this.
  6. @Arcorann: thanks for reply, especially because now I see that still the calculations in my code are wrong. For the example data provided by @Kitaru the result is correct but, if I take a data of the real game, e.g. the @niner80 record, the result is completely wrong. So thank you again. ;)

    From what I see in the example from @Kitaru, the level number before reaching max out is taken into account for the calculation. Now I understand even less how it should be calculated...

    @Kitaru did not write about the fact that it is important for calculations, so I did not take this into account.


    It is not possible to write a function that calculates the result, since I do not have complete information about the input data and about cases that should be taken into account.

    Maybe instead of explaining the example, someone will provide an algorithm in the form of a pseudo-code or in the code of some non-functional programming language (no matter which one)? Taking into account all important cases (with maxing by push down points) and providing sample data to check the correct operation. And what about PAL version? @Kitaru — can you do this?

    Formula and conditional instructions are key — then everything will be clear to me.
     
    Last edited: 20 May 2020
  7. Sorry, I may have gone a bit light on the explanation -- was aiming to answer the question at hand rather than write a complete specification. I wrongly assumed that scoring being based on level after lines are cleared was common knowledge, and thought boundary checking for level+lines notation in cases where the fractional result doesn't add up to 10+ lines would come naturally enough, but it's always better to have all the details accounted for.

    It's not the cleanest approach, but hopefully this bit of JavaScript specifies things well enough:
    Code:
    function _calcMax(entry) {
      var score = parseInt(entry['score'])
      if (score >= 999999) {
        score = 999999
        var level = parseInt(entry['level'])               //this is the level _after_ lines have been cleared
        var premaxScore = parseInt(entry['premaxScore'])
        var premaxLine = parseInt(entry['premaxLine'])
        var postmaxLine = parseInt(entry['postmaxLine'])
     
        var lines_delta = postmaxLine - premaxLine
        var score_delta = (level+1) * [0,40,100,300,1200][lines_delta]
        var scoreWithAlpha = premaxScore + score_delta
        var scoreExcess = scoreWithAlpha - score
     
        var lines_adjusted = lines_delta - ((scoreExcess/score_delta)*lines_delta)
        var postmaxLine_adjusted = premaxLine + lines_adjusted
     
        if (Math.floor(postmaxLine_adjusted/10) < Math.floor(postmaxLine/10)) {
          level -= 1
        }
        while (postmaxLine_adjusted >= 10) {
          postmaxLine_adjusted -= 10
        }
        var line = postmaxLine_adjusted
     
        entry['scoreWithAlpha'] = isNaN(scoreWithAlpha) ? null : scoreWithAlpha
        entry['level'] = isNaN(level) ? null : level
        entry['line'] = isNaN(line) ? null : line
      }
      return entry
    }
     
    furious programming likes this.
  8. Edge case I forgot about: what happens if the player maxes out but gets both line clear and soft drop points at the same time?
     
    furious programming likes this.
  9. @Kitaru: thank you again. I have translated your code to Free Pascal and now it works fine, returns the correct result. As a sample, I took the data from this video and the result coincides with that in the table of records.

    My code, translated almost 1:1 looks as follows:

    Code:
    uses
      Math, SysUtils;
    
    {
      argument        sample    meaning
    
      APreMaxScore  - 981653  - score before final line clear
      APostMaxScore - 1012853 - score after final line clear
    
      APreMaxLines  - 186     - lines count before final line clear
      APostMaxLines - 190     - lines count after final line clear
    
      APostMaxLevel - 25      - level id after final line clear
    
      Result        - 24+8.35 - max out result
    }
    
    function GetMaxOutResult(APreMaxScore, APostMaxScore, APreMaxLines, APostMaxLines, APostMaxLevel: Integer): String;
    type
      TIntDynArray = array of Integer;
    var
      Score, Level, LinesDelta, ScoreDelta, ScoreAlpha, ScoreExcess: Integer;
      Lines, LinesAdjusted, PostMaxLinesAdjusted: Single;
    begin
      if APostMaxScore >= 999999 then
      begin
        Score := 999999;
        Level := APostMaxLevel;
    
        LinesDelta := APostMaxLines - APreMaxLines;
        ScoreDelta := (Level + 1) * TIntDynArray.Create(0, 40, 100, 300, 1200)[LinesDelta];
    
        ScoreAlpha := APreMaxScore + ScoreDelta;
        ScoreExcess := ScoreAlpha - Score;
    
        LinesAdjusted := LinesDelta - ((ScoreExcess / ScoreDelta) * LinesDelta);
        PostMaxLinesAdjusted := APreMaxLines + LinesAdjusted;
    
        if Floor(PostMaxLinesAdjusted / 10) < Floor(APostMaxLines / 10) then
          Level -= 1;
    
        while PostMaxLinesAdjusted >= 10 do
          PostMaxLinesAdjusted -= 10;
    
        Lines := PostMaxLinesAdjusted;
        Result := '%.2d+%1.2f'.Format([Level, Lines]).Replace(',', '.');
      end
      else
        Result := '-';
    end;
    So, if the max out occured, the result is the valid string, otherwise the result is just dash character. But still this algorithm does not support maxing out by using push down points only (without line clear). In this scenario, the code above will crash at this line:

    Code:
    LinesAdjusted := LinesDelta - ((ScoreExcess / ScoreDelta) * LinesDelta);
    because the ScoreDelta is zero, so we have a division by zero exception (SIGFPE). What should be done in this situation and how the result should look like, if there was no line clear but the score grows to 999999+?

    I know that JavaScript is a different language than Free Pascal and can do different things (like silent math exception handling), but Pascal is practically the same as C/C++. So if you could answer in the context of general-purpose procedural languages I would be grateful, because I cannot translate 1:1 some JS constructions.

    Also I understand that maxing out using push down points only is an (edge case)³ and probably will never happen but, if such a situation occur, I don't want my software to crash. ;)


    @Kitaru: can you refer to this question? Does your code handle such a case correctly?
     
  10. If I am asking for too much, just write.

    After all, your help in this topic would be valuable to me. ;)
     
  11. If lines used to max-out is zero, there is no change from the absolute line count. No calculation is necessary.

    When maxing out with a line clear, you have no way of knowing if soft drop points were scored; the information is lost when the score is capped to 999,999 so naturally there is nothing that can be done about that. It's also generally ~0.05% difference in the score delta, so there isn't really a reason to consider it even if dealing with footage from an uncapped score counter.
     
    furious programming likes this.
  12. I understand it that if the max out is reached by soft drop, the result should contain sufix +0.00. But because you are calculating results for statistics, you invented those formulas, so I just should ask you for details. I want my software to be compatible with your scoring system.

    Only if you are using non-modified catridge/ROM and no Game Genie code. I'm using original, non-modified ROM with Game Genie code to see post-maxout score (as other players do), so I have this information (other players also).

    Unfortunately, not everyone uses this configuration, and the calculations need to handle all cases, so this knowledge becomes useless.

    Indeed, so this topic we have done, thank you.

    But the last question is — how do you calculating level+lines result for PAL? Are you considering the last line clear in the game and use the same algorithm as to calculate the max out?
     
    Last edited: 10 Jun 2020
  13. The suffix should be x.00, where x is the ones digit of the lines counter on-screen.

    If a soft drop max ever happens and my existing code breaks on it, it's like a 1~2 line change to get it working again. For your project, I would just put a conditional early on that returns the raw line count % 10 early on if no lines were cleared.

    No one has submitted a max yet. If a max is submitted, it will use the method as described here.
     
    furious programming likes this.
  14. Meh… of course — it's obvious.

    I see know that the PAL result is just a level+x.00, where x is the ones digit, so there is no magic. Ok, now I know everything. Thank you for help with this.
     
  15. @Kitaru — can you show me the actual JS code which you are using to perform calculations?
     
  16. In the Python version I'm still generally using for the score thread, I changed the line calculation to:
    Code:
    postmaxLine_corrected = round(premaxLine + lines_delta, 2)
    In the JavaScript version in my Google Sheets custom function, this looks like:
    Code:
    var postmaxLine_adjusted = (premaxLine + lines_adjusted).toFixed(2)
    Since this happens the line before the level and line counts are subtracted to compensate, rounding up to the nearest line now prevents weird display of Lv+10.0 later down the line when the ASCII table print function uses a formatter of %0.2f.
     
    furious programming likes this.

Share This Page