Algorytm wyznaczania tempa w Tetrisie – szukam pomysłu

0

W moim tetrisowym narzędziu zwanym Richtris docelowo ma być wyświetlany bieżący wynik punktowy oraz pod nim różnica punktów w stosunku do tempa, pozwalającego pobić obecny rekord. Chyba za długo przy tym siedzę i każdy nowy pomysł na implementację jest gorszy od poprzedniego, więc zwracam się o małą pomoc.

Zadanie jest dość trudne, dlatego trochę rzeczy muszę opisać bo wiem, że nie gracie w tego Tetrisa i nie wiecie czym się od charakteryzuje. Ale postaram się ograniczyć opis do minimum. ;)


Od początku. Klasyczny Tetris posiada 19 poziomów, z których można wystartować grę. Rozgrywka praktycznie kończy się wraz z przejściem do poziomu 29. Każdy poziom charakteryzuje się inną liczbą punktów przyznawaną za wyczyszczenie linii (single, double, triple, tetris). Otrzymywane punkty za wyczyszczenie linii oblicza się według poniższego wzoru:

(level + 1) * 40   // single
(level + 1) * 100  // double
(level + 1) * 300  // triple
(level + 1) * 1200 // tetris

Czyli tetris na poziomie 0 wart jest 1200 punktów, a np. na poziomie 18 jest to 22.800 punktów. Skoro mnożną w tym wzorze jest inkrementowany numer poziomu, to przyrost punktów w kolejnych poziomach jest liniowy – tu jest wszystko proste.

Po wyczyszczeniu 10 kolejnych linii, przechodzi się do następnego poziomu. 10 kolejnych i do następnego. Tutaj zaczynają się schody. Tetris ma buga związanego z obliczaniem liczby linii po których przechodzi się do kolejnego poziomu. Jeśli startuje się z poziomu od 0 do 9, to ładnie do 10 wyczyszczonych linii wskakuje kolejny poziom. Wzór sypie się na poziomach od 10 do 19, co pokazuje poniższa tabelka:

transition lines.png

Expected Lines oznacza, że takie wartości powinny być, aby wzór był spójny, natomiast Actual Lines zawiera wartości realne, które gra w wyniku błędu wykorzystuje.

Oznacza to, że startując np. z poziomu 13, należy zrobić 100 linii aby przejść do poziomu 14, a co za tym idzie, 250 linii do poziomu 29. Natomiast startując z poziomu 18, należy zrobić 130 linii aby przejść do poziomu 19 oraz łącznie 230 linii do poziomu 29. To trochę komplikuje obliczenia. Wzór pozwalający obliczyć ile linii mamy do osiągnięcia poziomu 29, na podstawie numeru poziomu startowego, wygląda następująco:

// obliczenie liczby linii do przejścia na poziom kolejny względem startowego
LinesTransition := Math.Min((LevelStart * 10 + 10), Math.Max(LevelStart * 10 - 50, 100));

// obliczenie liczby linii do osiągnięcia poziomu 29
LinesKillScreen := LinesTransition + (29 - LevelStart - 1) * 10;

Obliczenia te dają wyniki zgodne w wyżej podaną tabelką. Wszystkie informacje na temat internalsów tej wersji Tetrisa znajdują się w artykule Applying Artificial Intelligence to Nintendo Tetris.


Dane wejściowe, znane w każdej chwili to:

  • numer poziomu startowego – od 0 do 19,
  • obecny numer poziomu – od poziomu startowego do nieskończoności,
  • obecna liczba wyczyszczonych linii – od 0 do nieskończoności,
  • obecna liczba punktów – od 0 do nieskończoności,
  • liczba punktów będąca rekordem – od 0 do nieskończoności.

Znane też są stałe, którymi gra się posługuje:

  • liczba linii potrzeba do przejścia do kolejnego poziomu – dane z tabelki,
  • liczba linii potrzebna do osiągnięcia poziomu 29 – dane z tabelki oraz podanego drugiego wzoru.
  • liczba punktów, przyznawana za kasowanie linii – dane z pierwszego wzoru.

Sytuacja wydaje się prosta, bo teoretycznie wystarczy obliczyć ile linii mamy do poziomu 29, podzielić bieżący rekord punktowy przez tę liczbę linii, w wyniku czego uzyskać liczbę punktów przypadających na jedną linię. Następnie tę liczbę punktów pomnożyć przez liczbę obecnie wyczyszczonych linii – mamy tempo idealne. Na koniec od obecnej liczby punktów odjąć tę dla idealnego tempa i uzyskać liczbę dodatnią, jeśli tempo pozwala na pobicie rekordu lub ujemną, jeśli tempo jest za słabe.

Problem w tym, że taki wzór będzie produkował niepoprawną wartość tempa. Im niższy poziom startowy, tym strata tempa będzie coraz większa z każdą kolejną wyczyszczoną linią. Sytuacja unormuje się w granicach startowania z poziomu 10-15, a przy wyższych znów tempo będzie wyliczane niepoprawnie, tym razem w drugą stronę (zysk tempa będzie nienormalnie duży).

Chodzi o to, aby bez względu na numer poziomu startowego, robienie pod rząd tylko tetrisów powodowało wzrastanie zysku w stosunku do tempa. Dlatego na pewno należy wziąć pod uwagę poziom startowy i liczbę linii, po wyczyszczeniu których wskakuje kolejny poziom. Dalsze poziomy będą wskakiwać już co 10 kolejnych linii. Na pewno też należy wziąć pod uwagę punktację za single/double/triple/tetrisy w stosunku do poziomów pomiędzy startowym a końcowym (czyli 29).

Nie mam bladego pojęcia jak to policzyć. Mógłbym to zbrutforsować, ale że tempo liczone ma być po każdej wyczyszczonej linii, wolałbym oszczędzić moc obliczeniową. Ktoś ma jakiś pomysł?

Jeśli coś jest niejasne to piszcie – udzielę wszelkich informacji na temat gry oraz wymagań.

1

Występuję z pozycji laika, więc nie wiem, czy to się klei.

  1. Bierzesz co najmniej 10 ostatnich linii i na ich podstawie szacujesz średnią gracza. Najsłabiej to 10 razy single, najlepiej to 3 razy tetris. 2) Teraz liczysz, czy jeżeli gracz będzie grał tym samym tempem, to czy pobije rekord - jak tak, to ma tempo na rekord, jak nie, to nie ma. 3) Potem tylko liczysz, jak bardzo musi przyspieszyć/zwolnić i na tej podstawie robisz relatywne określenie tempa (jakieś "bardzo zielone" albo "+10" czy co tam chcesz).

  2. Tu robimy jakąś tabelkę oceniającą "skill gracza". 10 razy single to skill = 1, 9 razy single i raz double to skill = 2, tak kontynuujesz po wszystkich kombinacjach aż do 3 razy tetris. Tutaj nie wiem, jak ta tabelka ma dokładnie wyglądać, bo nie znam tetrisa, ale koncepcyjnie musisz tylko umieć porównać, czy tempo A single + B double + C triple + D tetris jest lepsze od jakiegoś A' + B' + C' + D'.

  3. To jest proste, ot emulujesz takie samo zdobywanie linii przez następne poziomy. Jak chcesz dopieszczać, to możesz wprowadzić jakiś współczynnik "trudności na wyższych poziomach" i "obniżać" przewidywany skill gracza.

  4. Jak gracz ma skill = X i idzie na rekord, to liczysz k, takie że X - k = Y i z tempem Y gracz przestaje iść na rekord (emulowanym jak w punkcie 2). Wtedy tempo gracza to koncepcyjnie +k i na tej podstawie aktualizujesz ekran, kolorki czy co tam chcesz.

Ponieważ wszystko ma skończoną rozdzielczość, możesz przeliczyć wartości zawczasu i zahardkodować. Potem w czasie działania programu będziesz musiał tylko wyliczyć punkt 1), punkt 2) robisz jednym dodawaniem i porównaniem, a punkt 3) w maksymalnie k dodawaniach i porównaniach. Pewnie styknie.

0

Dzięki za odpowiedź. Jeśli dobrze zrozumiałem Twoją propozycję, to niestety ona odpada – nie mogę bazować na historii ostatnich ”czyszczeń”, dlatego że koniec końców i tak muszę ją do czegoś porównać. Poza tym byłyby to szacunki, a mnie interesuje konkretna, dokładna liczba – dodatnia lub ujemna (albo zero). Muszę brać pod uwagę wymienione pod koniec posta dane wejściowe – historii i kolejności czyszczenia linii program nie przechowuje. ;)



Spróbuję to zobrazować. Rekord to 999.999, zaczynam z poziomu 0. Czas na pobicie tego rekordu to 290 linii – na kill screenie (poziom 29) praktycznie już niczego nie da się zrobić. Mój ułomny sposób wygląda tak:

  • obliczam ile linii mam do kill screena – tutaj 290,
  • dzielę rekord przez liczbę dostępnych linii – tutaj 999.999 przez 290,
  • wynikiem jest liczba punktów przypadająca na każdą linię – tutaj będzie to średnio 3448 punktów.

W ten sposób wyliczyłem sobie tempo, które muszę utrzymywać, aby zrównać wynik z rekordem. Z każdą kolejną linią muszę zyskiwać co najmniej ~3448 punktów, aby mieć szanse na pobicie rekordu. Robiąc tylko tetrisy, muszę zdobywać co najmniej 3448 razy cztery, czyli 13.793.

Problem w tym, że jeśli zaczynam z poziomu 0, jest to nieosiągalne – tetris na tym poziomie wart jest zaledwie 1200 punktów. Powoduje to, że zbijając tylko tetrisy, strata coraz bardziej rośnie:

  • robię pierwszy tetris – dostaję 1200 punktów, tempo wynosi 1200 - 13.793, czyli -12.593 punktów,
  • robię drugiego tetrisa – dostaję 1200 punktów, tempo wynosi -12.593 plus kolejne 1200 - 13.793, czyli już -25.186 punktów i tak dalej.

Tempo staje się coraz bardziej ujemne, co jest po prostu bez sensu – skoro robię tylko tetrisy, to gram idealnie.

Nawet jeśli zacznę z poziomu 0, to mam szanse nawet na 1.348.800 punktów. No ale problem powoduje to, że powyższy sposób nie uwzględnia maksymalnej puli punktów, możliwej do uzyskania na danym poziomie. Na poziomie 0 jest to dwa i pół tetrisa, czyli 2,5 * 1200, czyli 3000 punktów – gra nie może wymagać ponad trzynastu tysięcy.

I drugi przypadek – zaczynam z poziomu 18. Przez pierwsze 130 linii kosz czyszczonych linii jest stały i zgodnie z podanym wzorem koszt tetrisa wynosi (18 + 1) * 1200, czyli 22.800. Po uzyskaniu 130 linii wskakuje poziom 19 i koszt rośnie o 1200, po kolejnych 10 liniach wskakuje poziom 20 i koszt znów rośnie o kolejne 1200 punktów i tak dalej (kolejne poziomy wskakują co 10 linii, koszt tetrisa rośnie liniowo).

Zaznaczam też (bo jeszcze nie jest to oczywiste), że tempo na ekranie ma być liczbą dodatnią lub ujemną, będącą różnicą idealnej liczby punktów dla bieżącej liczby skasowanych linii oraz bieżącej liczby punktów. Jeśli mam za mało punktów w danym momencie, liczba ma być ujemna i określać ile punktów mi brakuje do idealnego tempa, w przeciwnym razie ma być dodatnia i informować ile punktów jestem ”do przodu”.

Pisałem, że to dość pokręcone… ;)

0
furious programming napisał(a):

historii i kolejności czyszczenia linii program nie przechowuje. ;)

To niech zacznie, nie wygląda to na specjalnie obciążające.

Nawet jeśli zacznę z poziomu 0, to mam szanse nawet na 1.5M punktów. No ale problem powoduje to, że powyższy sposób nie uwzględnia maksymalnej puli punktów, możliwej do uzyskania na danym poziomie. Na poziomie 0 jest to dwa i pół tetrisa, czyli 2,5 * 1200, czyli 3000 punktów – gra nie może wymagać ponad trzynastu tysięcy.

Moja koncepcja dalej działa, nawet jeżeli nie znasz historii. Znajdź najniższy skill = X dla którego pobijasz rekord, a potem wyliczasz, ile punktów ten skill zdobywa na obecnym poziomie i masz liczbę. Możesz też bawić się w poziomy ufności i sensowniejsze analizy, że jeżeli teraz spartolisz i zrobisz same single, to potem będziesz musiał na przykład robić same triple+ do końca gry.

To rozwiązanie jest moim zdaniem słabsze, bo liczba punktów niewiele mi mówi (chyba że tetrisowcy mają to w głowie i umieją łatwo przetłumaczyć P punktów na T tetrisów), ja wolałbym wiedzieć, że gram od kilku minut i trzymam dobre tempo bo na przykład robię tetrisa na dwa triple, a nie, że na tym poziomie muszę natłuc jeszcze P' punktów.

0
Afish napisał(a):

To niech zacznie, nie wygląda to na specjalnie obciążające.

Nie będzie obciążające. Po prostu podejrzewam, że istnieje sposób na wyliczenie wszystkiego bez konieczności przechowywania takiej historii i z tego względu wolałbym jej nie prowadzić.

Znajdź najniższy skill = X dla którego pobijasz rekord, a potem wyliczasz, ile punktów ten skill zdobywa na obecnym poziomie i masz liczbę.

Zgadzam się, i dokładnie o to pytam – jak to zrobić? :D

Jak w danej chwili znaleźć to minimum, znając numer poziomu startowego, numer bieżącego poziomu oraz bieżącą liczbę wyczyszczonych linii? Oczywiście biorąc pod uwagę zbugowane przedziały linii dla poszczególnych poziomów startowych.

Możesz też bawić się w poziomy ufności i sensowniejsze analizy, że jeżeli teraz spartolisz i zrobisz same single, to potem będziesz musiał na przykład robić same triple+ do końca gry.

To nie będzie konieczne – wystarczy mi prosty algorytm, który wyprodukuje sensowną liczbę.

To rozwiązanie jest moim zdaniem słabsze, bo liczba punktów niewiele mi mówi (chyba że tetrisowcy mają to w głowie i umieją łatwo przetłumaczyć P punktów na T tetrisów) […]

Dokładnie – tetrisowcy wykorzystują liczbę punktów do szacowania straty/zysku w tempie. Oczywiście grając ze sobą, bo wtedy tempo jest po prostu różnicą bieżących wyników obu graczy. W przypadku grania samemu, tempo i szanse na max out/rekord określa się na podstawie bieżącego poziomu i liczby posiadanych punktów.

Mam pewien trop, czyli mój ułomny sposób wyznaczania zysku per linia, ale dodatkowo podzielenie go przez 29 i następnie pomnożenie przez obecny poziom. To będzie miało większy sens, ale nadal coś mi w tym nie pasuje. Być może trzeba jeszcze uwzględnić w tym minimalny procentowy zysk z samych tetrisów.

0
furious programming napisał(a):

Jak w danej chwili znaleźć to minimum, znając numer poziomu startowego, numer bieżącego poziomu oraz bieżącą liczbę wyczyszczonych linii. Oczywiście biorąc pod uwagę przedziały linii dla poszczególnych poziomów startowych.

No to pisałem już: bierzesz 12 linii i na ich podstawie rozkminiasz.

Załóżmy, że do następnego poziomu trzeba 10 linii, weźmy dwa poziomy. 20 linii możesz zrobić dwudziestoma singlami, osiemnastoma singlami i doublem, siedemnastoma singlami i triplem, szesnastoma singlami i dwoma doublami, szesnastoma singlami i tetrisem, i tak dalej. Tu jest skończona liczba kombinacji. Możesz wziąć następne 12 linii (bo to się ładnie dzieli przez 1, 2, 3 i 4), możesz wziąć 20, 50, ile chcesz i jak to będzie sensownie (pewnie trzeba będzie potestować różne opcje).

Teraz skill = X oznacza, że gracz będzie robił 20 linii trzema tetrisami i czterema doublami. Zakładasz, że tak będzie grał do końca gry, więc emulujesz to na wszystkie linie dalej i liczysz, ile punktów uzyska takim tempem. Teraz znajdujesz najniższe X' (czyli kombinację najgorszych singlów/doublów/triplów/tetrisów), dla którego gracz bije rekord. Potem emulujesz tak obecny poziom i masz liczbę punktów do uzyskania.

Teraz dokładasz dane historyczne i możesz łatwo potwierdzić, czy gracz jest lepszy od X (bo chcieliśmy trzy tetris i cztery double, a gracz ostatnie 20 linii zrobił samymi tetrisami) i łatwo widać, że gracz gra o wiele bardziej ponad tempo.

0
Afish napisał(a):

Teraz skill = X oznacza, że gracz będzie robił 20 linii trzema tetrisami i czterema doublami. Zakładasz, że tak będzie grał do końca gry, więc emulujesz to na wszystkie linie dalej i liczysz, ile punktów uzyska takim tempem. Teraz znajdujesz najniższe X' (czyli kombinację najgorszych singlów/doublów/triplów/tetrisów), dla którego gracz bije rekord. Potem emulujesz tak obecny poziom i masz liczbę punktów do uzyskania.

Coś mi to podpowiedziało. Skoro w ten sposób mogę znaleźć odpowiednią kombinację, to również mogę po prostu odszukać taki procent tetrisów, że utrzymując go stale, zrównam się z rekordem. To samo, tylko mniej obliczeń i inaczej nazwane.

To by oznaczało, że potrzebuję maksymalnie 100 iteracji głównych, bo 100% to maksimum. Teraz sobie iteruję od 100 w dół (statystycznie mniej iteracji wystarczy do odnalezienia właściwego procentażu) i sprawdzam, czy utrzymując takie tempo wynik będzie bliski lub równy rekordowi. Sprawdzanie czy tak będzie, będzie zagnieżdżoną pętlą, jadącą od bieżącej linii do tej, w której wskakuje poziom 29, mnożąc maksymalną liczbę punktów per linia przez procentaż.

To jest właśnie bruteforce, którego chciałem uniknąć, zastępując go prostszą arytmetyką. Choć im dłużej nad tym myślę, tym bardziej jestem przekonany, że prosty wzór po prostu nie istnieje.

0
furious programming napisał(a):

To by oznaczało, że potrzebuję maksymalnie 100 iteracji głównych, bo 100% to maksimum. Teraz sobie iteruję od 100 w dół (statystycznie mniej iteracji wystarczy do odnalezienia właściwego procentażu) i sprawdzam, czy utrzymując takie tempo wynik będzie bliski lub równy rekordowi. Sprawdzanie czy tak będzie, będzie zagnieżdżoną pętlą, jadącą od bieżącej linii do tej, w której wskakuje poziom 29, mnożąc maksymalną liczbę punktów per linia przez procentaż.

To jest właśnie bruteforce, którego chciałem uniknąć, zastępując go prostszą arytmetyką. Choć im dłużej nad tym myślę, tym bardziej jestem przekonany, że prosty wzór po prostu nie istnieje.

No ale to jest zupełnie coś innego, niż ja proponuję.

Tak naprawdę problem jest bardzo prosty, bo możesz dla dowolnego momentu wziąć 290 linii i policzyć zawczasu wszystkie kombinacje ich uzyskania, a potem posortować po punktach. skill = X to byłaby po prostu jedna z tych kombinacji, a to możesz znaleźć szukaniem binarnym. Tylko wygenerowane kombinacje dla 290 linii będą pewnie zbyt duże do zahardkodowania, więc ja proponuję wygenerować kombinacje dla 12 linii i potem odpowiednio przemnożyć.

1
Afish napisał(a):

No ale to jest zupełnie coś innego, niż ja proponuję.

Tak, ale to o czym napisałeś podsunęło mi jeszcze inne, IMO też sensowne rozwiązanie.

Muszę się z tym przespać, bo póki co mam mętlik w głowie.

0

No dobra, rozgryzłem tego orzecha. ;)

Uparłem się przy tym, aby nie przechowywać historii czyszczonych linii i nie bazować na niej obliczania tempa. Kombinowałem z różnymi parametrami i w końcu znalazłem taki sposób, aby bez względu na poziom startowy oraz obecną liczbę skasowanych linii, wyznaczyć idealną liczbę punktów dla obecnego tempa, którego utrzymanie gwarantuje co najmniej zrównanie wyniku z rekordem.

Jednak to nie oznacza, że obliczeń jest mało – wręcz przeciwnie.


Pierwsze co należy wiedzieć to po ilu wyczyszczonych liniach wskoczy kolejny poziom. Tego dotyczy tabelka podana w jednym z poprzednich moich postów i tę liczbę linii należy obliczyć według określonego wzoru:

function CalculateTransitionLines(ALevelStart: Integer): Integer;
begin
  Result := Min((ALevelStart * 10 + 10), Max(ALevelStart * 10 - 50, 100));
end;

Następnie trzeba wiedzieć ile mamy czasu, czyli ile dostępnych jest linii od rozpoczęcia rozgrywki aż do osiągnięcia ekranu śmierci. Czyli najpierw należy znaleźć moment pierwszej zmiany na kolejny poziom (powyższa funkcja), a następnie dodać do niej tyle linii, aby osiągnąć poziom 29 (lub 19 dla wersji PAL):

function CalculateKillScreenLines(ALevelStart: Integer): Integer;
var
  LinesTransition: Integer;
begin
  LinesTransition := CalculateTransitionLines(ALevelStart);

  {$IFDEF REGION_PAL}
  Result := LinesTransition + (19 - ALevelStart - 1) * 10;
  {$ELSE}
  Result := LinesTransition + (29 - ALevelStart - 1) * 10;
  {$ENDIF}
end;

Przyda się też funkcja, która na podstawie numeru poziomu startowego oraz bieżącej liczby linii, zwróci numer poziomu:

function CalculateLevelByLines(ALevelStart, ALinesCurrent: Integer): Integer;
var
  LinesTransition, LevelCurrent, LinesCurrent: Integer;
begin
  LevelCurrent := ALevelStart;
  LinesCurrent := ALinesCurrent;
  LinesTransition := CalculateTransitionLines(ALevelStart);

  while LinesCurrent >= LinesTransition do
  begin
    LinesTransition += 10;
    LevelCurrent += 1;
  end;

  Result := LevelCurrent;
end;

Skoro wiadomo ile linii mamy do ekranu śmierci, z którego poziomu startujemy, ile mamy obecnie linii i punktów, a także ile wynosi punktowy rekord (wszystkie dane wejściowe), można przystąpić do właściwej części.

Pierwszy krok to odszukanie liczby punktów przypadających na każdą kolejną linię, od początku rozgrywki aż do ekranu śmierci. Aby to wyliczyć, należy wiedzieć na ile kawałków podzielić bieżący rekord. Wcześniej po prostu dzieliłem rekord – 999.999 – przez liczbę dostępnych linii i uzyskiwałem kawałek o wartości 3448 punktów, co było o wiele za dużą wartością i dla wczesnych poziomów strata w tempie ciągle rosła. Taki sposób nie uwzględnia możliwości punktowych na każdym poziomie z osobna.

Wymyśliłem więc, że zamiast dzielić rekord przez liczbę linii, każdemu poziomowi przypiszę liczbę kawałków będącą wartością o jeden większą od numeru danego poziomu. Wszystko dlatego, że numer poziomu wykorzystywany jest jako mnożna we wzorze na obliczanie punktów za czyszczone linie. Czyli dla jednej linii na poziomie 0 przypada 1 kawałek, na poziomie 1 przypadają dwa, a np. na poziomie 18 przypada ich 19. Aby wiedzieć ile łącznie kawałków jest dostępnych w grze rozpoczętej z danego poziomu, trzeba przeiterować od linii numer 0 do linii kill screena i kumulować te kawałki, sumując numer poziomu plus jeden.

Wiedząc ile łącznie kawałków jest dostępnych, wyliczenie liczby punktów przypadających na jeden kawałek to po prostu podzielenie rekordu przez liczbę tych kawałków. Funkcja wyznaczająca liczbę punktów jednego kawałka wygląda następująco:

function CalculateScoreChunk(ALevelStart, AScoreBest: Integer): Single;
var
  LinesKillScreen, LinesCurrent, LevelCurrent, ChunksCount: Integer;
begin
  LinesKillScreen := CalculateKillScreenLines(ALevelStart);

  LinesCurrent := 0;
  LevelCurrent := 0;
  ChunksCount := 0;

  while LinesCurrent < LinesKillScreen do
  begin
    LinesCurrent += 1;
    LevelCurrent := CalculateLevelByLines(ALevelStart, LinesCurrent);

    ChunksCount += (LevelCurrent + 1);
  end;

  Result := AScoreBest / ChunksCount;
end;

Skoro wiemy ile jest wart jeden kawałek, możemy policzyć ile kawałków przypada na obecny stan gry. Wiemy ile linii obecnie jest wyczyszczonych, więc aby policzyć ile punktów powinno być uzbieranych, należy wykorzystać podobny schemat, ale tym razem przeiterować od 0 do liczby obecnie skasowanych linii i kumulować liczbę kawałków przypadających na bieżącą linię pomnożoną przez wartość jednego kawałka. Wygląda to tak:

function CalculateScoreNeeded(ALevelStart, ALinesCurrent: Integer; AScoreChunk: Single): Integer;
var
  LinesCurrent, LevelCurrent: Integer;
  ScoreNeeded: Single;
begin
  ScoreNeeded := 0;

  LinesCurrent := 0;
  LevelCurrent := 0;

  while LinesCurrent <= ALinesCurrent do
  begin
    LinesCurrent += 1;
    LevelCurrent := CalculateLevelByLines(ALevelStart, LinesCurrent);

    ScoreNeeded += (LevelCurrent + 1) * AScoreChunk;
  end;

  Result := Round(ScoreNeeded);
end;

Wszystko już mamy – wołając powyższą funkcję otrzymujemy liczbę punktów dla tempa pozwalającego zrównać się z rekordem. Aby wiedzieć ile punktów nam brakuje (lub ile jesteśmy do tyłu), oczekiwaną liczbę punktów należy odjąć od obecnej liczby punktów. W ten sposób uzyskujemy liczbę dodatnią dla tempa pozwalającego pobić rekord lub ujemną, jeśli tempo jest zbyt słabe.

Aby uniknąć błędnych wartości tempa, należy wziąć pod uwagę kilka przypadków brzegowych – kiedy jeszcze nie mamy skasowanej żadnej linii, kiedy bieżący rekord wynosi 0 (jeszcze nie został ustalony), kiedy obecna liczba punktów jest większa od rekordu (czyli rekord został pobity) itp. No i pod uwagę należy wziąć też liczbę punktów uzyskiwanych za wciskanie strzałki w dół, czyli przyspieszanie opadania klocków.

Oczywiście na własne potrzeby mam też zabezpieczenie dla wersji PAL, w której kill screen to poziom nie 29, a 19, z którego również można zacząć rozgrywkę – w takim przypadku tempo jest zbędne, więc nie ma sensu go w ogóle liczyć.

Koniec końców, główna funkcja przyjmująca wszystkie dane wejściowe i zwracająca tempo w postaci ciągu znaków, będącego liczbą punktów, wygląda następująco:

function CalculatePace(ALevelStart, ALinesCurrent, AScoreCurrent, AScoreBest: Integer): String;
var
  ScorePace, ScoreNeeded: Integer;
  ScoreChunk: Single;
begin
  {$IFDEF REGION_PAL}
  if AStartLevel = 19 then Exit('-');
  {$ENDIF}

  if ALinesCurrent = 0 then
    ScorePace := AScoreCurrent
  else
    if (AScoreBest = 0) or (AScoreCurrent >= AScoreBest) then
      ScorePace := AScoreCurrent - AScoreBest
    else
    begin
      ScoreChunk := CalculateScoreChunk(ALevelStart, AScoreBest);
      ScoreNeeded := CalculateScoreNeeded(ALevelStart, ALinesCurrent, ScoreChunk);

      ScorePace := AScoreCurrent - ScoreNeeded;
    end;

  Result := IfThen(ScorePace >= 0, '+', '-') + Abs(ScorePace).ToString();
end;

Kodu wyszło dość sporo, ale łatwiej mi było testować różne rozwiązania, gdy miałem nieco kupkę małych funkcji (i trochę zduplikowanego kodu). W każdym razie można to wszystko skrócić i zastosować się do reguły DRY.


W razie gdyby ktoś chciał sprawdzić jak w praktyce wypada taki sposób liczenia tempa, do załączników dodaję mini-kalkulator wykorzystujący powyższe funkcje (razem ze źródłami projektu dla Lazarusa). Najpierw należy ustawić liczbę punktów obecnego rekordu, następnie wybrać poziom startowy i wcisnąć przycisk Reset. Aby dodawać kolejne linie i zwiększać liczbę punktów, należy wciskać przyciski po prawej stronie i obserwować jakie dane pokażą się w zablokowanych polach edycyjnych, w tym Current TRT (czyli tetris rate) oraz oczywiście Current Pace, czyli to o co się w tym wątku rozchodzi.

PS: mam jeszcze jeden algorytm do napisania – tym razem wyznaczający jakość losowanej sekwencji klocków. Ale to będzie znacznie łatwiejsze zadanie. Przy czym wskazówka od @Afish przyda się – na podstawie historii danych ”wydarzeń” można ciekawe rzeczy policzyć, w tym przypadku przyda się historia odstępów pomiędzy longbarami. ;)

1 użytkowników online, w tym zalogowanych: 0, gości: 1