Tema 0 - GwentStone Lite

Pentru orice neclaritate vă rugăm să folosiți forumul, urmând ca doar responsabilii temei să vă răspundă la întrebări. Se preferă folosirea forumului tocmai pentru a evita repetarea întrebărilor.

Obiective

  • Familiarizarea cu Java și conceptele de bază ale POO.
  • Fundamentarea practică a constructorilor și a agregării/moștenirii.
  • Dezvoltarea unor abilități de bază de organizare și design orientat obiect.
  • Scrierea unui cod cât mai generic, ce va permite ulterior adăugarea de noi funcționalități.
  • Respectarea unui stil de codare și de comentare.
  • Dezvoltarea aptitudinilor de depanare a problemelor în cod.

Introducere

Tocmai v-ați făcut primul vostru start-up. Fiind foarte pasionați de jocuri și de genul fantasy, ideea voastră de 1M de dolari este crearea unui joc de cărți pe calculator.

Norocul vă surâde pentru că studioul CD Projekt Red pare că nu mai este în vogă în materie de jocuri așa că le puteți fura jocul de cărți, mai exact Gwent. Deoarece doriți ca jocul vostru să nu fie o copie care va fi ușor uitată, ați adăugat mecanisme noi care vor fi asemănătoare cu alte jocuri de cărți populare. Drept urmare, jocul vostru preia cele mai bune lucruri din Hearthstone și din Gwent, fiind mult mai ușor de implementat, dar și de jucat.

De asemenea, ca în orice joc de calitate, o să ne asigurăm că avem și o poveste pe cinste pentru personajele noastre create.

Date tehnice ale jocului

Dorim ca jocul nostru să poată fi jucat de doi jucători în sistem amical. Cu toate acestea, pentru că încă suntem în etapa de dezvoltare a jocului în cadrul studioului nostru OOP-Everything, vom folosi un AI care va simula cei doi jucători.

AI-ul vă va da comenzile de joc în input, împreună cu server-ul care vă va trimite și comenzi de debugging. Comenzile de debugging sunt făcute pentru a verifica statutul jocului în diverse etape, astfel vom putea depana mai ușor problemele care pot apărea în cadrul implementării. De asemenea, server-ul vă solicită și date pe care le va folosi pentru a crea statistici, cum ar fi numărul de câștiguri ale unui jucător și numărul total de jocuri terminate de cei doi jucători. La output dorim să printăm orice informație ne cere server-ul sau orice eroare întâmpinăm în cadrul unei acțiuni invalide.

Prezentare scurtă a gameplay-ului

Jocul este destinat pentru doi jucători, fiecare având mai multe pachete de cărți, numite deck-uri. La începutul jocului, fiecare jucător își alege un deck și primește un erou care îl va reprezenta. Ordinea de joc este stabilită prin aruncarea unei monede.

Jocul este “turn-based”, deci fiecare jucător va avea câte o tură pe care o încheie explicit. Dupa ce fiecare jucător își încheie tura, va începe o nouă rundă. La începutul rundei fiecare jucător primește “mana” care reprezintă valuta jocului apoi se trage o carte din deck și se introduce în mâna jucătorului.

Cărțile din mâna unui jucător pot fi plasate pe masă pentru a fi folosite, dar pentru asta e nevoie de mana, deoarece fiecare carte “costă” o cantitate de mană pentru a putea fi plasata pe masa de joc.

Masa de joc este un loc în care se plasează cărțile pentru a putea interacționa între ele. Fiecare jucător are 2 rânduri pe care poate pune o carte, cărțile fiind astfel făcute să poată fi plasate doar pe anumite rânduri. Eroii jucătorilor sunt prezenți pe masă de la începutul jocului într-un loc special și aceștia au abilități speciale care pot afecta rândurile cu cărți ale jucătorilor.

Scopul jocului este să folosești cărțile de pe masă pentru a ataca eroul adversar, până când unul dintre eroi rămâne fără viață. În acel moment, jucătorul care a dat lovitura finală câștigă. Fiecare jucator are o metrică proprie în care se reține câte jocuri a câștigat și câte jocuri a jucat.

Masa de joc

Masa de joc este locul unde cărțile vor interacționa direct între ele. Ea poate fi reprezentată ca o matrice de 4×5, fiecare jucător având 2 rânduri asignate lui. Cărțile se plasează pe câte un rând conform restricțiilor impuse mai jos, fiind introduse pe rând de la stânga la dreapta, mai exact adăugăm mereu o carte la dreapta ultimei cărți adăugate până se umple rândul. Dacă o carte este omorâtă, atunci toate cărțile din dreapta ei vor fi mutate cu o poziție la stânga (fenomen cunoscut ca shiftare la stânga). De asemenea, pe un rând pot exista maxim 5 cărți.

Rândurile 0 și 1 sunt asignate jucătorului 2, iar rândurile 2 și 3 sunt asignate jucătorului 1, conform imaginii de mai jos. Rândurile din față vor fi reprezentate de rândurile 1 și 2, iar rândurile din spate vor fi 0 si 3 (jucătorii vor fi așezați față în față). Totodată, eroii jucătorilor vor avea un loc special în care vor fi așezați de la începutul jocului.



În teste anumite acțiuni necesită coordonate ale cărților de pe tablă. Aceste coordonate vă sunt date de parametrii “x” (rând) și “y” (coloană).

Formatul cărților de joc

Jucătorul va putea deține cărți de tip minion si o carte specială de tipul erou care îl va reprezenta în joc.

Cartea Minion

Minionii sunt cărți utilizate pentru atac și apărare pe tabla de joc. Fiecare minion are următoarele atribute:

  • mana (int): costul necesar pentru a plasa cartea pe masă.
  • health (int): numărul de puncte de viață pe care le are minionul. Când acest număr este mai mic sau egal cu 0, minionul este eliminat de pe tablă.
  • attackDamage (int): punctele de atac ale minionului, folosite pentru a lovi inamicul.
  • description (String): o scurtă descriere a minionului.
  • colors (String/Enum): culorile care definesc designul grafic al cărții.
  • name (String): numele cărții.

Atributele fiecărui minion sunt primite la input, iar valorile pot varia, chiar dacă numele cărții este același. Astfel, două cărți cu același nume pot avea proprietăți diferite. La fel, nu există un minion generic, ci doar cărți specializate de tipul minion.

Tipuri de minioni:

  • Sentinel, Berserker: Trebuie plasate pe rândul din spate al jucătorului.
  • Goliath, Warden: Trebuie plasate pe rândul din față al jucătorului.
Condiții suplimentare:
  • Jucătorul trebuie să aibă suficientă mană pentru a plasa un minion pe masă.
  • Minionii nu pot ataca de două ori în aceeași tură.
  • Minionii “frozen” (înghețați) nu pot ataca până la finalul turei curente.
  • Atacurile trebuie direcționate către cărțile adversarului.

Carțile de tip Tank

  • Minionii de tip Goliath și Warden au o abilitate specială: aceștia trebuie să fie atacați primii de către inamic. Această abilitate este pasivă și face ca atacatorii să nu poată ataca alte cărți sau eroul adversar până când toți minionii de tip Tank nu vor fi eliminați.

Exemplu de situație: Dacă player-ul 1 dorește să atace o carte a player-ului 2, trebuie verificat dacă există un minion Tank pe rândul din față. Dacă există, acesta trebuie atacat primul. Abia după eliminarea Tank-urilor pot fi atacați alți minioni sau eroul adversarului.

Carțile cu abilități speciale

Aceste cărți au puteri unice care pot fi folosite in timpul jocul. Tipurile de cărți cu abilități speciale sunt:

  • The Ripper: Abilitatea “Weak Knees” scade atacul unui minion inamic cu 2 puncte. Dacă atacul este mai mic de 2, acesta devine 0.
  • Miraj: Abilitatea “Skyjack” face schimb între viața lui și viața unui minion advers.
  • The Cursed One: Abilitatea “Shapeshift” face schimb între atacul și viața unui minion inamic. Dacă minionul atacat are atacul 0, viața acestuia devine 0 și este eliminat.
  • Disciple: Abilitatea “God's Plan” oferă +2 puncte de viață unui minion aliat.
Condiții suplimentare pentru abilități:
  • The Ripper și Miraj trebuie plasate pe rândul din față.
  • The Cursed One și Disciple trebuie plasate pe rândul din spate.
  • Abilitățile speciale pot fi folosite doar pe minionii inamici (cu excepția Disciple).
  • God's Plan se va folosi doar pe o carte a jucătorului curent.
  • O carte nu poate ataca și folosi abilitatea specială în aceeași tură.
  • Nu se poate folosi abilitatea unei cărți care este “frozen”.
  • Efectul abilităților nu se va reseta după terminarea rundei.
Informații suplimentare
  • Abilitățile nu sunt date la input, ele fiind fixate pentru fiecare carte. Trebuie ca voi sa implementați efectul dorit atunci când se folosește abilitatea specială a unei cărți.
  • Cărțile The Cursed One și Disciple au atacul 0, deoarece în povestea noastră ei reprezintă niște druizi. În schimb, The Ripper și Miraj au atacul diferit de 0 fiind minioni legendari. Chiar dacă druizii au atacul 0, ei pot ataca pe oricine.

Cartea Erou

Eroii reprezintă personajele principale ale fiecărui jucător. Fiecare erou are următoarele atribute:

  • mana (int): necesară pentru a folosi abilitatea specială a eroului.
  • health (fix 30): punctele de viață ale eroului. Când acestea ajung la 0, jocul se termină și adversarul câștigă.
  • description (String): o descriere a eroului.
  • colors (String/Enum): culorile asociate eroului.
  • name (String): numele eroului.

Fiecare erou are o abilitate unică ce poate fi folosită pe parcursul jocului:

Lord Royce

Sub-Zero: îngheață toate cărțile de pe rând.

Empress Thorina

Low Blow: distruge cartea cu cea mai mare viață de pe rând.

King Mudface

Earth Born: +1 viață pentru toate cărțile de pe rând.

General Kocioraw

Blood Thirst: +1 atac pentru toate cărțile de pe rând.

Condiții suplimentare pentru eroi:

  • Abilitățile eroilor Lord Royce și Empress Thorina pot fi folosite doar pe rândurile inamicului.
  • Abilitățile eroilor King Mudface și General Kocioraw pot fi folosite doar pe rândurile proprii.
  • Jucătorul trebuie să aibă suficientă mana pentru a folosi abilitatea specială a eroului.
  • Eroul jucătorului poate să atace o singură data per tură.

Informatii suplimentare:

  • Eroul jucătorului este ales random de către joc și vă va fi comunicat în input.
  • Abilitățile nu sunt date la input, ele fiind fixate pentru fiecare carte. Trebuie ca voi să implementați efectul dorit atunci când se folosește abilitatea specială a unei cărți de tipul erou.

Pentru aceste abilități se iterează de la stânga la dreapta și pentru Low Blow se ia prima carte de pe rând care respectă criteriul specificat mai sus (ex. dacă cartea cu indexul 0 și cartea cu indexul 3 au viață maximă de pe rând, se va selecta cartea cu indexul 0).

Gameplay detaliat

Pregătirea jucătorilor

Fiecare jucător are mai multe pachete cu care poate începe jocul, prima lui sarcină fiind să aleagă pachetul cu care dorește să joace. Veți primi la input numărul de pachete de cărți, numărul de cărți din fiecare pachet, o listă cu pachetele de cărți și jucătorul pentru care se aplică acești parametri.

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

"playerOneDecks": {
        "nrCardsInDeck": 2,
        "nrDecks": 2,
        "decks": [
          [
            {
              "mana": 1,
              "attackDamage": 1,
              "health": 7,
              "description": "Standard card: A warrior who is never afraid of battle, no matter the costs",
              "colors": [
                "Red",
                "Purple"
              ],
              "name": "Goliath"
            },
            {
              "mana": 1,
              "attackDamage": 0,
              "health": 1,
              "description": "Premium card: Nobody suspects an outcast, not even the Gods.",
              "colors": [
                "Black",
                "White",
                "Red"
              ],
              "name": "The Cursed One"
            },
          ],
          [
            {
              "mana": 1,
              "attackDamage": 2,
              "health": 2,
              "description": "Premium card: Fear him. Your last breath is now.",
              "colors": [
                "Red",
                "Blue",
                "Pink"
              ],
              "name": "Berserker"
            },
            {
              "mana": 1,
              "attackDamage": 0,
              "health": 1,
              "description": "Standard card: Thrown from the highest cliff, revenge will be his glory.",
              "colors": [
                "Pink",
                "Purple"
              ],
              "name": "The Cursed One"
            }
          ]
        ]
      }
    },
    {
      "playerTwoDecks": {
        "nrCardsInDeck": 2,
        "nrDecks": 2,
        "decks": [
          [
            {
              "mana": 1,
              "attackDamage": 3,
              "health": 2,
              "description": "Premium card: Fear him. Your last breath is now.",
              "colors": [
                "Purple",
                "Yellow"
              ],
              "name": "Berserker"
            },
            {
              "mana": 1,
              "attackDamage": 0,
              "health": 3,
              "description": "Standard card: Thrown from the highest cliff, revenge will be his glory.",
              "colors": [
                "Black",
                "White",
                "Red"
              ],
              "name": "The Cursed One"
            }
          ],
          [
            {
              "mana": 1,
              "attackDamage": 1,
              "health": 2,
              "description": "Standard card: Trained by the best, nothing will catch him off-guard.",
              "colors": [
                "Black",
                "Grey",
                "Blue"
              ],
              "name": "Sentinel"
            },
            {
              "mana": 1,
              "attackDamage": 6,
              "health": 4,
              "description": "Standard card: Death incarnate. No better time to die than now.",
              "colors": [
                "Orange",
                "Red",
                "Black"
              ],
              "name": "The Ripper"
            }
          ]
        ]
      }
    }

Acest pas se întâmplă doar când se începe un test nou. Dacă jucătorii doresc să joace alt joc în cadrul aceluiași test, AI-ul vă va specifica doar ce pachet doresc să folosească pentru următorul meci, astfel repetându-se pasul următor. Ordinea pachetelor rămâne aceeași.

La finalul unei partide dintre cei doi jucători aceștia trebuie să aibă posibilitatea să aleagă același pachet ca în jocul trecut pentru un nou joc, deci va trebui ca pachetul folosit să poată să revină exact la starea inițială, înainte de procesul de amestecare.

Pregătirea jocului

Fiecare jucător își alege un deck, acesta fiind specificat de AI în input. Deck-ul ales de fiecare jucător va fi amestecat la începutul jocului, deoarece dorim ca jocul sa nu fie previzibil. Pentru a ne asigura că rezultatele vor fi deterministe, server-ul va trimite la input și un seed folosit ca parametru pentru procesul de amestecare.

Server-ul alege câte un erou aleator pentru fiecare jucător, aceștia fiind specificați la input.

Se alege un jucător care să înceapă prima rundă, fiind specificat la input.

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

"startGame": {
        "playerOneDeckIdx": 1,
        "playerTwoDeckIdx": 0,
        "shuffleSeed": 882661,
        "playerOneHero": {
          "mana": 2,
          "description": "Standard card: Absolute order, a soldier must devote his life to his General.",
          "colors": [
            "Red",
            "Black",
            "Purple"
          ],
          "name": "General Kocioraw"
        },
        "playerTwoHero": {
          "mana": 2,
          "description": "Premium card: Glory to all we see, glory to all he is, glory to everyone who will try to escape him.",
          "colors": [
            "Black",
            "Red",
            "Grey"
          ],
          "name": "Lord Royce"
        },
        "startingPlayer": 1
      }

Pentru amestecarea cărților, vă recomandăm să folosiți metodele din clasa Random Shuffle.

Pentru a avea pachetul amestecat cum trebuie de fiecare dată trebuie să reinstanțiați clasa Random la fiecare amestecare.

Începutul unei runde

La începutul fiecărei runde, ambii jucători primesc prima carte disponibilă din pachetul de cărți ales. Cartea primită de fiecare jucător va fi adăugată în “mâna” acestuia. Cartea primită este adăugată la sfârșitul listei de cărți din mână.

Dacă nu mai există cărți în pachet, nu mai trebuie trasă nicio carte în mână.

De asemenea, ambii jucători primesc mana la începutul fiecărei runde. Mana pe care o primesc jucătorii în prima rundă este 1, urmând ca aceasta sa fie incrementată pana la 10. In momentul în care se ajunge la 10 mana, aceasta nu va mai fi incrementată.

Exemplu primire mana Să presupunem că jucătorii nu folosesc deloc mana. În prima rundă fiecare jucător primește 1 mana. După ce își fac cei 2 tura, începe runda a 2-a. Fiecare jucător primește 2 mana, ceea ce înseamnă că acum ambii jucători au 3 mana. După ce își fac ambii tura, începe runda a 3-a. Cei 2 jucători primesc 3 mana, ceea ce înseamnă că fiecare are 6 mana acum ș.a.m.d.

Prima carte care va fi trasă de jucător va fi cea de la începutul listei de cărți amestecate ale jucătorului. Mai exact, cea cu index-ul 0 în lista de cărți din deck.

  • O rundă este formată din două ture, una per jucător.
  • În cadrul unei ture pot fi făcute mai multe acțiuni de către jucătorul curent.

Sfârșitul unei ture

La finalul turei unui jucător, cărțile acestuia care au fost marcate ca fiind “frozen” sunt demarcate, întrucât acestea au stat o tura.

Totodată, se verifică dacă ambii jucători și-au sfârșit turele în cadrul rundei curente. Dacă această condiție este adevarată, se trece la următoarea rundă si se aplică pașii descriși mai sus.

Marcarea sfârșitului unei ture va fi precizată explicit de AI în input pentru jucătorul curent.

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
    "command": "endPlayerTurn"
}


Plasarea unei cărți pe masă

Cărțile din mână pot fi plasate pe masă, cu condiția ca acestea să nu aibă costul de mana mai mare decât mana jucătorului.

Cărțile trebuie plasate pe masă, pe rândul aferent jucătorului, dar și al tipului de carte (conform secțiunilor de mai sus).

AI-ul vă va preciza în input indexul cărții din mână pe care dorește să o plaseze pe masa.

Cazurile invalide trebuie testate la acest pas în această ordine:

  1. Se verifică dacă cartea are costul mai mic decât mana jucătorului, altfel se printează: ”Not enough mana to place card on table.”
  2. Se verifică dacă rândul pe care trebuie plasata cartea nu este plin, altfel se printează: ”Cannot place card on table since row is full.”

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
    "command": "placeCard",
    "handIdx": 1
}

Click pentru output în cazul unei erori

Click pentru output în cazul unei erori

{
    "command": "placeCard",
    "handIdx": 0,
    "error": "Not enough mana to place card on table."
}

  • Un jucător poate plasa toate cărțile din mână în cadrul aceleași ture dacă îndeplinește condițiile precizate.
  • În input nu se precizează mâna cărui jucător trebuie actualizată, drept urmare aceasta comandă va fi activată pentru jucătorul curent.

Comanda de atac a unei cărți

Fiecare carte plasată pe masă poate ataca altă carte care se află pe masa. În acest pas, se dorește ca viața minion-ului adversar țintit să scadă cu atâtea puncte câte puncte are cartea atacatorului în câmpul attackDamage. Daca viața cărții atacate ajunge să fie ⇐ 0, aceasta este eliminată de pe masă. O carte poate ataca o altă carte o singură dată per tură. După ce cartea și-a folosit atacul, aceasta nu va mai putea ataca altă carte sau erou decât tura următoare.

AI-ul va specifica în input coordonatele de pe masă ale cărții atacate și ale cărții atacatorului.

Cazurile invalide care trebuie testate în acest pas sunt:

  1. Se verifică dacă acea carte atacată este o carte a inamicului, altfel se printează: ”Attacked card does not belong to the enemy.”.
  2. Se verifică dacă acea carte a atacatorului nu a atacat/folosit abilitatea specială deja tura aceasta, altfel se printează: ”Attacker card has already attacked this turn.”.
  3. Se verifică dacă acea carte a atacatorului nu este “frozen”, altfel se printează: ”Attacker card is frozen.”.
  4. Se verifică dacă există o carte Tank pe rândurile adversarului.
    1. Se verifică mai departe dacă aceasta a fost selectată pentru atac, altfel se printează: ”Attacked card is not of type 'Tank’.”.

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
    "command": "cardUsesAttack",
    "cardAttacker": {
         "x": 1,
         "y": 0
     },
     "cardAttacked": {
         "x": 3,
         "y": 0
     }
}

Click pentru output în cazul unei erori

Click pentru output în cazul unei erori

{
    "command": "cardUsesAttack",
    "cardAttacker": {
      "x": 3,
      "y": 1
    },
    "cardAttacked": {
      "x": 3,
      "y": 1
    },
    "error": "Attacked card does not belong to the enemy."
}

  • Dacă o carte este omorâtă, aceasta nu se introduce înapoi în deck-ul jucătorului atacat.
  • Dacă adversarul are mai multe cărți de tipul Tank pe rândurile lui, atunci atacatorul trebuie să atace una din cărțile de tipul Tank.

Comanda de utilizare a abilității unei carti

Fiecare carte plasată pe masă își poate folosi abilitatea pe altă carte care se află pe masă. Fiecare abilitate specială va avea un efect diferit asupra celui atacat, abilitățile fiind descrise și asociate cu anumiți minioni conform secțiunilor de mai sus.

AI-ul va specifica în input coordonatele de pe masă a cărții atacate și a cărții atacatorului.

Cazurile invalide trebuie testate la acest pas în această ordine:

  1. Se verifică dacă cartea atacatorului nu este “frozen”, altfel se printează: ”Attacker card is frozen.”.
  2. Se verifică dacă cartea atacatorului nu a atacat/folosit abilitatea specială deja tura aceasta, altfel se printează: ”Attacker card has already attacked this turn.”.
  3. Se verifică dacă atacatorul este Disciple.
    1. Se verifică dacă cartea atacată este o carte aliată, altfel se printează: ”Attacked card does not belong to the current player.”.
  4. Se verifică dacă atacatorul este The Ripper, Miraj sau The Cursed One
    1. Se verifică dacă cartea atacată este o carte a inamicului, altfel se printează: ”Attacked card does not belong to the enemy.”.
    2. Se verifică dacă există o carte Tank pe rândurile adversarului.
      1. Se verifică mai departe dacă aceasta a fost selectată pentru atac, altfel se printează: ”Attacked card is not of type 'Tank’.”.

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
    "command": "cardUsesAbility",
    "cardAttacker": {
        "x": 2,
        "y": 0
     },
     "cardAttacked": {
        "x": 1,
        "y": 0
     }
}

Click pentru output în cazul unei erori

Click pentru output în cazul unei erori

{
    "command": "cardUsesAbility",
    "cardAttacker": {
      "x": 3,
      "y": 0
    },
    "cardAttacked": {
      "x": 3,
      "y": 0
    },
    "error": "Attacker card is frozen."
}

  • Se garantează că în input atacatorul este o carte care are abilitate specială.
  • Dacă atacatorul este Disciple, nu trebuie să se verifice că minionul atacat este Tank.
  • Dacă o carte este omorâtă (caz posibil pentru abilitatea Shapeshift), aceasta nu se introduce înapoi în deck-ul jucătorului atacat.
  • Dacă adversarul are mai multe cărți de tipul Tank pe rândurile lui, este suficient ca cel atacat să fie oricare dintre aceste cărți.

Comanda de utilizare a atacului unei cărți asupra eroului

Fiecare carte plasată pe masă poate ataca eroul inamicului. În acest pas, se dorește ca viața eroului să scadă cu atâtea puncte câte are atacatorul atributul attackDamage. La finalul comenzii se verifică dacă eroul inamicului a murit, în acest caz jocul fiind câștigat de jucătorul curent.

AI-ul va specifica în input coordonatele de pe masă a cărții atacatorului.

Cazurile invalide trebuie testate la acest pas în această ordine:

  1. Se verifică dacă cartea atacatorului nu este “frozen”, altfel se printează: ”Attacker card is frozen.”
  2. Se verifică dacă cartea atacatorului nu a atacat/folosit abilitatea specială deja tura aceasta, altfel se printează: ”Attacker card has already attacked this turn.”
  3. Se verifică dacă există o carte Tank pe rândurile adversarului.
    1. Se verifică mai departe dacă aceasta a fost selectată pentru atac, altfel se printează: ”Attacked card is not of type 'Tank’.”

În urma atacului, se va verifica dacă eroul adversar mai este în viață. Dacă eroul adversar a fost învins, atunci se va afișa în funcție de caz: ”Player one killed the enemy hero.” sau “Player two killed the enemy hero.”.

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
    "command": "useAttackHero",
    "cardAttacker": {
        "x": 3,
        "y": 0
     }
}

Click pentru output în cazul unei erori

Click pentru output în cazul unei erori

  {
    "command": "useAttackHero",
    "cardAttacker": {
      "x": 2,
      "y": 0
    },
    "error": "Attacked card is not of type 'Tank'."
  }

Click pentru output în cazul în care unul din eroi este învins

Click pentru output în cazul în care unul din eroi este învins

{
   "gameEnded": "Player one killed the enemy hero."
}

  • Se permit doar atacuri directe către eroi, nu și folosirea abilităților speciale pe eroi.
  • Nu se permite atacarea propriilor eroi.

Dacă un erou a fost omorât, jocul curent se termină instant. Mai exact, nu se va mai schimba nici tura și nici runda, iar fiecare jucător nu va mai primi mana sau cărți.

Comanda de utilizare a abilității unui erou

Fiecare erou poate ataca un rând de pe masă. Abilitățile eroilor sunt descrise în secțiunile de mai sus, aceștia având posibilitatea să atace doar minioni ai adversarului sau minioni aliați în funcție de abilitate.

AI-ul vă specifică în input rândul ales pentru abilitate.

Cazurile invalide trebuie testate la acest pas în această ordine:

  1. Se verifică dacă jucătorul are destulă mana ca să folosească eroul, altfel se printează: ”Not enough mana to use hero's ability.”
  2. Se verifică dacă eroul nu a atacat deja tura aceasta, altfel se printează: ”Hero has already attacked this turn.”
  3. Se verifică dacă eroul este Lord Royce sau Empress Thorina.
    1. Se verifică mai departe dacă rândul selectat pentru abilitate este un rând al adversarului, altfel se printează: “Selected row does not belong to the enemy.”
  4. Se verifică dacă eroul este General Kocioraw sau King Mudface.
    1. Se verifică mai departe dacă rândul selectat pentru abilitate este un rând al jucătorului curent, altfel se printează: “Selected row does not belong to the current player.”

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
    "command": "useHeroAbility",
    "affectedRow": 3
}

Click pentru output în cazul unei erori

Click pentru output în cazul unei erori

{
    "command": "useHeroAbility",
    "affectedRow": 3,
    "error": "Selected row does not belong to the enemy."
}

  • Se garantează în input că eroii nu vor ataca rânduri goale.
  • Abilitățile eroilor nu iau în considerare prezența cărților de tip Tank.
  • Dacă se folosește abilitatea Sub-Zero pe o carte care este deja marcată ca fiind frozen, aceasta va rămâne tot frozen o singură tura.
  • Dacă o carte este omorâtă (caz posibil pentru abilitatea Low Blow), aceasta nu se introduce înapoi în deck-ul jucătorului.

Comenzi de debug

Pe parcursul jocului, server-ul vă poate cere oricând anumiți parametri pentru a verifica implementarea voastră.

Aflarea cărților din mână

Pentru această comandă trebuie să puneți în output toate cărțile care se află în mâna jucătorului dat ca parametru la input.

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
    "command": "getCardsInHand",
    "playerIdx": 2
}

Click pentru un exemplu de output pentru acest pas

Click pentru un exemplu de output pentru acest pas

{
    "command": "getCardsInHand",
    "playerIdx": 2,
    "output": [
      {
        "mana": 4,
        "attackDamage": 2,
        "health": 7,
        "description": "Premium card: No matter how many, his body will fight. Save your breath for the Mountain is strong.",
        "colors": [
          "Red",
          "Purple"
        ],
        "name": "Goliath"
      },
      {
        "mana": 4,
        "attackDamage": 4,
        "health": 4,
        "description": "Standard card: When he is near, close your eyes. They betray your mind.",
        "colors": [
          "Red",
          "Black"
        ],
        "name": "Miraj"
      }
    ]
}

Aflarea cărților din pachet

Pentru această comandă trebuie să puneți în output toate cărțile care se află în deck-ul jucătorului dat ca parametru la input.

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
    "command": "getPlayerDeck",
    "playerIdx": 2
}

Click pentru un exemplu de output pentru acest pas

Click pentru un exemplu de output pentru acest pas

{
    "command": "getPlayerDeck",
    "playerIdx": 2,
    "output": [
      {
        "mana": 3,
        "attackDamage": 3,
        "health": 5,
        "description": "Standard card: He has seen everything, his mind has never been asleep since the making of the realm.",
        "colors": [
          "Brown",
          "Blue"
        ],
        "name": "Warden"
      }
    ]
}

Afișarea cărților de pe masă

Pentru această comandă trebuie să afișați la output toate cărțile prezente pe masă. Cărțile trebuie printate începând de la rândul 0, coloana 0 până la rândul 3, coloana 4, conform pozei de mai sus.

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
    "command": "getCardsOnTable"
}

Click pentru un exemplu de output pentru acest pas

Click pentru un exemplu de output pentru acest pas

{
    "command": "getCardsOnTable",
    "output": [
      {
        "mana": 3,
        "attackDamage": 6,
        "health": 3,
        "description": "Premium card: Ravished by the winds of hell, no soul is to be forgiven.",
        "colors": [
          "Orange",
          "Red",
          "Black"
        ],
        "name": "The Ripper"
      },
      {
        "mana": 5,
        "attackDamage": 4,
        "health": 3,
        "description": "Standard card: When he is near, close your eyes. They betray your mind.",
        "colors": [
          "Yellow",
          "Orange",
          "Red"
        ],
        "name": "Miraj"
      },
      {
        "mana": 4,
        "attackDamage": 6,
        "health": 3,
        "description": "Standard card: Death incarnate. No better time to die than now.",
        "colors": [
          "Orange",
          "Red",
          "Black"
        ],
        "name": "The Ripper"
      },
      {
        "mana": 3,
        "attackDamage": 1,
        "health": 8,
        "description": "Standard card: A warrior who is never afraid of battle, no matter the costs",
        "colors": [
          "Brown",
          "Black",
          "Grey"
        ],
        "name": "Goliath"
      },
      {
        "mana": 3,
        "attackDamage": 5,
        "health": 3,
        "description": "Premium card: Ravished by the winds of hell, no soul is to be forgiven.",
        "colors": [
          "Black",
          "Yellow"
        ],
        "name": "The Ripper"
      },
      {
        "mana": 3,
        "attackDamage": 2,
        "health": 5,
        "description": "Standard card: He has seen everything, his mind has never been asleep since the making of the realm.",
        "colors": [
          "Black",
          "White",
          "Green"
        ],
        "name": "Warden"
      },
      {
        "mana": 4,
        "attackDamage": 4,
        "health": 4,
        "description": "Standard card: When he is near, close your eyes. They betray your mind.",
        "colors": [
          "Red",
          "Black"
        ],
        "name": "Miraj"
      },
      {
        "mana": 2,
        "attackDamage": 3,
        "health": 6,
        "description": "Standard card: He has seen everything, his mind has never been asleep since the making of the realm.",
        "colors": [
          "Brown",
          "Blue"
        ],
        "name": "Warden"
      },
      {
        "mana": 4,
        "attackDamage": 2,
        "health": 7,
        "description": "Premium card: No matter how many, his body will fight. Save your breath for the Mountain is strong.",
        "colors": [
          "Red",
          "Purple"
        ],
        "name": "Goliath"
      },
      {
        "mana": 4,
        "attackDamage": 1,
        "health": 7,
        "description": "Premium card: No matter how many, his body will fight. Save your breath for the Mountain is strong.",
        "colors": [
          "Red",
          "Purple"
        ],
        "name": "Goliath"
      }
    ]
  }

  • Cărțile de pe masă se vor printa sub forma unei liste începând de la rândul 0 până la rândul 3, de la stânga la dreapta.

Aflarea jucătorului activ

Pentru această comandă trebuie să printați la output jucătorul activ (adică jucătorul care nu și-a încheiat încă tura).

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
    "command": "getPlayerTurn"
}

Click pentru un exemplu de output pentru acest pas

Click pentru un exemplu de output pentru acest pas

{
    "command": "getPlayerTurn",
    "output": 2
}

Aflarea eroului unui jucător

Pentru această comandă trebuie să printați la output eroul jucătorului dat ca parametru la input.

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
    "command": "getPlayerHero",
    "playerIdx": 1
}

Click pentru un exemplu de output pentru acest pas

Click pentru un exemplu de output pentru acest pas

{
    "command": "getPlayerHero",
    "playerIdx": 1,
    "output": {
      "mana": 2,
      "description": "Premium card: An army is not sufficient, he is ready obliterate, it is time for chaos.",
      "colors": [
        "Red",
        "Black",
        "Purple"
      ],
      "name": "General Kocioraw",
      "health": 30
    }
  }

Afișarea unei cărții la o poziție dată

Pentru această comandă trebuie să printați la output cartea aflată la poziția dată în input.

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
   "command": "getCardAtPosition",
   "x": 3,
   "y": 0
}

Click pentru un exemplu de output pentru acest pas

Click pentru un exemplu de output pentru acest pas

{
    "command": "getCardAtPosition",
    "x": 3,
    "y": 0,
    "output": {
      "mana": 2,
      "attackDamage": 0,
      "health": 3,
      "description": "Premium card: Nobody suspects an outcast, not even the Gods.",
      "colors": [
        "Black",
        "White",
        "Red"
      ],
      "name": "The Cursed One"
    }
}

Click pentru un exemplu de eroare pentru acest pas

Click pentru un exemplu de eroare pentru acest pas

{
    "command": "getCardAtPosition",
    "x": 3,
    "y": 0,
    "output": "No card available at that position."
}

Dacă nu există o carte la poziția dată, veți printa “No card available at that position.”.

Afișarea manei unui jucător

Pentru această comandă trebuie să printați la output mana unui jucător primit la input.

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
    "command": "getPlayerMana",
    "playerIdx": 2
}

Click pentru un exemplu de output pentru acest pas

Click pentru un exemplu de output pentru acest pas

{
    "command": "getPlayerMana",
    "playerIdx": 2,
    "output": 3
}

Afișarea cărților de pe masă care sunt "frozen"

Pentru această comandă trebuie să printați la output toate cărțile de pe masă care au flag-ul “frozen” setat. Ordinea printării trebuie să respecte ordinea cărților de pe masă, mai exact se începe de la linia 0, coloana 0 și se termină la linia 3, coloana 4.

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
   "command": "getFrozenCardsOnTable"
}

Click pentru un exemplu de output pentru acest pas

Click pentru un exemplu de output pentru acest pas

{
  "command": "getFrozenCardsOnTable",
  "output": [
    {
      "mana": 1,
      "attackDamage": 6,
      "health": 4,
      "description": "Premium card: Ravished by the winds of hell, no soul is to be forgiven.",
      "colors": [
        "Orange",
        "Red",
        "Black"
      ],
      "name": "The Ripper"
    }
  ]
}

Dacă nu există cărți pe masă care să fie “frozen”, veți afișa o listă goală.

Comenzi pentru statistici

Statisticile sunt reprezentate de numărul total de jocuri la care a participat fiecare jucator și numărul total de câștiguri

Afișarea numărului total de jocuri jucate

Pentru această comandă va trebui să afișați la output numărul total de jocuri încheiate.

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
   "command": "getTotalGamesPlayed"
}

Click pentru un exemplu de output pentru acest pas

Click pentru un exemplu de output pentru acest pas

{
   "command": "getTotalGamesPlayed",
   "output": 2
}

Afișarea numărului total de jocuri câștigate

Pentru această comanda va trebui să afișați la output numărul total de jocuri câștigate de un jucător. Vor exista două comenzi pentru fiecare jucător (“getPlayerOneWins” si “getPlayerTwoWins”).

Click pentru input-ul asociat acestui pas

Click pentru input-ul asociat acestui pas

{
   "command": "getPlayerOneWins"
}

Click pentru un exemplu de output pentru acest pas

Click pentru un exemplu de output pentru acest pas

{
   "command": "getPlayerTwoWins",
   "output": 2
}


Scheletul de cod

În rezolvarea temei va fi nevoie de folosirea unor obiecte pentru stocarea și maparea datelor primite în format JSON. Scheletul temei vă oferă mai multe clase utile pentru citirea și rularea temei. Acestea se regăsesc în cadrul scheletului astfel:

  • Clasele de input din cadrul pachetului fileio : Acestea se vor folosi pentru parsarea datelor de input. Astfel preluați toate informațiile necesare pentru a crea propriile clase pentru rezolvarea temei.
  • Clasa Main care este punctul de intrare pentru logica de rezolvare a temei.

Pentru a întelege mai bine cum funcționează citirea/scrie în fișierele JSON vă recomandăm să citiți Json & Jackson.

Implementarea voastră va începe din funcția Main.action, unde veți adăuga în ArrayNode output pe parcursul execuției toate output-urile comezilor date. De asemenea, aveți un mic exemplu în schelet despre cum puteți face asta.

Output-ul nu trebuie formatat ca în ref-uri, fiindcă se verifică conținutul obiectelor și array-urilor JSON, nu textul efectiv. Cu toate acestea, dacă folosiți Jackson, vă recomandăm să utilizați PrettyPrinter Documentație PrettyPrinter. Totodată, pentru a înțelege cum se poate realiza scrierea în fișierele JSON de output, vă sugerăm să consultați JSON Array.

Aveți în folder-ul “lib” toate dependințele necesare pentru rularea temei, mai exact bibliotecile Jackson.

Execuția temei

  1. Se încarcă datele citite din fișierul de test, în format JSON, în obiecte.
  2. Se primesc secvențial acțiuni și se execută pe măsură ce sunt primite, rezultatul lor având efect asupra Repository-ului.
  3. După executarea unei acțiuni, se afișează rezultatul ei în fișierul JSON de ieșire.
  4. La terminarea tuturor acțiunilor se termină și execuția programului.

  • După ce clonați repo-ul de pe GitHub, vă rugăm să vă faceți un repository propriu privat în care să vă puneți doar conținutul folder-ului “tema” de pe repo-ul echipei de POO. Dacă nu puneți folder-ul cu tema la alta cale, nu o să puteți să faceți schimbări in Git, deoarece vă aflați în rădăcina repository-ului echipei de POO.
  • Pentru ca checker-ul să funcționeze trebuie să deschideți tema din Intellij la calea unde se află folderele “src”, “lib”, “ref”, “input”. Aveți folder-ul .idea pregenerat ca să vă ajute in acest sens. De asemenea, fișier-ul .iml contine calea către bibliotecile Jackson. Dacă aveți probleme stergeți folder-ul .idea si fișierul .iml si generațile voi din nou din Intellij.

Recomandări

  • Tema a fost concepută pentru a vă testa cunoștințele dobândite în urma parcurgerii laboratoarelor 1-3, aceasta putând fi rezolvată doar cu noțiunile invățate din acele laboratoare.
  • Tema fiind o specificație destul de stufoasă recomandăm citirea de cel putin două ori a enunțului inainte de inceperea implementării.
  • Pentru depanarea diferențelor dintre output-ul vostru si fișierele ref, vă recomandăm acest site.
  • Verificați periodic această pagină, deoarece scheletul/cerința pot suferi modificări în urma unor erori din partea noastră.

Evaluare

Punctajul constă din:

  • 80p implementare - trecerea testelor
  • 10p coding style (vezi checkstyle)
  • 5p README clar, concis, explicații axate pe design (flow, interacțiuni)
  • 5p folosire git pentru versionarea temei

Pe pagina Indicații pentru teme găsiți indicații despre scrierea readme-ului și baremul folosit la corectarea temei. Vă incurajăm să treceți prin el cel puțin odată.

Pentru a primi punctajul pentru Git, după ce ați terminat tema și ați făcut toate commit-urile, executați comanda git log > git_log.txt in rădăcina proiectului și adăugați fisierul in arhiva trimisă.

Pentru folosirea tool-ului Git vă punem la dispoziție un tutorial actualizat și amplu despre el la acest link și aveți de asemenea și un tutorial despre comenzile pe care puteți să le dați din IntelliJ la acest link.

Depunctările pentru designul și organizarea codului se vor scădea din punctajul testelor. Dacă vor apărea depunctări specifice temei în momentul evaluării, nemenționate pe pagina cu depunctări generale, ele se vor încadra în limitele de maxim 10p pentru design, 5p pentru readme. Dacă tema nu respecta cerințele, sau are zero design OOP atunci se pot face depunctari suplimentare.

Folosirea git pentru versionare va fi verificata din folderul .git pe care trebuie să îl includeți în arhiva temei. Punctajul se va acorda dacă ați făcut minim 3 commit-uri relevante și cu mesaj sugestiv. NU este permis să aveți repository-urile de git publice până la deadline-ul hard.

Bonusuri: La evaluare, putem oferi bonusuri pentru design foarte bun, cod bine documentat dar și pentru diverse elemente suplimentare alese de voi.

  • Temele vor fi testate împotriva plagiatului. Orice tentativă de copiere va duce la anularea punctajului de pe parcursul semestrului şi repetarea materiei atât pentru sursă(e) cât şi pentru destinație(ii), fără excepție.
  • Aveți grijă să nu puneți pe Vmchecker fișiere .idea sau .iml.

Checkstyle

Unul din obiectivele temei este învățarea respectării code-style-ului limbajului pe care îl folosiți. Aceste convenții (de exemplu cum numiți fișierele, clasele, variabilele, cum indentați) sunt verificate pentru temă de către tool-ul checkstyle.

Pe pagina de Recomandări cod găsiți câteva exemple de coding style.

Dacă numărul de erori depistate de checkstyle depășește 30, atunci punctele pentru coding-style nu vor fi acordate. Dacă punctajul este negativ, acesta se trunchiază la 0.

Exemple:

  • punctaj_total = 100 și nr_erori = 200nota_finala = 90
  • punctaj_total = 100 și nr_erori = 29nota_finala = 100
  • punctaj_total = 80 și nr_erori = 30nota_finala = 80
  • punctaj_total = 80 și nr_erori = 31nota_finala = 70

Upload temă

Arhiva o veţi urca pe VMChecker, unde sunt si informații despre structura ei.

FAQ

Q: Pot folosi biblioteca “X”?
A: Dacă doriți să folosiți o anumită bibliotecă vă recomandăm să întrebați pe forum, ca apoi să o validăm și să o includem și pe Vmchecker.

Q: Am descoperit edge case-ul “Y”, trebuie să îl tratez?
A: Nu. Toate datele necesare pentru soluționarea temei vă sunt date in cerință. Dacă totuși am omis ceva ne puteți contacta pe forum.

Q: Pot folosi clase de tip “Enum” pentru constante?
A: Da.

Q: Ce JDK recomandați?
A: 11

Q: Pot să fac în orice ordine testele?
A: Da! Testele au fost concepute sa fie cât mai decuplate și să testeze câte o funcționalitate în întregime. Cu toate astea, vă recomandăm să implementați mai întâi testele 01, 02, 03 deoarece reprezintă preambulul fiecărui joc.

Resurse și linkuri utile

poo-ca-cd/teme/2024/tema.txt · Last modified: 2024/11/21 11:35 by florian_luis.micu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0