V tomto tutoriálu si spolu naprogramujeme karetní hru Černý Petr. Použijeme programovací jazyk Java a zaměříme se na to, abychom použili OOP přístup, tedy objektově orientované programování.
Pravidla hry
Nejprve si musíme zanalyzovat danou hru. To uděláme tak, že si řekneme a určíme pravidla. Ve hře je 33 karet. Jedna karta nemá pár, ostatní ho mají. Hru může hrát 3 až 6 hráčů. Všechny karty se rozdají mezi hráče. Automaticky si hráči vytřídí z ruky karty, které mají páry. S ostatními začíná hra. Ten, co má nejvíc karet, nechá táhnout hráče po své pravici. Pokud ten hráč získal pár, tak ho vyloží a další hráč od něj táhne kartu. Pokud hráč přišel o všechny karty, už víc nehraje. Komu zůstane poslední karta, ten prohrál hru.
Analýza hry - vytváření objektů
Nyní je čas připravit si popis našich tříd, rozhraní a podobně. Ve zkratce, uvažujeme nad pravidly, okolnostmi a členy dané hry a chceme je přetvořit na objekty.
Čím obecněji napíšeme naše objekty, tím lépe pro jejich znovupoužitelnost. Pokud bychom chtěli někdy naprogramovat karty žolíkové, sedmové nebo ledajaké jiné, tak si nechme tuto možnost. Tedy například vytváření instancí karet nedávejme do třídy balíku, ale jinde.
Postup:
vytvořím kartu
vytvořím balík karet
vytvořím hráče
interakci s uživatelem
správu hry
logiku hry Černý Peter
Karta
Každá hrací všeobecná karta má nějaké specifikum. Je to král srdeční, král pikový a podobně. V našem případě máme páry a každá karta v páru je jiná, společné mají to, že jsou páry. Jako například v žolíkových kartách máme 4 krále. Každý je jiný, ale mají společné, že jsou to králové.
package sk.jaro.CiernyPeter;
public class Karta {
private int cisloKarty; //každá karta ma iné číslo
private int cisloParu; //každý prá má iné číslo, len dve karty majú to
isté číslo páru
public Karta(int cisloKarty, int cisloParu) {
this.cisloKarty = cisloKarty;
this.cisloParu = cisloParu;
}
public int getCisloKarty() {
return cisloKarty;
}
public int getCisloParu() {
return cisloParu;
}
}
Balíček karet
Dále budeme potřebovat tyto karty uložit do balíčku. Každá hra má několik karet, které tvoří balíček. Takže náš balíček bude obsahovat seznam karet. Co lze dělat s balíkem? Například míchat karty, nebo z balíku můžeme vyjmout kartu. Když vybírám karty nebo míchám karty, tak tam musí nějaké být. Protože pokud vyberu postupně všechny karty z balíku, tak nakonec budu mít balík prázdný. Zkuste míchat prázdný balík karet :) Proto si vytvořím i pomocnou metodu, která zjistí, zda je balík prázdný nebo ne.
package sk.jaro.CiernyPeter;
import java.util.Collections;
import java.util.List;
public class BalikKariet {
private List<Karta> karty; //implementacia listu pre zachovanie poradia
public BalikKariet(List<Karta> karty) {
this.karty = karty;
}
public List<Karta> getKarty() {
return karty;
}
public void zamiesajKarty(){
if(!jeBalikPrazdny())
Collections.shuffle(karty);
}
private boolean jeBalikPrazdny(){
return karty == null || karty.isEmpty();
}
public Karta getKartu(){
Karta karta = null;
if(!jeBalikPrazdny()) {
karta = karty.get(0); //vytiahnem prvú kartu
karty.remove(karta); //kartu odstránim z balíku
}
return karta;
}
}
Hráč
Do každé hry potřebuji hráče, tedy někoho, kdo bude danou hru hrát. Rozhodl jsem se, že hráči dám jméno a karty v ruce. Když vytvářím nového hráče pomocí new, tak se zavolá konstruktor dané třídy a tam si všimni, že jsem mu do ruky nedal nic, tedy tam má prázdno. To proto, že ještě nedostal žádnou kartu při rozdávání, ale musí mít nějaké úložiště kde mu je dám :)
Je tam ještě metoda, která má na starosti odstranit z ruky hráče všechny páry. Kdo by si to chtěl nějak zobecnit, tak může. Tedy do objektu Hrac, by dal jen metodu pro odstranění jedné karty, nebo seznamu karet. A které karty to budou to nechá na jiný objekt, který spravuje pravidla hry Černý Peter.
package sk.jaro.CiernyPeter;
import java.util.ArrayList;
import java.util.List;
public class Hrac {
private String meno;
private List<Karta> kartyVRuke;
public Hrac(String meno) {
this.meno = meno;
this.kartyVRuke = new ArrayList<>();
}
public String getMeno() {
return meno;
}
public List<Karta> getKartyVRuke() {
return kartyVRuke;
}
public void odstranParyZRuky() {
ArrayList<Karta> akeKartyOdstraniZRuky = new ArrayList<>();
for(Karta karta : kartyVRuke){
try {
for (Karta k : kartyVRuke) {
if (karta.getCisloParu()
== k.getCisloParu()
&& karta.getCisloKarty() != k.getCisloKarty()) {
akeKartyOdstraniZRuky.add(karta);
akeKartyOdstraniZRuky.add(k);
break;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
kartyVRuke.removeAll(akeKartyOdstraniZRuky);
}
}
Ovládání hry
Jakou chceš udělat aplikaci? Jak chceš komunikovat s uživatelem? Přes grafické rozhraní? Přes konzoli, nebo jinak? Nyní budeme dělat konzolovou interakci , ale pokud bys chtěl dělat v budoucnu grafické rozhraní, tak je vynikající idea udělat interface, tedy rozhraní, kde popíšu metody jaké chci používat pro interakci s uživatelem. Potom když budeš dělat grafické rozhraní, tak si jen zaimplementuješ toto nové rozhraní a někde v kódu hry řekneš, že nyní používat tuto implementaci, a nemusíš přepisovat i celou hru, neboť metody jsou tam stejné, jen z jiného zdroje.
Co potřebujeme vypsat uživateli, nebo co od něj chci získat? Počet hráčů, jejich jména, jakou kartu chceme hráči vzít a chceme vypsat konec hry. Pokud chceš něco víc, tak si to klidně dodělej.
package sk.jaro.CiernyPeter.rozhrania;
import sk.jaro.CiernyPeter.Hra;
import sk.jaro.CiernyPeter.Hrac;
public interface IOvladanieHry {
int vyberPocetHracov();
Hrac getMenoHraca(int i);
int zoberKartu(Hrac hrac1, Hrac hrac2);
void vypisKtoPrehral(Hra hra);
}
Nyní si musíme zaimplementovat toto rozhraní. Nyní máme jen předpis metod ale ne jejich nitro. Budeme používat konzoli, kterou budeme číst pomocí scanneru a na konci hry si uzavřeme scanner. Každá metoda je jednoduchá, vypíšu na konzoli co chci a potom nechám uživatele, aby mi to napsal.
Všimni si, když bereš nextInt(), tak se to pokusí vzít číslo. Pokud najde něco jiného je to chyba a tu ošetříme. Klidně si dodělej více ošetření, podmínek a výpisů. Potom ale musíš vzít i zbytek. Nebo co udělal uživatel? Zadal číslo a stiskl enter. Ty jsi vzal jen to číslo, ale ne i enter. Proto tam máme ještě nextLine - to nám vezme zbytek řádku is enterem.
Černý Peter bude hráč, který zůstal poslední ve hře.
package sk.jaro.CiernyPeter.gui;
import sk.jaro.CiernyPeter.Hra;
import sk.jaro.CiernyPeter.Hrac;
import sk.jaro.CiernyPeter.rozhrania.IOvladanieHry;
import java.util.Scanner;
public class OvladanieHry implements IOvladanieHry {
Scanner scanner = new Scanner(System.in);
@Override
public int vyberPocetHracov() {
int pocetHracov = 0;
System.out.println("Zadaj počet hráčov (3 až 6):");
try {
pocetHracov = scanner.nextInt();
scanner.nextLine();
} catch (Exception ex) {
System.out.println("Nepodarilo sa načítať počet hráčov. Zadal si správne číslo?");
pocetHracov = vyberPocetHracov();
}
return pocetHracov;
}
@Override
public Hrac getMenoHraca(int i) {
Hrac hrac = null;
System.out.println(String.format("Zadaj meno pre hráča %d :", i));
String meno = scanner.next();
scanner.nextLine();
if (meno.equals("") || meno.equals(" ")) {
System.out.println(String.format("Prosím znovu zadajte meno pre hráča %d :", i));
hrac = getMenoHraca(i);
} else {
hrac = new Hrac(meno);
}
return hrac;
}
@Override
public int zoberKartu(Hrac hrac1, Hrac hrac2) {
int zoberKartuCislo = 0;
System.out.print(hrac1.getMeno() + " ,ktorú kartu cheš zobrať hračovi "+hrac2.getMeno()+"?: ");
for(int i = 0; i < hrac2.getKartyVRuke().size(); i++){
System.out.print(i+", ");
}
try {
zoberKartuCislo = scanner.nextInt();
scanner.nextLine();
} catch (Exception ex) {
System.out.println("Nepodarilo sa získať akú kartu chceš zobrať. Zadal si správne číslo?");
zoberKartuCislo = zoberKartu(hrac1,hrac2);
}
return zoberKartuCislo;
}
@Override
public void vypisKtoPrehral(Hra hra) {
System.out.println("Čierny Peter je hráč "+hra.getHraci().get(0).getMeno());
scanner.close();
}
}
Hra
Každá hra má několik hráčů, má balíček karet se kterými si hraje a má také ovládání. Toto si definujeme.
public class Hra{
private BalikKariet balikKariet;
private int pocetHracov;
private List<Hrac> hraci;
private OvladanieHry ovladanieHry;
V konstruktoru této Hry si nastavíme to, co víme:
public Hra() {
this.ovladanieHry = new OvladanieHry();
this.pocetHracov = ovladanieHry.vyberPocetHracov();
this.hraci = vytvorHracov();
}
Nestavili jsme balíček karet, protože ten je specifický pro každý typ hry jiný. V našem případě jsou to karty pro hru Černý Peter. Tak ty si vytvořím později.
V kuse kódu výše jsme si vytvořili instanci ovládání hry a hned jsme ji také použili při výběru počtu hráčů. Metoda výběr hráčů je jednoduchá, uživatele aplikace se ptám jak se jmenují a rovnou je vytvořím a dám do seznamu.
public List<Hrac> vytvorHracov() {
ArrayList<Hrac> hraci = new ArrayList<>();
for(int i = 0; i < pocetHracov; i++){
Hrac hrac = ovladanieHry.getMenoHraca(i+1);
hraci.add(hrac);
}
return hraci;
}
Logiku hry spustím a tedy začnu ji hrát když zavolám metodu zacniHrat.
public void zacniHru() {
HraCiernyPeter ciernyPeter = new HraCiernyPeter();
//vseobecna logika ku kazdej hre
balikKariet = vytvorBalik(ciernyPeter.vytvorKarty());
balikKariet.zamiesajKarty();
//rozdaj karty z baliku
ciernyPeter.rozdajKarty(this);
// pre hru urcim prveho hraca
// v ciernom petrovi je to hrac s najviac kartami a ten zacina tahat
Hrac prvyHrac = ciernyPeter.getHracaSNajviacKartami(getHraci());
//vsobecne na zaklade prveho hraca zistim jeho poradie v zozname hracov v hre
int prvyHracIndex = getHraci().indexOf(prvyHrac);
ciernyPeter.zlozHracomParyZRuky(this);
ciernyPeter.odstranHracovZHry(this);
if(!ciernyPeter.jeKoniecHry(this)){
//idu do kruhu az kym hraju aspon dvaja hraci
ciernyPeter.kolobehHry(this,prvyHracIndex);
}
}
Zde si vytvořím instanci třídy HraCiernyPeter, která má na starosti logiku, která je specifická právě pro tento typ hry. Tu si vytvoříme později.
Na tomto místě si vytvořím také balík karet pomocí karet, které se vytvářejí ve třídě HraCiernyPeter. Jelikož jsem zvolil názvy metod takové, aby se snadno chápaly, tak tušíme co dané metody dělají. Když vytvořím balíček a jdu hrát, tak karty pomíchám, pak je rozdám hráčům.
Musím si určit, který hráč začíná jako první. V černém petrovi je to ten, co má nejvíc karet.
Jak jsme si řekli na začátku, tak když mají hráči rozdané karty, tak si složí všechny páry a tím se zbaví nějakých karet. Zkontroluji jestli snad někdo neměl všechno páry na ruce a tím pádem skončil ve hře. Zeptám se, jestli je konec hry - zda zůstal jen jeden hráč, který má černého petra - neboť tato karta nemá pár. Pokud ne, tak začnu koloběh hry.
V této třídě mám i jiné pomocné třídy. Zkus si je projít sám.
package sk.jaro.CiernyPeter;
import sk.jaro.CiernyPeter.gui.OvladanieHry;
import java.util.ArrayList;
import java.util.List;
public class Hra{
private BalikKariet balikKariet;
private int pocetHracov;
private List<Hrac> hraci;
private OvladanieHry ovladanieHry;
public Hra() {
this.ovladanieHry = new OvladanieHry();
this.pocetHracov = ovladanieHry.vyberPocetHracov();
this.hraci = vytvorHracov();
}
public BalikKariet getBalikKariet() {
return balikKariet;
}
public List<Hrac> getHraci() {
return hraci;
}
public OvladanieHry getOvladanieHry() {
return ovladanieHry;
}
public List<Hrac> vytvorHracov() {
ArrayList<Hrac> hraci = new ArrayList<>();
for(int i = 0; i < pocetHracov; i++){
Hrac hrac = ovladanieHry.getMenoHraca(i+1);
hraci.add(hrac);
}
return hraci;
}
public BalikKariet vytvorBalik(List<Karta> karty) {
return new BalikKariet(karty);
}
public void odstranHracaZHry(Hrac hrac) {
//ak ma prazdnu ruku odstranim ho
if(hrac.getKartyVRuke().isEmpty()){
getHraci().remove(hrac);
}
}
public void ukonciHru() {
ovladanieHry.vypisKtoPrehral(this);
}
public void zacniHru() {
HraCiernyPeter ciernyPeter = new HraCiernyPeter();
//vseobecna logika ku kazdej hre
balikKariet = vytvorBalik(ciernyPeter.vytvorKarty());
balikKariet.zamiesajKarty();
//rozdaj karty z baliku
ciernyPeter.rozdajKarty(this);
// pre hru urcim prveho hraca
// v ciernom petrovi je to hrac s najviac kartami a ten zacina tahat
Hrac prvyHrac = ciernyPeter.getHracaSNajviacKartami(getHraci());
//vsobecne na zaklade prveho hraca zistim jeho poradie v zozname hracov v hre
int prvyHracIndex = getHraci().indexOf(prvyHrac);
ciernyPeter.zlozHracomParyZRuky(this);
ciernyPeter.odstranHracovZHry(this);
if(!ciernyPeter.jeKoniecHry(this)){
//idu do kruhu az kym hraju aspon dvaja hraci
ciernyPeter.kolobehHry(this,prvyHracIndex);
}
}
}
Logika hry Černý Peter
V této části si vytvoříme karty specifické pro tuto hru. Tedy 16 párů a jednoho černého petra.
public List<Karta> vytvorKarty() {
ArrayList<Karta> karty = new ArrayList<>();
int j = 1;
for(int i = 0; i < 16; i++, j=j+2){
karty.add(new Karta(j, i));
karty.add(new Karta(j+1, i));
}
karty.add(new Karta(33,-1)); //Čierny Peter
return karty;
}
Když rozdávám karty, tak je rozdávám po jedné. Tato metoda by mohla být také ve třídě Hra, ale teoreticky pro jiné typy her by se karty rozdávaly jinak. Tady rozdávám všechny karty.
Z balíku karet vezmu první kartu, z balíku ji odstraním a dám ji hráči do ruky. Tady je takový fígl, že jdu přes všechny karty a dělám modulo nad pořadím karty s počtem hráčů, to mi zaručí, že budu dokola procházet hráče dokud neskončí balík.
public void rozdajKarty(Hra hra) {
BalikKariet balikKariet = hra.getBalikKariet();
List<Hrac> hraci = hra.getHraci();
int pocetKariet = balikKariet.getKarty().size();
for(int i = 0; i<pocetKariet;i++){
Hrac hrac = hraci.get(i%hraci.size());
hrac.getKartyVRuke().add(balikKariet.getKartu());
}
}
Když se chystám odstranit hráče ze hry (když nemají už žádné karty na ruce), tak je nemohu odstranit během toho, jak přes ně procházím (iteruji). Proto si je dávám do pomocného seznamu a až po iteraci je odstraním.
public void odstranHracovZHry(Hra hra) {
//nemôžem mazať hraca z kolekcie ak cez nu prechadzam, preto si vytvorim novy zoznam a odstranim potom
ArrayList<Hrac> hraciNaOdstranenie = new ArrayList<>();
for(Hrac hrac : hra.getHraci()){
//skontrolujem ci uz niekto neskoncil, teda ma prazdnu ruku
//ak ano odstranim ho z hry
if(hrac.getKartyVRuke().isEmpty()){
hraciNaOdstranenie.add(hrac);
}
}
for(Hrac hrac : hraciNaOdstranenie){
hra.odstranHracaZHry(hrac);
}
}
Když někomu vezmu kartu z ruky, tak každému z těch hráčů pomíchám karty. Jednomu hráči vezmu kartu z kolekce kartiček, což má na ruce a druhému přidám do kolekce další kartu.
public void zoberHracoviKartu(Hrac hrac1, Hrac hrac2, Hra hra) {
int poradieZobranejKarty = hra.getOvladanieHry().zoberKartu(hrac1,hrac2);
Karta vzataKarta = hrac2.getKartyVRuke().get(poradieZobranejKarty);
hrac1.getKartyVRuke().add(vzataKarta);
hrac2.getKartyVRuke().remove(vzataKarta);
//pomiesam karty v ruke
Collections.shuffle(hrac1.getKartyVRuke());
Collections.shuffle(hrac2.getKartyVRuke());
}
Samozřejmě koloběh hry jede následovně. Hrajeme do té doby, než nám ve hře zůstanou alespoň dva hráči. Začínám u prvního hráče, který vezme kartu druhému hráči. A tady jsem si natrefil na chybu. Přece hráč s největším počtem karet netáhne ale mělo by se táhnout jemu tedy, ten co je za ním táhne od něj. Tak tady si to můžete opravit, to nechám na vás. Pomůcka: upravte index prvního hráče ve třídě Hra, pokud si pamatujete, tam jsme ho určili.
public void kolobehHry(Hra hra, int prvyHracIndex) {
while (hra.getHraci().size() > 1) {
int pocetHracov = hra.getHraci().size();
Hrac hrac1 = hra.getHraci().get(prvyHracIndex%pocetHracov);
Hrac hrac2 = hra.getHraci().get((prvyHracIndex + 1)%pocetHracov);
zoberHracoviKartu(hrac1, hrac2,hra);
zlozHracomParyZRuky(hra);
odstranHracovZHry(hra);
if(jeKoniecHry(hra)) {
break;
}
prvyHracIndex++;
}
}
Tady je pak celý kód třídy is jinými pomocnými metodami.
package sk.jaro.CiernyPeter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class HraCiernyPeter {
public List<Karta> vytvorKarty() {
ArrayList<Karta> karty = new ArrayList<>();
int j = 1;
for(int i = 0; i < 16; i++, j=j+2){
karty.add(new Karta(j, i));
karty.add(new Karta(j+1, i));
}
karty.add(new Karta(33,-1)); //Čierny Peter
return karty;
}
public void rozdajKarty(Hra hra) {
BalikKariet balikKariet = hra.getBalikKariet();
List<Hrac> hraci = hra.getHraci();
int pocetKariet = balikKariet.getKarty().size();
for(int i = 0; i<pocetKariet;i++){
Hrac hrac = hraci.get(i%hraci.size());
hrac.getKartyVRuke().add(balikKariet.getKartu());
}
}
public Hrac getHracaSNajviacKartami(List<Hrac> hraci) {
int max = 0;
Hrac hracMax = null;
for(Hrac hrac : hraci){
int size = hrac.getKartyVRuke().size();
if(max < size){
max = size;
hracMax = hrac;
}
}
return hracMax;
}
public void zlozHracomParyZRuky(Hra hra) {
for(Hrac hrac : hra.getHraci()) {
hrac.odstranParyZRuky();
}
}
public void odstranHracovZHry(Hra hra) {
//nemôžem mazať hraca z kolekcie ak cez nu prechadzam, preto si vytvorim novy zoznam a odstranim potom
ArrayList<Hrac> hraciNaOdstranenie = new ArrayList<>();
for(Hrac hrac : hra.getHraci()){
//skontrolujem ci uz niekto neskoncil, teda ma prazdnu ruku
//ak ano odstranim ho z hry
if(hrac.getKartyVRuke().isEmpty()){
hraciNaOdstranenie.add(hrac);
}
}
for(Hrac hrac : hraciNaOdstranenie){
hra.odstranHracaZHry(hrac);
}
}
public boolean jeKoniecHry(Hra hra) {
if(hra.getHraci().size() < 2){
hra.ukonciHru();
return true;
}
return false;
}
public void zoberHracoviKartu(Hrac hrac1, Hrac hrac2, Hra hra) {
int poradieZobranejKarty = hra.getOvladanieHry().zoberKartu(hrac1,hrac2);
Karta vzataKarta = hrac2.getKartyVRuke().get(poradieZobranejKarty);
hrac1.getKartyVRuke().add(vzataKarta);
hrac2.getKartyVRuke().remove(vzataKarta);
//pomiesam karty v ruke
Collections.shuffle(hrac1.getKartyVRuke());
Collections.shuffle(hrac2.getKartyVRuke());
}
public void kolobehHry(Hra hra, int prvyHracIndex) {
while (hra.getHraci().size() > 1) {
int pocetHracov = hra.getHraci().size();
Hrac hrac1 = hra.getHraci().get(prvyHracIndex%pocetHracov);
Hrac hrac2 = hra.getHraci().get((prvyHracIndex + 1)%pocetHracov);
zoberHracoviKartu(hrac1, hrac2,hra);
zlozHracomParyZRuky(hra);
odstranHracovZHry(hra);
if(jeKoniecHry(hra)) {
break;
}
prvyHracIndex++;
}
}
}
Main
Nakonec jsem si vytvořil třídu s jednou main metodou, která se nám bude jmenovat při spuštění programu.
public static void main(String[] args) {
Hra hra = new Hra();
hra.zacniHru();
}
Dodělejte výpis, jaké karty byly hráči odstraněny z ruky, když složil páry. Udělejte další podmínky při zadávání údajů od uživatele, aby nebral karty, které někdo nemá v ruce a podobně.
Autorem tohoto blogu je Jaro Beňo , autor Java online kurzu , který můžeš na Learn2Code studovat zdarma.
🥇 Sme jednotka v online vzdelávaní na Slovensku. Na našom webe nájdeš viac ako 300 rôznych videokurzov z oblastí ako programovanie, tvorba hier, testovanie softwaru, grafika, UX dizajn, online marketing, MS Office a pod. Vyber si kurz, ktorý ťa posunie vpred ⏩