Ohjelmointi

Kortti moottori Java

Kaikki alkoi, kun huomasimme, että Java-kielellä kirjoitettuja korttipelisovelluksia tai sovelmia oli hyvin vähän. Aluksi ajattelimme kirjoittaa pari peliä, ja aloitimme selvittämällä korttipelien luomiseen tarvittavat ydinkoodit ja luokat. Prosessi jatkuu, mutta nyt on olemassa melko vakaa kehys, jota voidaan käyttää erilaisten korttipeliratkaisujen luomiseen. Tässä kuvataan, miten tämä kehys suunniteltiin, miten se toimii, ja työkaluja ja temppuja, joita käytettiin tekemään siitä hyödyllinen ja vakaa.

Suunnitteluvaihe

Kohdekeskeisessä suunnittelussa on äärimmäisen tärkeää tuntea ongelma sisältä ja ulkoa. Muuten on mahdollista käyttää paljon aikaa luokkien ja ratkaisujen suunnitteluun, joita ei tarvita tai jotka eivät toimi erityistarpeiden mukaan. Korttipeleissä yksi tapa on visualisoida, mitä tapahtuu, kun yksi, kaksi tai useampi henkilö pelaa kortteja.

Korttipakassa on yleensä 52 korttia neljässä eri puvussa (timantit, sydämet, mailat, lapiot), joiden arvot vaihtelevat deuce-kuninkaasta plus ässä. Välittömästi syntyy ongelma: Pelisäännöistä riippuen ässät voivat olla joko pienin, suurin tai molemmat.

Lisäksi on pelaajia, jotka vievät kortit kannelta käteen ja hallitsevat kättä sääntöjen mukaisesti. Voit joko näyttää kortit kaikille asettamalla ne pöydälle tai katsella niitä yksityisesti. Pelin tietystä vaiheesta riippuen kädessäsi voi olla N määrä kortteja.

Vaiheiden analysointi tällä tavalla paljastaa erilaisia ​​malleja. Käytämme nyt tapauskohtaista lähestymistapaa, kuten edellä on kuvattu, joka on dokumentoitu Ivar Jacobsonin julkaisussa Kohdeohjattu ohjelmistotuotanto. Tässä kirjassa yksi perusideoista on mallintaminen luokkiin tosielämän tilanteiden perusteella. Tämä tekee paljon helpommaksi ymmärtää, miten suhteet toimivat, mikä riippuu mistä ja miten abstraktit toimivat.

Meillä on luokkia, kuten CardDeck, Hand, Card ja RuleSet. CardDeck sisältää aluksi 52 korttiobjektia, ja CardDeckillä on vähemmän korttiobjekteja, koska ne vedetään käsiobjektiin. Käsiobjektit puhuvat RuleSet-objektin kanssa, jolla on kaikki pelisäännöt. Ajattele RuleSet-peliä pelikirjana.

Vektorikurssit

Tässä tapauksessa tarvitsimme joustavan tietorakenteen, joka käsittelee dynaamiset syötemuutokset, mikä eliminoi Array-tietorakenteen. Halusimme myös helpon tavan lisätä inserttielementti ja välttää paljon koodaamista, jos mahdollista. Saatavana on erilaisia ​​ratkaisuja, kuten binääripuiden erilaisia ​​muotoja. Java.util-paketissa on kuitenkin Vector-luokka, joka toteuttaa joukon esineitä, jotka kasvavat ja kutistuvat kooltaan tarvittaessa, mikä oli juuri sitä mitä tarvitsimme. (Vector-jäsenfunktioita ei ole selitetty täysin nykyisessä dokumentaatiossa; tässä artikkelissa selitetään tarkemmin, kuinka Vector-luokkaa voidaan käyttää samanlaisissa dynaamisissa objektiluettelotilanteissa.) Vector-luokkien haittana on lisämuisti, koska paljon muistia kopiointi tapahtuu kulissien takana. (Tästä syystä taulukot ovat aina parempia; ne ovat kooltaan staattisia, joten kääntäjä voisi selvittää tapoja optimoida koodi). Suurempien objektijoukkojen kohdalla meillä saattaa olla rangaistuksia hakuajoista, mutta suurin vektori, jonka voimme ajatella, oli 52 merkintää. Se on edelleen kohtuullista tässä tapauksessa, ja pitkät hakuajat eivät olleet huolenaiheita.

Seuraavassa on lyhyt selitys kunkin luokan suunnittelusta ja toteutuksesta.

Korttiluokka

Card-luokka on hyvin yksinkertainen: se sisältää arvoja, jotka ilmoittavat värin ja arvon. Siinä voi olla myös viitteitä GIF-kuviin ja vastaaviin yksiköihin, jotka kuvaavat korttia, mukaan lukien mahdollinen yksinkertainen käyttäytyminen, kuten animaatio (käännä kortti) ja niin edelleen.

luokka Card toteuttaa CardConstants {public int color; julkinen int-arvo; julkinen merkkijono ImageName; } 

Nämä korttiobjektit tallennetaan sitten eri vektoriluokkiin. Huomaa, että korttien arvot, mukaan lukien väri, määritetään käyttöliittymässä, mikä tarkoittaa, että kukin kehysluokka voisi toteuttaa ja sisällyttää siten vakiot:

käyttöliittymä CardConstants {// käyttöliittymäkentät ovat aina julkisia staattisia lopullisia! int SYDÄMÄT 1; int DIAMOND 2; int SPADE 3; int KLUBIT 4; sis. JACK 11; sis. kuningatar 12; sis. kuningas 13; int ACE_LOW 1; int ACE_HIGH 14; } 

CardDeck-luokka

CardDeck-luokassa on sisäinen Vector-objekti, joka alustetaan 52 korttiobjektilla. Tämä tapahtuu sekoitus -menetelmällä. Tarkoituksena on, että aina kun sekoitat, aloitat pelin määrittämällä 52 korttia. On tarpeen poistaa kaikki mahdolliset vanhat objektit ja aloittaa oletusasetuksista uudelleen (52 korttiobjektia).

 public void shuffle () {// Nollaa aina kansivektori ja alusta se tyhjästä. deck.removeAllElements (); 20 // Aseta sitten 52 korttia. Yksi väri kerrallaan (int i ACE_LOW; i <ACE_HIGH; i ++) {Card aCard new Card (); aCard.color SYDÄT; aCard.value i; deck.addElement (aCard); } // Tee sama CLUBS, DIAMONDS ja SPADES. } 

Kun piirrämme korttiobjektin CardDeckistä, käytämme satunnaislukugeneraattoria, joka tuntee joukon, josta se valitsee satunnaisen sijainnin vektorin sisällä. Toisin sanoen, vaikka korttiobjektit olisikin järjestetty, satunnaisfunktio valitsee mielivaltaisen sijainnin vektorin sisällä olevien elementtien puitteissa.

Osana tätä prosessia poistamme myös varsinaisen objektin CardDeck-vektorista, kun välitämme tämän objektin Käsi-luokalle. Vector-luokka kartoittaa korttipakan ja käden tosielämän tilanteen ohittamalla kortin:

 julkinen kortinotto () {Card aCard null; int-asema (int) (Math.random () * (kannen koko = ())); kokeile {aCard (Card) deck.elementAt (position); } catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } deck.removeElementAt (sijainti); palauta aCard; } 

Huomaa, että on hyvä tarttua mahdollisiin poikkeuksiin, jotka liittyvät objektin ottamiseen vektorista sijainnista, jota ei ole läsnä.

On apuohjelma, joka toistaa vektorin kaikki elementit ja kutsuu toista menetelmää, joka pudottaa ASCII-arvon / väripari merkkijonon. Tämä ominaisuus on hyödyllinen, kun korjataan sekä Deck- että Hand-luokkia. Vektorien laskemisominaisuuksia käytetään paljon Käsi-luokassa:

 public void dump () {Enumeration enum deck.elements (); while (enum.hasMoreElements ()) {Korttikortti (Kortti) enum.nextElement (); RuleSet.printValue (kortti); }} 

Käsi-luokka

Käsi-luokka on todellinen työhevonen tässä yhteydessä. Suurin osa vaaditusta käyttäytymisestä oli jotain, joka oli hyvin luonnollista sijoittaa tähän luokkaan. Kuvittele, että ihmiset pitävät kortteja käsissään ja tekevät erilaisia ​​toimintoja katsellessaan Kortin esineitä.

Ensinnäkin tarvitset myös vektorin, koska monissa tapauksissa ei tiedetä, kuinka monta korttia noutetaan. Vaikka voisit toteuttaa taulukon, on hyvä olla joustavuutta myös tässä. Luonnollisin tapa, jota tarvitsemme, on kortin ottaminen:

 public void take (Card theCard) {cardHand.addElement (theCard); } 

CardHand on vektori, joten lisätään vain Kortti-objekti tähän vektoriin. Kädessä tapahtuvien "lähtö" -operaatioiden tapauksessa meillä on kuitenkin kaksi tapausta: yksi, jossa näytämme kortin, ja toinen, jossa molemmat näytämme ja vedämme kortin kädestä. Meidän on toteutettava molemmat, mutta perinnöllä kirjoitamme vähemmän koodia, koska kortin piirtäminen ja näyttäminen on erityinen tapaus vain kortin näyttämisestä:

 public Card show (int position) {Kortti aCard null; kokeile {aCard (Card) cardHand.elementAt (position); } catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } return aCard; } 20 julkista kortinvetoa (int-sijainti) {Card aCard -näyttö (sijainti); cardHand.removeElementAt (sijainti); palauta aCard; } 

Toisin sanoen piirtotapaus on näyttötapaus, jolla on ylimääräinen käyttäytyminen, kun objekti poistetaan Käsi-vektorista.

Kirjoittaessamme testikoodia eri luokille löysimme yhä enemmän tapauksia, joissa oli tarpeen selvittää kädessä olevat erityisarvot. Esimerkiksi joskus meidän piti tietää, kuinka monta tietyntyyppistä korttia oli kädessä. Tai yhden oletusarvon vähäinen ässä-arvo oli vaihdettava arvoon 14 (korkein arvo) ja takaisin. Kaikissa tapauksissa käyttäytymistuki siirrettiin takaisin Käsi-luokkaan, koska se oli hyvin luonnollinen paikka tällaiselle käyttäytymiselle. Jälleen oli melkein kuin ihmisen aivot olisivat käden takana tekemässä näitä laskelmia.

Vektorien laskentaominaisuutta voidaan käyttää selvittämään, kuinka monta tietyn arvon korttia oli läsnä Käsi-luokassa:

 julkiset int NC-kortit (int-arvo) {int n 0; Luettelon enum cardHand.elements (); while (enum.hasMoreElements ()) {tempCard (Card) enum.nextElement (); // = tempCard määritelty, jos (tempCard.value = arvo) n ++; } paluu n; } 

Vastaavasti voit iteroida korttiobjektien läpi ja laskea korttien kokonaissumman (kuten testissä 21) tai muuttaa kortin arvoa. Huomaa, että kaikki objektit ovat oletusarvoisesti Java-viitteitä. Jos haet mielestäsi väliaikaisen objektin ja muokkaat sitä, todellinen arvo muuttuu myös vektorin tallentaman objektin sisällä. Tämä on tärkeä asia pitää mielessä.

RuleSet-luokka

RuleSet-luokka on kuin sääntökirja, jonka tarkistat silloin tällöin peliä pelatessasi. se sisältää kaiken sääntöihin liittyvän käyttäytymisen. Huomaa, että mahdolliset strategiat, joita pelinpelaaja voi käyttää, perustuvat joko käyttöliittymän palautteeseen tai yksinkertaiseen tai monimutkaisempaan tekoälykoodiin. RuleSet huolehtii vain siitä, että sääntöjä noudatetaan.

Myös muut kortteihin liittyvät käyttäytymiset asetettiin tähän luokkaan. Esimerkiksi loimme staattisen toiminnon, joka tulostaa kortin arvotiedot. Myöhemmin tämä voidaan sijoittaa myös Korttiluokkaan staattisena toimintona. Nykyisessä muodossa RuleSet-luokassa on vain yksi perussääntö. Se vie kaksi korttia ja lähettää takaisin tietoja siitä, mikä kortti oli korkein:

 julkinen int korkeampi (Kortti yksi, Kortti kaksi) {int kumpi 0; jos (yksi.arvo = ACE_LOW) yksi. arvo ACE_HIGH; jos (kaksi.arvo = ACE_LOW) kaksi. arvo ACE_HIGH; // Tässä sääntöjoukossa korkein arvo voittaa, emme ota huomioon // väriä. jos (yksi.arvo> kaksi.arvo) kumpi 1; jos (yksi.arvo <kaksi.arvo) kumpi 2; jos (yksi.arvo = kaksi.arvo) kumpi 0; // Normalisoi ACE-arvot, joten syötetyllä on samat arvot. jos (yksi.arvo = ACE_HIGH) yksi. arvo ACE_LOW; jos (kaksi.arvo = ACE_HIGH) kaksi. arvo ACE_LOW; palauta mikä; } 

Testin suorittamisen aikana sinun on muutettava ässä-arvot, joiden luonnollinen arvo on 1–14. On tärkeää muuttaa arvot takaisin jälkikäteen mahdollisten ongelmien välttämiseksi, koska oletamme tässä yhteydessä, että ässät ovat aina yksi.

21: n tapauksessa alaluokitimme RuleSetin luomaan TwentyOneRuleSet-luokan, joka osaa selvittää, onko käsi alle 21, täsmälleen 21 tai yli 21. Se ottaa huomioon myös ässäarvot, jotka voivat olla joko yksi tai 14, ja yrittää selvittää parhaan mahdollisen arvon. (Lisää esimerkkejä on lähdekoodissa.) Strategioiden määritteleminen on kuitenkin pelaajan tehtävä; tässä tapauksessa kirjoitimme yksinkertaisesta tekoälyjärjestelmästä, jossa jos kätesi on alle 21 kahden kortin jälkeen, otat yhden kortin lisää ja pysähdyt.

Kuinka käyttää luokkia

Tämän kehyksen käyttö on melko suoraviivaista:

 myCardDeck uusi CardDeck (); myRules uusi RuleSet (); käsiUusi käsi (); handB uusi käsi (); DebugClass.DebugStr ("Piirrä viisi korttia kumpikin kädelle A ja käsi B"); for (int i 0; i <NCARDS; i ++) {handA.take (myCardDeck.draw ()); handB.take (myCardDeck.draw ()); } // Testaa ohjelmat, poista se käytöstä kommentoimalla tai käyttämällä DEBUG-lippuja. testHandValues ​​(); testCardDeckOperations (); testCardValues ​​(); testHighestCardValues ​​(); testi21 (); 

Eri testiohjelmat eristetään erillisiksi staattisiksi tai ei-staattisiksi jäsenfunktioksi. Luo niin monta kättä kuin haluat, ota kortit ja anna roskakorin päästä eroon käyttämättömistä käsistä ja korteista.

Soitat RuleSetiin antamalla käden tai kortin objektin, ja palautetun arvon perusteella tiedät lopputuloksen:

 DebugClass.DebugStr ("Vertaa kädessä A ja B olevaa toista korttia"); int voittaja myRules.higher (handA.show (1), = handB.show (1)); if (voittaja = 1) o.println ("Kädellä A oli korkein kortti."); else if (voittaja = 2) o.println ("Kädellä B oli korkein kortti."); else o.println ("Se oli tasapeli."); 

Tai, jos kyseessä on 21:

 int tulos myTwentyOneGame.isTwentyOne (käsiC); if (tulos = 21) o.println ("Saimme kaksikymmentäyksi!"); else if (tulos> 21) o.println ("Menetimme" + tulos); else {o.println ("Otamme toisen kortin"); // ...} 

Testaus ja virheenkorjaus

On erittäin tärkeää kirjoittaa testikoodi ja esimerkkejä toteutettaessa varsinaista kehystä. Näin tiedät koko ajan kuinka hyvin toteutuskoodi toimii; huomaat tosiasiat ominaisuuksista ja yksityiskohdista toteutuksesta. Lisäajan myötä olisimme toteuttaneet pokerin - tällainen testitapaus olisi tarjonnut entistä enemmän tietoa ongelmasta ja osoittanut, kuinka kehys on määriteltävä uudelleen.