Programmering

Kortmotor i Java

Dette startet da vi la merke til at det var veldig få kortspillapplikasjoner eller applets skrevet i Java. Først tenkte vi på å skrive et par spill, og startet med å finne ut kjernekoden og klassene som trengs for å lage kortspill. Prosessen fortsetter, men nå er det et ganske stabilt rammeverk å bruke for å lage forskjellige kortspillløsninger. Her beskriver vi hvordan dette rammeverket ble designet, hvordan det fungerer, og verktøyene og triksene som ble brukt for å gjøre det nyttig og stabilt.

Designfase

Med objektorientert design er det ekstremt viktig å kjenne problemet innvendig og utvendig. Ellers er det mulig å bruke mye tid på å utforme klasser og løsninger som ikke er nødvendige eller som ikke fungerer i henhold til spesifikke behov. Når det gjelder kortspill, er en tilnærming å visualisere hva som skjer når en, to eller flere personer spiller kort.

En kortstokk inneholder vanligvis 52 kort i fire forskjellige drakter (diamanter, hjerter, klubber, spar), med verdier som spenner fra deuce til kongen, pluss esset. Det oppstår straks et problem: avhengig av spillereglene kan essene være enten den laveste kortverdien, den høyeste eller begge deler.

Videre er det spillere som tar kort fra kortstokken til en hånd og håndterer hånden basert på regler. Du kan enten vise kortene til alle ved å plassere dem på bordet eller se på dem privat. Avhengig av hvilken fase av spillet du har, kan du ha N antall kort i hånden.

Å analysere trinnene på denne måten avslører ulike mønstre. Vi bruker nå en saksdrevet tilnærming, som beskrevet ovenfor, som er dokumentert i Ivar Jacobsons Objektorientert programvareteknikk. I denne boken er en av de grunnleggende ideene å modellere klasser basert på virkelige situasjoner. Det gjør det mye lettere å forstå hvordan relasjoner fungerer, hva avhenger av hva og hvordan abstraksjonene fungerer.

Vi har klasser som CardDeck, Hand, Card og RuleSet. Et CardDeck vil inneholde 52 kortobjekter i starten, og CardDeck vil ha færre kortobjekter ettersom disse trekkes inn i et håndobjekt. Håndobjekter snakker med et RuleSet-objekt som har alle reglene som gjelder spillet. Tenk på et RuleSet som spillhåndbok.

Vektorklasser

I dette tilfellet trengte vi en fleksibel datastruktur som håndterer endringer i dynamiske oppføringer, noe som eliminerte Array-datastrukturen. Vi ønsket også en enkel måte å legge til et innsatselement og unngå mye koding hvis mulig. Det finnes forskjellige løsninger, for eksempel forskjellige former for binære trær. Imidlertid har java.util-pakken en Vector-klasse som implementerer en rekke objekter som vokser og krymper i størrelse etter behov, noe som var akkurat det vi trengte. (Vector-medlemsfunksjonene er ikke fullstendig forklart i den gjeldende dokumentasjonen. Denne artikkelen vil forklare nærmere hvordan Vector-klassen kan brukes til lignende forekomster av dynamiske objektlister.) Ulempen med Vector-klasser er ekstra minnebruk på grunn av mye minne kopiering gjort bak kulissene. (Av denne grunn er Arrays alltid bedre; de ​​er statiske i størrelse, så kompilatoren kan finne ut måter å optimalisere koden på). Også, med større sett med gjenstander, kan vi ha straffer for oppslagstider, men den største Vector vi kunne tenke oss var 52 oppføringer. Det er fortsatt rimelig for denne saken, og lange oppslagstider var ikke noe bekymring.

En kort forklaring på hvordan hver klasse ble designet og implementert følger.

Kortklasse

Kortklassen er veldig enkel: den inneholder verdier som signaliserer fargen og verdien. Det kan også ha pekere til GIF-bilder og lignende enheter som beskriver kortet, inkludert mulig enkel oppførsel som animasjon (snu et kort) og så videre.

klasse Kort implementerer CardConstants {public int color; offentlig int verdi; offentlig String ImageName; } 

Disse kortobjektene lagres deretter i forskjellige vektorklasser. Merk at verdiene for kortene, inkludert farge, er definert i et grensesnitt, noe som betyr at hver klasse i rammeverket kan implementeres og på denne måten inkluderer konstanter:

grensesnitt CardConstants {// grensesnittfelt er alltid offentlig statisk endelig! int HJERTER 1; int DIAMANT 2; int SPADE 3; int CLUBS 4; int JACK 11; int DRONNING 12; int KONING 13; int ACE_LOW 1; int ACE_HIGH 14; } 

CardDeck-klasse

CardDeck-klassen vil ha et internt Vector-objekt, som vil initialiseres med 52 kortobjekter. Dette gjøres ved hjelp av en metode som heter shuffle. Implikasjonen er at hver gang du blander, starter du virkelig et spill ved å definere 52 kort. Det er nødvendig å fjerne alle mulige gamle objekter og starte fra standardtilstanden igjen (52 kortobjekter).

 public void shuffle () {// Nullstill alltid dekkvektoren og initialiser den fra bunnen av. deck.removeAllElements (); 20 // Sett deretter inn de 52 kortene. Én farge om gangen for (int i ACE_LOW; i <ACE_HIGH; i ++) {Card aCard new Card (); aCard.color HJERTER; aCard.value i; deck.addElement (aCard); } // Gjør det samme for CLUBS, DIAMONDS og SPADES. } 

Når vi tegner et kortobjekt fra CardDeck, bruker vi en tilfeldig tallgenerator som kjenner settet som den vil velge en tilfeldig posisjon inne i vektoren. Med andre ord, selv om kortobjektene er bestilt, vil den tilfeldige funksjonen velge en vilkårlig posisjon innenfor omfanget av elementene i Vector.

Som en del av denne prosessen fjerner vi også det faktiske objektet fra CardDeck-vektoren når vi sender dette objektet til Hand-klassen. Vector-klassen kartlegger den virkelige situasjonen til en kortstokk og en hånd ved å sende et kort:

 public Card draw () {Card aCard null; int posisjon (int) (Math.random () * (deck.size = ())); prøv {aCard (Card) deck.elementAt (position); } fange (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } deck.removeElementAt (posisjon); returner et kort; } 

Merk at det er bra å fange eventuelle unntak knyttet til å ta et objekt fra Vector fra en posisjon som ikke er til stede.

Det er en verktøymetode som går gjennom alle elementene i vektoren og kaller en annen metode som vil dumpe en ASCII-verdi / fargeparstreng. Denne funksjonen er nyttig når du feilsøker både Deck og Hand-klassene. Oppregningsfunksjonene til vektorer brukes mye i håndklassen:

 public void dump () {Enumeration enum deck.elements (); while (enum.hasMoreElements ()) {Card card (Card) enum.nextElement (); RuleSet.printValue (kort); }} 

Håndklasse

Hand-klassen er en reell arbeidshest i dette rammeverket. Det meste av oppførselen som kreves var noe som var veldig naturlig å plassere i denne klassen. Tenk deg folk som holder kort i hendene og gjør forskjellige operasjoner mens de ser på kortobjektene.

Først trenger du også en vektor, siden det i mange tilfeller er ukjent hvor mange kort som blir hentet. Selv om du kan implementere en matrise, er det også bra å ha litt fleksibilitet. Den mest naturlige metoden vi trenger er å ta et kort:

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

CardHand er en vektor, så vi legger bare til kortobjektet i denne vektoren. Men når det gjelder "output" -operasjonene fra hånden, har vi to tilfeller: en der vi viser kortet, og en der vi begge viser og trekker kortet fra hånden. Vi må implementere begge deler, men ved hjelp av arv skriver vi mindre kode fordi tegning og visning av kort er et spesielt tilfelle fra å bare vise et kort:

 public Card show (int position) {Card aCard null; prøv {aCard (Card) cardHand.elementAt (position); } fange (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } returner aCard; } 20 offentlig korttegning (int posisjon) {Card aCard show (posisjon); cardHand.removeElementAt (posisjon); returner et kort; } 

Med andre ord er tegnesaken et showcase, med den ekstra oppførselen ved å fjerne objektet fra håndvektoren.

Ved å skrive testkode for de forskjellige klassene fant vi et økende antall tilfeller der det var nødvendig å finne ut om ulike spesialverdier i hånden. Noen ganger trengte vi for eksempel å vite hvor mange kort av en bestemt type som var i hånden. Eller standard ess lav verdi på en måtte endres til 14 (høyeste verdi) og tilbake igjen. I alle tilfeller ble atferdsstøtten delegert tilbake til Hand-klassen, da det var et veldig naturlig sted for slik oppførsel. Igjen var det nesten som om en menneskelig hjerne sto bak hånden og gjorde disse beregningene.

Oppregningsfunksjonen til vektorer kan brukes til å finne ut hvor mange kort av en spesifikk verdi som var til stede i håndklassen:

 offentlige int NCards (int-verdi) {int n 0; Oppregning enum cardHand.elements (); while (enum.hasMoreElements ()) {tempCard (Card) enum.nextElement (); // = tempCard definert hvis (tempCard.value = verdi) n ++; } returnere n; } 

På samme måte kan du gjenta gjennom kortobjektene og beregne den totale summen av kort (som i 21-testen), eller endre verdien på et kort. Merk at som standard er alle objekter referanser i Java. Hvis du henter ut det du mener er et midlertidig objekt og endrer det, endres den faktiske verdien også inne i objektet som er lagret av vektoren. Dette er en viktig sak å huske på.

RuleSet-klasse

RuleSet-klassen er som en regelbok som du sjekker innimellom når du spiller et spill; den inneholder all atferd som gjelder reglene. Merk at de mulige strategiene en spillespiller kan bruke er basert på tilbakemeldinger fra brukergrensesnittet eller på enkel eller mer kompleks kunstig intelligens (AI) -kode. Alt RuleSet bekymrer seg for er at reglene følges.

Annen oppførsel relatert til kort ble også plassert i denne klassen. For eksempel opprettet vi en statisk funksjon som skriver ut informasjon om kortverdien. Senere kan dette også plasseres i kortklassen som en statisk funksjon. I den nåværende formen har RuleSet-klassen bare en grunnleggende regel. Det tar to kort og sender informasjon om hvilket kort som var det høyeste:

 public int higher (Card one, Card two) {int whichone 0; hvis (one.value = ACE_LOW) one.value ACE_HIGH; hvis (two.value = ACE_LOW) two.value ACE_HIGH; // I denne regelen angir du den høyeste verdien vinner, tar vi ikke hensyn til // fargen. hvis (en.verdi> to.verdi) hvilken 1; hvis (en.verdi <to.verdi) hvilken 2; hvis (one.value = two.value) hvilken 0; // Normaliser ACE-verdiene, så det som ble sendt inn har de samme verdiene. hvis (one.value = ACE_HIGH) one.value ACE_LOW; hvis (two.value = ACE_HIGH) two.value ACE_LOW; returner hvilken; } 

Du må endre essverdiene som har den naturlige verdien fra en til 14 mens du gjør testen. Det er viktig å endre verdiene tilbake til en etterpå for å unngå mulige problemer, da vi i dette rammeverket antar at ess alltid er ett.

I tilfelle 21 subklasserte vi RuleSet for å lage en TwentyOneRuleSet-klasse som vet hvordan man skal finne ut om hånden er under 21, nøyaktig 21 eller over 21. Det tar også hensyn til essverdiene som kan være enten en eller 14, og prøver å finne ut den best mulige verdien. (For flere eksempler, se kildekoden.) Det er imidlertid opp til spilleren å definere strategiene; i dette tilfellet skrev vi et enkeltsinnet AI-system der hvis hånden din er under 21 etter to kort, tar du ett kort til og stopper.

Hvordan bruke klassene

Det er ganske greit å bruke dette rammeverket:

 myCardDeck nye CardDeck (); myRules nye RuleSet (); håndEn ny hånd (); håndB ny hånd (); DebugClass.DebugStr ("Tegn fem kort hver til hånd A og hånd B"); for (int i 0; i <NCARDS; i ++) {handA.take (myCardDeck.draw ()); handB.take (myCardDeck.draw ()); } // Test programmer, deaktiver ved å kommentere eller bruke DEBUG-flagg. testHandValues ​​(); testCardDeckOperations (); testCardValues ​​(); testHighestCardValues ​​(); test21 (); 

De forskjellige testprogrammene er isolert i separate statiske eller ikke-statiske medlemsfunksjoner. Lag så mange hender du vil, ta kort og la søppeloppsamlingen bli kvitt ubrukte hender og kort.

Du ringer til RuleSet ved å levere hånd- eller kortobjektet, og basert på den returnerte verdien, vet du utfallet:

 DebugClass.DebugStr ("Sammenlign det andre kortet i hånd A og hånd B"); int vinner myRules.higher (handA.show (1), = handB.show (1)); hvis (vinner = 1) o.println ("Hånd A hadde det høyeste kortet."); annet hvis (vinner = 2) o.println ("Hånd B hadde det høyeste kortet."); annet o.println ("Det var uavgjort."); 

Eller i tilfelle 21:

 int resultat myTwentyOneGame.isTwentyOne (handC); hvis (resultat = 21) o.println ("Vi har tjueen!"); annet hvis (resultat> 21) o.println ("Vi mistet" + resultat); annet {o.println ("Vi tar et annet kort"); // ...} 

Testing og feilsøking

Det er veldig viktig å skrive testkode og eksempler mens du implementerer selve rammeverket. På denne måten vet du hele tiden hvor godt implementeringskoden fungerer; du innser fakta om funksjoner og detaljer om implementering. Hvis vi fikk mer tid, ville vi ha implementert poker - en slik testtilfelle ville gitt enda mer innsikt i problemet og ville ha vist hvordan vi kunne omdefinere rammeverket.

$config[zx-auto] not found$config[zx-overlay] not found