L8: Szachy Magów#

Korespondencyjna drużyna zdalnych magów nudziła się podczas telepat-konferencji. Archibald i Eleonora wymyślili własny wariant szachów — z pomocnikami i ognistymi kulami! Do tej pory grali przez Sieć[1] za pomocą jasnowidztwa. Działało to nieźle, ale istniało ryzyko zobaczenia przyszłych ruchów przeciwnika — dając nieuczciwa przewagę. Zamiast rzucać zaklęcia w ciemno, chcą wysyłać datagramy.

[1] — Tkanina magii przenikająca całą materialną i duchową rzeczywistość.

Przygotuj dla nich serwer UDP, który reaguje na wiadomości o stałym rozmiarze 16 bajtów w następującym formacie:

  • 1 bajt — znak (char) definiujący typ wiadomości
  • 1 bajt — bez znaczenia, tzw. padding
  • 14 bajtów — treść wiadomości uzupełniona znakami null ('\0'), zależnie od jej typu:
    • l — wiadomość login — ciąg znaków zawierający nazwę gracza o maksymalnej długości 14
    • c — wiadomość cast — 3 dwubajtowe liczby całkowite (uint16_t) reprezentujące kolejno: wybór zaklęcia, współrzędną X celu oraz współrzędną Y celu.
    • q — wiadomość quit — puste ciało

Program przyjmuje jeden argument — port, na którym nasłuchuje serwer:

  ./sop-mages <port>

Etapy:#

Etap 1 (6 pkt)#

Program oczekuje na datagramy na podanym porcie. Obsługuje 3 typy wiadomości; po otrzymaniu odpowiedniej wypisuje na standardowe wyjście:

  • [Login] Welcome, <name>
  • [Cast] Someone casts <spell_name> onto <X>,<Y>
  • [Quit] Someone quit. Goodbye!

Ponadto program kończy działanie po odebraniu i obsłużeniu 4 wiadomości.

Po otrzymaniu datagramu o nieprawidłowym rozmiarze, nieprawidłowym pierwszym bajcie (innym niż 'l', 'c' lub 'q'), nieprawidłowym wyborze zaklęcia (poza tablicą spell_names) lub nieprawidłowych współrzędnych (mniejszych niż 0, większych lub równych BOARD_SIZE), należy wypisać odpowiedni komunikat o błędzie, ale nie przerywać działania serwera.

Wskazówka: Przy użyciu kompilatora z rodziny gcc, aby zapewnić przewidywalny układ pamięci struktury, można użyć atrybutu __attribute__((__packed__)) — przykład użycia znajduje się w sop-mag.c.

Etap 2 (7 pkt)#

Po otrzymaniu wiadomości cast, zapisz komendę z trzema otrzymanymi liczbami w kolejce FIFO. Równolegle z odbieraniem datagramów uruchom THREAD_COUNT wątków-pomocników, które pobierają najstarszą komendę i rzucają zaklęcie — czyli czekają FAMILIAR_DELAY ms i wypisują odpowiedni komunikat na standardowe wyjście jak w etapie 1. Kolejka ma pojemność MAX_QUEUE komend. Gdy nie ma miejsca na kolejną komendę, odrzuć ją, wypisz odpowiedni komunikat o błędzie i kontynuuj działanie. Do synchronizacji użyj semafora lub zmiennej warunkowej.

Przypomnienie: Aktywne oczekiwanie (tzw. busywaiting) jest zabronione.

Etap 3 (6 pkt)#

Program poprawnie obsługuje logowanie. Odrzucaj wszystkie wiadomości inne niż login przed rozpoczęciem gry — odebraniem wiadomości login od dwóch graczy. Gracze muszą mieć różny port i/lub adres IP. Po rozpoczęciu gry odrzucaj wszystkie wiadomości niepochodzące od graczy. Zalogowani gracze zaczynają z 10 kamykami w sakiewkach — pewna miara punktów w grze.

Zaklęcia rzucane przez pomocników będą kosztować graczy kamyki. Zależnie od rodzaju zaklęcia, będzie to kolejno: 1 kamyk za „Wróżenie" (wybór zaklęcia 0), 3 za „Przyzwanie Żywiołaka" (wybór 1) i 4 za „Kulę Ognia" (wybór 2). Jeśli gracz nie ma wystarczającej liczby kamyków, pomocnik wypisuje na standardowe wyjście [tee hee] Not enough pebbles, <name>!, czeka FAMILIAR_DELAY ms i czeka na kolejną komendę.

Komunikat wypisywany przez pomocnika po pomyślnym rzuceniu zaklęcia od tego etapu uwzględnia również imię rzucającego: [Cast] <name> casts <spell_name> onto <X>,<Y>.

Wiadomość quit od zalogowanego gracza po rozpoczęciu gry oznacza poddanie się. Jego przeciwnik wygrywa. Wypisz komunikat o poddaniu się [Quit] <name> quit. Goodbye!, a następnie ogłoszenie zwycięzcy -= Congratulations, <name>, you win! =-. Następnie program kończy działanie.

Etap 4 (5 pkt)#

Gra rozgrywa się na planszy o wymiarach BOARD_SIZE x BOARD_SIZE. Każde pole może być puste lub zajęte przez żywiołaka należącego do jednego z graczy.

Oprócz dotychczasowych wątków, pojawia się wątek sędziego. Od momentu rozpoczęcia gry (tzn. gdy obaj gracze się zalogują), co sekundę wypisuje on na standardowe wyjście stan planszy i sakiewek graczy wraz z legendą wyjaśniającą, który znak reprezentuje żywiołaka którego gracza, używając ich imion.

Poza tym, co sekundę sędzia zmienia stan sakiewek obu graczy o \(\Delta_K = \lfloor \frac{N}{2} \rfloor - 1\) kamyków, gdzie \(\text{N}\) to liczba żywiołaków danego gracza na planszy. Po takiej zmianie sędzia wysyła do każdego gracza 2-bajtowy datagram z aktualną liczbą kamyków jako liczbą całkowitą bez znaku. Jeśli gracz nie ma już kamyków i \(\Delta_K < 0\), jego przeciwnik wygrywa. Jeśli miałoby to nastąpić jednocześnie dla obu graczy, wygrywa ten, który zalogował się pierwszy. Sędzia ogłasza zwycięzcę na standardowym wyjściu -= Congratulations, <name>, you win! =- i wysyła 1-bajtowy datagram ze znakiem 'w' do zwycięzcy i 'l' do przegranego. Następnie program kończy działanie.

Zaklęcie „Wróżenie" (wybór zaklęcia 0) pozwala graczowi poznać zawartość pól w kwadracie 5x5 wokół wskazanej lokalizacji. Serwer wysyła graczowi datagram o długości 50 bajtów zawierający 25 2-bajtowych liczb całkowitych reprezentujących zawartość 25 pól otaczających cel zaklęcia:

zawartość polapusteżywiołak wróżącego graczażywiołak przeciwnikapoza planszą
wysłana liczba0123

Zaklęcie „Przyzwanie Żywiołaka" (wybór zaklęcia: 1) przywołuje żywiołaka należącego do rzucającego gracza na wskazane pole, jeśli jest ono puste. W przeciwnym razie nic się nie dzieje, a kamyki są tracone.

Zaklęcie „Kula Ognia" (wybór zaklęcia: 2) niszczy wszystkie (!) żywiołaki w kwadracie 3x3 wokół wskazanej lokalizacji.

Po pomyślnym rzuceniu zaklęcia, pomocnik wysyła graczowi 2-bajtowy datagram zawierający liczbę kamyków w sakiewce tego gracza. Oznacza to, że po zaklęciu wróżenia wysyłane są 2 datagramy.

Kod startowy#