SnakeASCII, czyli kultowy "wąż" w specyficznej tekstowej formie

Odpowiedz Nowy wątek
2014-05-26 19:21
1

Z racji tej, że zarwałem kilka wcześniejszych nocek, co przyczyniło się do kompletnego rozregulowania mojego zegara biologicznego, ostatnią noc spędziłem przy komputerze; A że pora nie sprzyjała kończeniu pracy nad moim projektem formatu TreeStructInfo, postanowiłem przysiąść nad czymś przyjemnym, ale nie wymagającym wilekiego skupienia i główkowania;

Zawsze ciągło mnie do napisania kultowej gry Snake w trybie tekstowym, ale w nieco inny sposób, niż wyświetlanie węża zbudowanego ze znaków np. X; Miałem w końcu czas na zapisanie czegoś na kształ odwiecznych ciągotek;

Tak narodził się pomysł stworzenia symulatora gry Snake w trybie tekstowym, który miałby służyć do przetestowania algorytmów zarówno sterowania wężem, jak i jego wyświetlania na ekranie; Ale głównym założeniem było wyświetlanie węża, złożonego ze specjalnych znaków ASCII, służących do budowania rozmaitych ramek okien itd., tak aby wąż był wyświetlany jako jedna, łamana, podwójna linia, gdzie "zakrętami" węża były by znaki rogów ów ramek okien; Wąż także miałby być wyświetlany w różnych kolorach dla różnych jego modułów, a także umożliwiać zjadanie wyświetlanych na ekranie "kropek" (ja nazwałem je "złotem", choć to niezbyt trafne określenie);

I tak udało się stworzyć taki symulator, w całości zaimplementowany w Lazarusie; Nie jest to cudo algorytmiki - kod pisałem po prostu "na pałę", bez wcześniejszego zaprojektowania aplikacji; Nie jest to jednak jeden wielki WTF - zarówno wąż, jak i właściwa gra posiadają osobne klasy, udostępniające określone metody, potrzebne do prowadzenia symulacji; Całość pisałem z rozwagą i przemyśleniem, a kod jest zakomentowany, aby mniej więcej było wiadomo do czego służą dane jego elementy;

Jego przybliżona funkcjonalność:

  • sterowanie wężem za pomocą klawiszy strzałek,
  • resetowanie stanu symulacji, przez skrócenie długości węża do długości początkowej oraz jego ustawienie w domyślnych koordynatach,
  • możliwość przerwania symulacji i zamknięcia programu,
  • możliwość poruszania się po określonym obszarze planszy,
    *wykrywanie kolizji głowy węża z jego ciałem oraz barierkami planszy i blokowanie w takim przypadku ruchu,
  • wyświetlanie "złota" w randomowych miejscach planszy, z wykluczeniem pojawienia się złota poza planszą, na barierce planszy lub na ciele węża,
  • zjadanie "złota" i wydłużanie ciała węża,
  • malowanie węża w dwóch kolorach - głowa i czubek ogona na żółto, a ciało węża na zielono;
    Z głównych funkcji to chyba tyle, choć mogłem o czymś zapomnieć;

Klawiszologia:

klawisz funkcja opis
strzałka w górę MoveUp ruch węża w górę lub skręcenie w górę
Down MoveDown ruch węża w dół lub skręcenie w dół
Left MoveLeft ruch węża w lewo lub skręcenie w lewo
Right MoveRight ruch węża w prawo lub skręcenie w prawo
R Reset przywrócenie długości i pozycji węża do wartości domyślnych
E Exit zakończenie symulacji i wyjście z programu

W najbliższym czasie, jeśli tylko znów znajdę chwilę czasu, to z symulatora poruszania się węża zrobię pełną grę, z możliwością:

  • automatycznego poruszania się węża,
  • możliwość szybszego poruszania się w danym kierunku, przez przytrzymanie klawisza strzłaki,
  • obsługi różnych plansz, nie tylko pustych w środku, ale także "labiryntów",
  • zmianę koloru modułu węża, na którym znajdowało się zjedzone "złotko",
  • liczniki zjedzonych "złotek",
  • prowadzenie rankingu najlepszych wyników + możliwość podania nazwy gracza,
  • plansze wczytywane ze specjalnych plików (pewnie Ini, bo fajnie będzie je można tworzyć ręcznie),
  • różne inne funkcje, dzięki którym gra będzie ciekawa i jakoś może będzie przyciągać;
    Póki co uddaję do dyspozycji sam symulator, który posiada wcześniej opisaną funkcjonalność; Poniżej zrzut ekranu konsoli z uruchomioną symulacją:

snake_simulation.png

Pełne źródła symulatora oraz osobny plik wykonywalny oczywiście udostępniam, dlatego że nie ma sensu ukrywania go przed światem - raczej na nim nie zarobię; A nóż widelec komuś się przyda, lub zainspiruje do programowania; Źródła dołączone do posta w załączniku, w formie archiwum zip;

Zachęcam do przetestowania symulatora węża, a jeśli ma ktoś jakieś sugestie lub propozycje na przyszłość - walcie śmiało; Wszelka krytyka oczywiście także mile widziana; Miejcie jednak na uwadze fakt, iż to dopiero początek - żeby z symulatora zrobić ciekawą grę - jeszcze sporo pracy i modyfikacji istnijącego kodu.


edytowany 2x, ostatnio: furious programming, 2014-06-02 00:49

Pozostało 580 znaków

2014-07-02 15:20
0
babubabu napisał(a)

Gdy wonsz porusza się w lewo to widać przerwy ale gdy porusza się w prawo to tych przerw już nie ma.

Wonsz budowany jest zawsze z tych samych znaków; jeśli część jego ciała idzie w lewo lub w prawo - wykorzystywany jest znak o kodzie 205, a jeśli w górę lub w dół - znak o kodzie 186; Kody znaków wrzucone są do enumów typu TSnakeModuleBasicType:

type
  { snake single module basic type enumeration }
  TSnakeModuleBasicType = (
    smbtNone            = 0,   { none module                 }
    smbtVertical        = 186, { vertical body               }
    smbtCornerLeftDown  = 187, { corner from left to bottom  }
    smbtCornerLeftUp    = 188, { corner from left to top     }
    smbtCornerRightUp   = 200, { corner from right to top    }
    smbtCornerRightDown = 201, { corner from right to bottom }
    smbtHorzizontal     = 205, { horizontal body             }
    smbtHead            = 219  { head module character       }
  );

Natomiast znaki dodatkowe, które używane są do wyświetlania "bramek", zawarte są w enumach typu TSnakeModuleAdditionalType:

type
  { snake single module additional type enumeration }
  TSnakeModuleAdditionalType = (
    smatLeftVert   = 185, { gate from left to vertical     }
    smatTopHorz    = 202, { gate from top to horizontal    }
    smatBottomHorz = 203, { gate from bottom to horizontal }
    smatRightVert  = 204, { gate from right to vertical    }
    smatCrossGate  = 206  { cross gate                     }
  );

Więc jak widać nie korzystam w ogóle ze znaku równości =; Przyczyną musi więc być font używany w konsoli; W kazdym razie zawsze możesz zdebugować źródła i znaleźć ewentualny bug - źródła są publiczne :]


dzek69 napisał(a)

Czyli nie da rady "wypaść" przez ścianę jak odpowiednio zmulę komputer?

Nie wykonasz żadnego niedozwolonego ruchu - przykro mi :]

dzek69 napisał(a)

BTW: Można pogodzić obie metody, ale rzadko widzę takie implementacje.

Owszem, ale na celu miałem zaprogramowanie głównej pętli tak, jak miało to miejsce w grach na Pegasusa; Jeśli zamulisz komputer to całość powinna zwolnić, zarówno logika, jak i malowanie interfejsu; Wykorzystałem tak rzadko stosowaną metodę po pierwsze z ciekawości (zawsze podziwiałem gry z Pegasusa) i aby sprawdzić/przetestować, jak trudne jest napisanie takich pętli dzisiaj; A po drugie też dlatego, że nie znam technik tworzenia gier, synchronizacji logiki i widoku, dlatego też na dzień dzisiejszy nie do końca wiem jak miałoby to wyglądać; Poza tym nie chciałem implementować jakiejś zaawansowanej logiki w tak prostej grze, więc skorzystałem z prostego i ciekawego sposobu;


Marooned napisał(a)

Spodziewałbym się, że poruszać wężem będę kursorami. Niestety, po ruszeniu nie udało mi się nawet skręcić.
Dobra, przeczytałem gdzieś w połowie tego wątku o WSAD. Ale nie lubię WSAD to nie odpalam już ;]

Kurde, tu jest problem z obsługą klawiszy strzałek jako znaków AnsiChar; Nie wiem jeszcze co powoduje nierozpoznanie tych klawiszy przez kontroler, w każdym razie póki co sterowanie ustawione jest na WSAD; Jeśli odkryję problem, to na pewno zamienię z powrotem na strzałki; Ale najpierw muszę znaleźć ten bug i zależy mi na tym, czyli przedebugować kontroler;

W późniejszych wersjach dorobię na pewno ustawienia gry i możliwość zmiany klawiszy sterowania, więc jeśli nie uda mi się przywrócić poprawnej obsługi strzałek - będzie to jakieś połowiczne rozwiązanie;


Jeszcze bardzo dużo będzie w tej grze zrobione, więc obecna forma na pewno będzie modyfikowana i można spodziewać się poprawek i usprawnień :]


Pokaż pozostałe 5 komentarzy
Ale dlaczego chcesz czytać klawisze funkcyjne jako AnsiChar? - Marooned 2014-07-02 19:28
@Marooned Mam wrażenie, że robi to po to by można było odpalić grę na jakimkolwiek systemie - babubabu 2014-07-02 19:32
Póki co znaki czytam jako AnsiChar (czy tam Char - obojętne), bo na to pozwala ReadKey; Zwykłe znaki są jednobajtowe, a strzałki i inne klawisze funkcyjne dwubajtowe; Standardowymi metodami nie pobiorę klawisza jako np. WideChar czy zwrócić kod jako UInt16, bo musiałbym zmodyfikować funkcję ReadKey; Wyszłoby na to samo co jest teraz, bo musiałbym opakować odczyt z bufora i wykonać translację inputu; Z pobieraniem znaków jako AnsiChar nie ma problemu, tylko nie wiedziałem że strzałki są dwubajtowe, stąd brak ich obsługi w poprzednim kodzie (obecnie poprawione); - furious programming 2014-07-02 19:39
Jeśli macie jakieś pomysły to piszcie swoje propozycje w postach :) - furious programming 2014-07-02 19:40
Nigdy nie pisałem w Lazarusie, ale czy użycie GetKeyEvent nie załatwiłoby sprawy? Wiem, że klawisze funkcyjne da się odczytać jak "zwykłe", ale dwubajtowe, ale do funkcyjnych chyba lepiej używać scan code (można wtedy nawet rozróżniać lewy/prawy ctrl/alt/shift). - Marooned 2014-07-03 10:00

Pozostało 580 znaków

2014-07-02 17:01
0

No i poszło - zmodyfikowanie kodu tak aby obsługiwał także klawisze strzałek oznaczało dodanie jednego warunku do pętli w metodzie TGameController.ProcessInput():

procedure TGameController.ProcessInput();
begin
  { check if the key was pressed }
  if KeyPressed() then
  begin
    { get the key from input }
    FInputKey := UpCase(ReadKey());

    { detect an arrow key }
    if (FInputKey = INPUT_KEY_NONE) and KeyPressed() then  // tutaj
      { get the arrow key character }
      FInputKey := UpCase(ReadKey);

    { clear input buffer }
    while KeyPressed() do
      ReadKey();

    { validate input key }
    if (FInputKey in INPUT_SNAKE_MOVE_KEYS_SET) or (FInputKey in INPUT_SPECIAL_KEYS_SET) then
      Exit;
  end;

  { if key is not supported or key is not pressed - set the input null key }
  FInputKey := INPUT_KEY_NONE;
end;

Przywróciłem więc możliwość sterowania wężem strzałkami, więc możliwe że @Marooned program z kolejnej aktualizacji już uruchomi :]


Niestety obsługa klawiszy jako znaków AnsiChar znów objawia kolejny problem; Z racji tej, że i tak znak strzałki ląduje jako któraś z liter H, K, P lub M, póki co nie ma możliwości wybrania strzałek do sterowania oraz liter H, K, P i M do innych opcji, a obecnie literka P służy do zapauzowania gry; Dlatego zmieniłem literkę P na Esc do obsługi pauzy;

Rozwiązaniem tego problemu będzie wprowadzenie translacji AnsiChar na liczbę typu UInt16 i przechowywanie kodu wciśniętego klawisza, a nie jego wartości; Przykładowo młodszy bajt będzie zawierał kod literki, a starszy np. zapalony najmłodszy bit informujący o podaniu rozszerzonego klawisza; Dzięki temu w przypadku litery H, kod klawisza to będzie 0x0048, a strzałki w górę (czyli też litery H) kodem będzie liczba 0x0148; Obojętne jest to, w jaki sposób budowane będą 16-bitowe liczby kodów znaków, ważne żeby dało się je rozróżnić;


Dziękuję za szybką poradę dotyczącą klawiszy strzałek i wyręczenia mnie z Googlowania (choć zacząłem już czytać pewien wymieniony w komentarzu wątek, ale użytkownicy mnie ubiegli) :]


edytowany 3x, ostatnio: furious programming, 2014-07-02 17:04
dodaj 256 do kodu przy #0 i po kłopocie. - vpiotr 2014-07-02 17:11
Też może być - to w sumie obojętne; - furious programming 2014-07-02 17:13

Pozostało 580 znaków

2014-07-03 13:59
fsadsafdfdsa
0
furious programming napisał(a):

Z racji tej, że i tak znak strzałki ląduje jako któraś z liter H, K, P lub M, póki co nie ma możliwości wybrania strzałek do sterowania oraz liter H, K, P i M do innych opcji

zauważ że naciskając strzałkę, kod litery jest poprzedzony znakiem #0 (czyli dwa kody, najpierw zero, potem właściwy) co bardzo łatwo jest obsłużyć

Pozostało 580 znaków

2014-07-03 16:27
0

@fsadsafdfdsa</span></b> - teraz już wiem, dzięki @babubabu, @dzek69 i @vpiotr; Pamiętam jak jeszcze w TI pisząc jakąś grę na ocenę 6 (albo Hanoi, albo Kółko i Krzyżyk - nie pamiętam już), znalazłem w sieci informacje na temat znaków, jakie reprezentują strzałki; Wtedy wszystko obsługiwałem przez ReadKey i wszystkie znaki traktowałem jako jednobajtowe, więc utrwaliło mi się to w głowie, że klawisz strzałki to w sumie jeden znak; Ale to była błędna wiedza, którą nosiłem w sobie do wczoraj;

W kodzie metody TGameController.ProcessInput, który podałem w poprzednim poście, klawisze strzałki są rozpoznawane poprawnie; Nie obsługuję ich od razu, a zapisuję do pola klasy, czyli do FInputKey; Klawisz ten muszę zapamiętać, bo gdybym tego nie zrobił, a jeszcze nie nadszedł czas na obsługę ruchu węża (czyli zegar nie doliczył maksymalnej liczby cykli) - klawisz byłby tracony; W takim wypadku aby skręcić wężem, trzeba by klawisz sterowania wciskać zawsze w ostatniej klatce, a to oczywiście uniemożliwiło by granie;

Więc z racji tej, że wciśnięty klawisz zawsze trzeba zapamiętać, kod znaku trzeba rozszerzyć do dwóch bajtów, ustalając inne wartości dla zwykłych-jednobajtowych klawiszy, a także inne dla dwubajtowych; Rozwiązałem to tak, że przechowywany jest kod znaku jako liczba UInt16; Młodszy bajt zawiera kod znaku, a starszy albo 0x00 dla jednobajtowego klawisza, albo 0xFF dla specjalnego; Do zbudowania takiego kodu znaku wystarczy prosta alternatywa na dwóch liczbach - 16-bitowej i 8-bitewej:

$0000 or $50 = $0050  // dla podstawowego klawisza P
$FF00 or $50 = $FF50  // dla klawisza strzałki w dół, który także posiada kod litery P

Dzięki temu np. strzałka w dół oraz klawisz P mogą mieć osobne funkcje i nie będą się nakładać; Kod już zmodyfikowałem w tym celu, no i działa prawidłowo; Obecna forma wygląda w ten sposób:

procedure TController.ProcessInput();
var
  chrInput: AnsiChar;
begin
  { check if the key was pressed }
  if KeyPressed() then
  begin
    { get the key from input }
    chrInput := UpCase(ReadKey());

    { detect extended key }
    if (Ord(FInputKey) = INPUT_KEY_NONE) and KeyPressed() then
    begin
      { get the extended key character }
      chrInput := UpCase(ReadKey());
      { build the extended key code }
      FInputKey := INPUT_EXT_KEY_CODE_PREFIX or Ord(chrInput);
    end
    else
      { build the standard key code }
      FInputKey := INPUT_STD_KEY_CODE_PREFIX or Ord(chrInput);

    { clear input buffer }
    while KeyPressed() do
      ReadKey();

    { validate input key }
    if ValidateInputKey(INPUT_SNAKE_MOVE_KEYS_ARR) or ValidateInputKey(INPUT_SPECIAL_KEYS_ARR) then
      Exit;
  end;

  { if key is not supported or key is not pressed - set the input null key }
  FInputKey := INPUT_KEY_NONE;
end;

deklaracje stałych prefiksów dla kodów klawiszy i ich typu poniżej:

type
  { double-byte type for input key codes }
  TInputKey = UInt16;

const
  { input key code prefix for standard key }
  INPUT_STD_KEY_CODE_PREFIX = TInputKey($0000);
  { input key code prefix for special key }
  INPUT_EXT_KEY_CODE_PREFIX = TInputKey($FF00);

To oczywiście wygląda bardziej na bajer niż najsensowniejsze rozwiązanie, dlatego że tak jak @Marooned zauważył - można czytać klawisze od razu jako liczby 16-bitowe, wykorzystując zdefiniowane kody klawiszy ze zbioru stałych z prefiksami VK_; Żeby skorzystać np. z GetKeyState musiałbym po pierwsze sprawdzić, czy ta funkcja jest uniezależniona od platformy (w 2009 roku jeszcze nie była) oraz przystosować kod tak, aby można było obsłużyć kilka klawiszy naraz; Tutaj pojawiło by się więcej warunków, bo trzymając wciśnięty klawisz trybu turbo (domyślnie jest to klawisz spacji) oraz skrętu węża, trzeba by skręcenie węża wykonać bez względu na stan timera; Z drugiej strony, jeśli klawisz turbo nie jest wciśnięty a klawisz sterowania jest - trzeba by go zapamiętać i obsłużyć tylko w przypadku, jeśli tajmer doliczył odpowiednią ilość cykli;

W każdym razie GetKeyEvent odpada, bo blokowałby program; Z tego co wyczytałem, ta funkcja oczekuje na klawisz, jeśli nie został wciśnięty, czyli zamroziłby grę; Jej odpowiednikiem, który nie blokuje programu jest PollKeyEvent, więc tę funkcję można by wykorzystać; Zaś samo zapamiętywanie musiało by być zmienione - zamiast zapamiętywania jednego klawisza, trzeba by pamietać wszystkie; Czyli zmienić FInputKey z pojedynczej liczby 16-bitowej, np. na kolejkę, którą trzeba by odpowiednio obsługiwać;

Oczywiście wszystko można zaprogramować, tylko że z założenia tryb turbo miał być domyślne możliwy do wykorzystania tylko w poruszaniu się w linii prostej, bez możliwości używania go również na zakrętach; Czytanie klawiszy jako znaków AnsiChar po pierwsze umożliwia w prosty sposób rozróżnić klawisze podstawowe od specjalnych, a po drugie samo z siebie wyłącza automatycznie tryb turbo, nawet jeśli nie zwolni się jego klawisza :]

Tu muszę się jeszcze zastanowić, czy umożliwić wykorzystanie tego trybu cały czas, czy tylko na prostych; Jeśli cały czas - kontroler i główna pętla gry pójdą praktycznie całe do modyfikacji;

Następnym elementen do modyfikacji jest obsługa macierzy typu TSnakeModulesArr w klasie TSnake; Nie wiem czym ja myślałem (chyba tym z tył na dole), ale obecnie każdorazowo po złapaniu złotka, macierz z pola FModules jest powiększana o jedno miejsce, czyli realokowana; Co złapanie złotka - relokacja macierzy, a im dłuższy wąż, tym dłużej ta relokacja będzie trwać; Przy bardzo długim wężu, składającym się przykładowo ze 100-150 modułów, ta relokacja może spowalniać grę; Jakiś dziwne spowolnienia przy testowaniu bardzo długiego węża już zauważyłem, ale nie śledziłem co jest tego przyczyną; Podejrzewam więc stratę czasu na wydłużanie macierzy z modułami;

Rozwiązanie jest takie samo, jak np. w przypadku alokacji pamięci dla wewnętrznej macierzy w klasie TStrings; Rezerwacja pamięci dla kolejnych n * 2 modułów, czy tam np. n + 16 modułów; Można by tez zaalokować od razu dla 819 modułów, bo tyle się zmieści na planszy (gdyby wąż zapełnił całą jej przestrzeń); Jak dobrze liczę, była by to statyczna alokacja 819 * SizeOf(TSnakeModuleRec), czyli 819 * 4, co daje nam 3276 bajtów (3,2KiB); Alokowanie macierzy o stałym rozmiarze całkowicie wykluczyłoby konieczność jej relokacji, a tym samym tracenie czasu pomiędzy kolejnymi klatkami gry;

Podsumowując - kolejny znaczący krok w optymalizacji i raczej to ostatnie rozwiązanie wykorzystam, bo 3,2KiB pamięci alokowane z góry raczej nie można nazwać "zasobożernością" :]


edytowany 12x, ostatnio: furious programming, 2014-07-03 17:06

Pozostało 580 znaków

2014-07-03 16:50
1

Fakt, że nie zaglądałem do kodu więc mogłem od razu napisać PollKeyEvent jako opcja nieblokująca.

Z 10 lat temu gdy pisało się gierki pod DOSa, widziałem bardzo banalny ale skuteczny sposób obsługi nawet i 50 klawiszy na raz - nawet jeśli klawiatura obsługiwała 3 na raz (tyle miałem max na mojej pierwszej, nie licząc ctrl/shift/alt). Autor budował tablicę na wszystkie obsługiwane znaki - dla scan kodów wystarczyła o rozmiarze 255.
Jeśli klawisz zostaje naciśnięty, wstawiamy w tablicę o indeksie klawisza 1, jak puszczony, dajemy 0. Fragment odpowiadający za poruszanie testuje sobie dane w tej tablicy (czy mamy turbo, czy mamy strzałkę w prawo etc).

To działa dla ciągłego poruszania. Średnio nadaje się jednak jeśli chcemy kolejkować klawisze, czyli stuknięcie kursora ma skręcić bohatera nawet jeśli stuknięcie jest krótkie, a w czasie trzymania klawisza nie odpali się kod poruszający postacią.

To tak gwoli teoretyzowania.
Peace


Pozostało 580 znaków

2014-07-03 16:57
0

Muszę przyznać, że ciekawe rozwiązanie, upraszczające obsługę klawiszy;

Jeśli klawisz zostaje naciśnięty, wstawiamy w tablicę o indeksie klawisza 1, jak puszczony, dajemy 0. [...]

Można by zamieć 0 i 1 na wartości typu Boolean, albo wręcz wykorzystać tablicę alokowaną na 256 div 8 bajtach, zapalając i gasząc pojedyncze bity jako flagi stanu wciśnięcia;

Jeśli skusiłbym się na obsługę kilku klawiszy jednocześnie, to kolejkowanie raczej odpada - nie było by możliwości w łatwy sposób sprawdzać stan kilku klawiszy, nie zdejmując ich z kolejki; Mimo wszystko jeszcze się zastanowię nad tym sterowaniem, bo może lepszym wyjściem okaże się obsługa kilku klawiszy; Dziękuję za informacje.


edytowany 1x, ostatnio: furious programming, 2014-07-03 16:57
Pascal wypacza programistę. 0 i 1 to jest boolean :) - Marooned 2014-07-03 17:17
Nie zawsze - False to zawsze 0, ale wszystko większe to True ;) - furious programming 2014-07-03 19:29
Twoje zdanie nie powoduje, że moje jest fałszywe :) - Marooned 2014-07-04 10:51
No tak, to tylko uściślenie :P - furious programming 2014-07-04 19:02

Pozostało 580 znaków

2014-07-05 02:35
0

Czas na kolejną, małą aktualizację; Trochę rzeczy pozmieniałem i usprawiniłem, dzięki czemu rozgrywka na pewno stanie się ciekawsza;


Przede wszystkim zmieniłem sterowanie wężem; Przystosowałem kod pod obsługę klawiszy rozszerzonych (specjalnych), w prosty sposób odczytując je jako dwubajtowe; Dzięki temu kody znaków strzałek nie gryzą się z podstawowymi literkami;


Druga sprawa to rozszerzona funkcjonalność trybu turbo; Wcześniej było tak, że aby skorzystać z tego trybu, trzeba było wcisnąć i trzymać albo klawisz sterowania, albo klawisz dedykowany (czyli klawisz spacji); Teraz jest inaczej; Przytrzymanie klawisza sterowania (strzałki) działa tak jak poprzednio - dopóki trzymamy klawisz, wąż będzie się poruszał z maksymalną prędkością; Natomiast wprowadzona została funkcja trwałego turbo, obsługiwanego klawiszem spacji; Wciśnięcie i puszczenie spacji aktywuje tryb trwałego przyspieszenia, a ponowne wciśnięcie i puszcze wyłącza; Dzięki temu klawiszami sterowania możemy wykorzystywać turbo na krótkich odcinkach, a trwałe turbo cały czas; Bez względu na to czy wąż idzie w linii prostej, czy chcemy nim skręcać, tryb turbo się w takim przypadku nie wyłączy sam;

W przypadku używania trwałego trybu turbo (za pomocą klawisza Spacji) trzeba uważać, bo wąż naprawdę zapierdziela i trudno go okiełznać; Granie stało się trudniejsze, ale to raczej dobrze :]

Następna sprawa to macierz przechowująca informacje o modułach węża; Zrezygnowałem całkowicie z manipulowania jej rozmiarem - rozmiar maceirzy typu TSnakeModulesArr jest zawsze statyczny; Dzięki temu nie ma w ogóle konieczności relokacji pamięci po każdym złapanym złotku, co wcześniej mogło lekko opóźniać grę; Teraz bez względu na długość węża, powiększany jest jedynie licznik jego modułów, czyli wartośc pola TSnake.FLength; Całość działa bardzo szybko;


Kolejną poprawkę naniosłem w klasie TGold; Wcześniej losowanie nowej pozycji było wykonywane tak, że po wylosowaniu nowych współrzędnych sprawdzane było, czy złotko znajdowałoby się na wężu lub w niedostępnym miejscu planszy; Jeśli oba warunki zostały spełnione, czyli złotko nie będzie znajdować się na wężu oraz znajdzie się w dostępnym miejscu planszy - losowanie było kończone;

Jednak zdażało się tak, że po złapaniu złotka, nowe było losowane zaraz obok niego - jedno pole obok; Przez przypdaek można było złapać dwa złotka w jednej linii, dzięki czemu pokazywały się na ciele węża obok siebie dwie bramki; I tutaj byłby problem, gdyby węża pokierować tak, że wchodzi w jedną bramkę, zakręca, staje na drugiej, zakręca i schodzi z drugiej bramki; Problem nie byłby ze sterowaniem, a z wyświetlaniem ogona węża; W takim przypadku nie można by wyświetlić "teownika", tylko trzeba by wyświetlić odpowiedni moduł węża;

Zamiast po raz kolejny rozbudowywać warunki w metodzie rysującej węża, przerobiłem lekko metodę losującą nową pozycję złotka; Teraz dodatkowo sprawdzane jest, czy poprzednie i nowe współrzędne wypadają koło siebie i jeśli tak - losowanie wykonywane jest powtórnie; Warunkiem zakańczającym losowanie jest odstęp co najmniej o jedno pole na osi X lub osi Y; Dzięki temu teraz nie ma możliwości, aby złotko wylosowane zostało jedno obok drugiego; To zwalnia z rozbudowy warunków dotyczących malowania ogona węża;


Z większych modyfikacji to by było tyle; Mniejsze - poprawienie komentarzy, dodanie i zmienienie nazw kilku stałych, zabezpieczenie przywrócenia ustawień konsoli oraz wyczyszczenia jej ekranu po zakończeniu gry (w głównym module programu);

Zmieniona została także klawiszologia:

klawisze strzałek | sterowanie wężem, włączenie/wyłączenie krótkiego trybu turbo
Spacja | włączenie/wyłączenie trwałego trybu turbo
P | zapauzowanie gry
R | zresetowanie gry
Esc | wyjście z gry
To by było na tyle zmian;


Kolejnych zmian będzie trochę i będą to duże zmiany, głównie w klasie TGame; Muszę przygotować kod do obsługi węża tak, aby można go było wykorzystać w wielu miejscach programu; Przyda się to do zrobienia różnych trybów gry - practice, time attack, battle i adventure;

Jeśli uda się skutecznie wydzielić kod do grania, zabiorę się za oprawę gry - czołówkę, menu, opcje ustawień, szybki tutorial o tym jak grać i co można, a czego nie można itd.; Najważniejsze już jest zrobione - wąż umie sam się poruszać, można nim sterować i obsługuje poprawnie klawisze oraz poprawnie zachowuje się na dowolnych planszach;

Co chciałbym jeszcze dodać - chciałbym sprzężyć logikę gry z nowymi funkcjami; Dorobić różne liczniki - licznik żyć, licznik punktów, licznik złapanych złotek (czyli długości węża) oraz licznik czasu rozgrywki; Jeśli mowa o licznikach, to chciałbym przed pokazaniem się planszy, pokazać ekran poprzedzający - numer/nazwę planszy, miniaturkę węża i ilość żyć (coś jak w Super Mario); Chciałbym jeszcze rozszerzyć funkcjonalność klasy rysującej, aby po złapaniu złotka wyświetlała się obok ustalona ilość punktów, która po jakimś czasie zniknie; To samo w przypadku przejść przez bramki - za każde przejście ekstra punkty; Coś jak w grach na pegasusa - po złapaniu czegoś pojawiała się ilość punktów, która po chwili znikała; To samo chciałbym zrobić w tej grze;


Aktualne źródła wersji 0.109 dostępne w załaczniku, a w drugim załączniku skompilowany plik wykonywalny, przeznaczony dla Windowsów;

Życzę miłego testowania i oczywiście jestem otwarty na wszelkie propozycje :]


edytowany 1x, ostatnio: furious programming, 2014-07-05 02:42
Pokaż pozostałe 5 komentarzy
Miało wyjść krótsze. - Sopelek 2014-07-05 15:50
@furious programming no to możesz animację trawy wprowadzić na przykład co 5 ruchów wonsza. - babubabu 2014-07-05 19:33
@babubabu - to nie będzie działało tak samo, dlatego że ilość ruchów węża w jednej sekundzie uzależniona jest od ustawień planszy; Jest pięć róznych prędkości, od ruchu co dwie klatki po ruch co siedem klatek; Dlatego trzeba będzie wykorzystać inny tajmer, a nie ten sam co do ruchów węża; - furious programming 2014-07-05 20:47
Spoko ja tylko zaproponowałem :) - babubabu 2014-07-05 20:56
Trawka w sumie też mogła by się ruszać, więc coś w tym temacie pomyślę - dzięki :) - furious programming 2014-07-05 21:09

Pozostało 580 znaków

2014-07-05 15:52
1
Sopelek napisał(a)

Takie przerysowanie trawy co jedną klatkę byłoby bardzo męczące dla oczu i użytkownika.

Nie tylko dla gracza, ale także dla klasy TPainter; Jeśli już skusiłbym się na przemalowywanie trawki, to jedynym sensownym rozwiązaniem jest stworzenie osobnego tajmera i rysowanie co np. dwie sekundy, czyli co 48 klatek; Wszystko dlatego, aby nie obciążać metody rysującej i utrzymywać płynność gry;

Dzięki za sugestię - zobaczę jak by to wyglądało i co zrobić, aby wyglądało to jak najlepiej; Jeśli chcecie i umiecie to możecie zmodyfikować kod i pokazać tutaj jak według Was wyglądało by animowanie tej trawki; Źródła są w załaczniku :]


edytowany 1x, ostatnio: furious programming, 2014-07-05 15:54

Pozostało 580 znaków

2014-07-05 18:55
1

Źle wygląda gdy konsola ma ustawiony inny niż domyślny font (np. Luicida Console).

Można zacząć od tego:

uses 
  (...) Windows, jwawincon;

Const
 LF_FACESIZE=32;

type
 COORD = Record X,Y:Word; end;
 CONSOLE_FONT_INFOEX =
  record
   cbSize:Cardinal;
   nFont:Cardinal;
   dwFontSize:Coord;
   FontFamily:Cardinal;
   FontWeight:Cardinal;
   FaceName:Array[0..LF_FACESIZE-1] of WideChar;
  end;
 PCONSOLE_FONT_INFOEX=^CONSOLE_FONT_INFOEX;

function GetNumberOfConsoleFonts(): DWORD; stdcall; external 'KERNEL32.dll';
function GetConsoleFontInfo(hnd: Handle;bool: BOOL; uint: DWORD; fonts: PCONSOLE_FONT_INFO): BOOL; stdcall; external 'KERNEL32.dll';
function GetCurrentConsoleFontEx(hnd: Handle;bool: BOOL; var fontInfo: CONSOLE_FONT_INFOEX): BOOL; stdcall; external 'KERNEL32.dll';
function SetCurrentConsoleFontEx(hnd: Handle;bool: BOOL; fonts: PCONSOLE_FONT_INFO): BOOL; stdcall; external 'KERNEL32.dll';
function SetConsoleFont(hnd: Handle; index: DWORD): BOOL; stdcall; external 'KERNEL32.dll';

function GetFontFamilyName(FontFamily: Cardinal): string;
begin
  case FontFamily of
    FF_DECORATIVE: result := 'FF_DECORATIVE';
    FF_DONTCARE: result := 'FF_DONTCARE';
    FF_MODERN: result := 'FF_MODERN';
    FF_ROMAN: result := 'FF_ROMAN';
    FF_SCRIPT: result := 'FF_SCRIPT';
    FF_SWISS: result := 'FF_SWISS';
    else
      result := '???';
  end;
end;

// Check if current console font is "Terminal" and if not - correct it
procedure checkFont;
const
  MAX_FONTS = 40;
  TERMINAL_FACE_NAME: WideString = 'Terminal';
var
  handle: THandle;
  fontCount: DWORD;
  font: CONSOLE_FONT_INFO;
  fontex: CONSOLE_FONT_INFOEX;
  fontIdx: DWORD;
  fonts: array[0..MAX_FONTS-1] of CONSOLE_FONT_INFO;
begin
  fontCount := GetNumberOfConsoleFonts();
  if fontCount > MAX_FONTS then
    fontCount := MAX_FONTS;

  writeLn('Font count: ', fontCount);

  handle := GetStdHandle(STD_OUTPUT_HANDLE);
  if not GetCurrentConsoleFont(handle, TRUE, font) then
    raiseLastWin32Error;

  fontIdx := font.nFont;

  writeLn('Font index: ', font.nFont);
  fontEx.cbSize := sizeof(fontex);

  if not GetCurrentConsoleFontEx(handle, TRUE, fontex) then
    raiseLastWin32Error;

  writeLn('Font retrieved');

  writeLn('Font name: ', WideString(fontEx.FaceName));
  writeLn('Font family: ', GetFontFamilyName(fontex.FontFamily));

  if (fontEx.FaceName = 'Terminal') and (fontEx.FontFamily = FF_MODERN) then
    exit;

  writeLn('Font incorrect, running reconfiguration');

  fontex.FontFamily := FF_MODERN;
  move(TERMINAL_FACE_NAME, fontEx.FaceName, Length(TERMINAL_FACE_NAME)+1);

  if not SetCurrentConsoleFontEx(handle, TRUE, @fontex) then
    raiseLastWin32Error;
end;

begin
  checkFont;
end.

Szacuje się, że w Polsce brakuje 50 tys. programistów
edytowany 1x, ostatnio: vpiotr, 2014-07-05 18:57

Pozostało 580 znaków

2014-07-05 19:02
0

Od razu jeśli da się ustawić czcionkę, to pasowało by też rozmiar.
Domyślna czcionka konsolowa pokazuje tylko jeden rozmiar, w którym wysokość jest równa szerokości. Jest to 8x8, ale to jest za małe.
Była by możliwość ustawienia np. 16x16?

Font, nie czcionkę ;) - Marooned 2014-07-07 09:43

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

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