Programmering

Kortmotor i Java

Det hele startede, da vi bemærkede, at der var meget få kortspilapplikationer eller applets skrevet i Java. Først tænkte vi på at skrive et par spil og startede med at finde ud af kernekoden og klasser, der var nødvendige for at skabe kortspil. Processen fortsætter, men nu er der en forholdsvis stabil ramme, der skal bruges til at skabe forskellige kortspilløsninger. Her beskriver vi, hvordan denne ramme blev designet, hvordan den fungerer, og de værktøjer og tricks, der blev brugt til at gøre den nyttig og stabil.

Designfase

Med objektorienteret design er det ekstremt vigtigt at kende problemet indefra og ude. Ellers er det muligt at bruge meget tid på at designe klasser og løsninger, der ikke er nødvendige eller ikke fungerer efter specifikke behov. I tilfælde af kortspil er en tilgang at visualisere, hvad der sker, når en, to eller flere personer spiller kort.

Et kortspil indeholder normalt 52 kort i fire forskellige dragter (diamanter, hjerter, køller, spar) med værdier, der spænder fra deuce til kongen plus esset. Der opstår straks et problem: afhængigt af spillereglerne kan esserne være enten den laveste kortværdi, den højeste eller begge dele.

Derudover er der spillere, der tager kort fra bunken i en hånd og styrer hånden baseret på regler. Du kan enten vise kortene til alle ved at placere dem på bordet eller se på dem privat. Afhængigt af den særlige fase af spillet kan du have N antal kort i din hånd.

Analyse af stadierne på denne måde afslører forskellige mønstre. Vi bruger nu en sagsstyret tilgang, som beskrevet ovenfor, der er dokumenteret i Ivar Jacobsons Objektorienteret softwareudvikling. I denne bog er en af ​​de grundlæggende ideer at modellere klasser baseret på virkelige situationer. Det gør det meget lettere at forstå, hvordan relationer fungerer, hvad der afhænger af hvad, og hvordan abstraktionerne fungerer.

Vi har klasser som CardDeck, Hand, Card og RuleSet. Et CardDeck vil indeholde 52 kortobjekter i starten, og CardDeck vil have færre kortobjekter, da disse trækkes ind i et håndobjekt. Håndgenstande taler med et RuleSet-objekt, der har alle reglerne for spillet. Tænk på en RuleSet som spilhåndbogen.

Vektorklasser

I dette tilfælde havde vi brug for en fleksibel datastruktur, der håndterer ændringer af dynamiske indtastninger, hvilket eliminerede Array-datastrukturen. Vi ønskede også en nem måde at tilføje et indsatselement og undgå en masse kodning, hvis det er muligt. Der er forskellige løsninger tilgængelige, såsom forskellige former for binære træer. Imidlertid har java.util-pakken en Vector-klasse, der implementerer en række objekter, der vokser og krymper i størrelse efter behov, hvilket var nøjagtigt det, vi havde brug for. (Vector-medlemsfunktionerne forklares ikke fuldt ud i den aktuelle dokumentation; denne artikel forklarer yderligere, hvordan Vector-klassen kan bruges til lignende forekomster af dynamiske objektlister.) Ulempen med Vector-klasser er ekstra hukommelsesbrug på grund af meget hukommelse kopiering udført bag kulisserne. (Af denne grund er Arrays altid bedre; de ​​er statiske i størrelse, så compileren kunne finde ud af måder at optimere koden på). Også med større sæt objekter har vi muligvis sanktioner vedrørende opslagstider, men den største vektor, vi kunne tænke på, var 52 poster. Det er stadig rimeligt for denne sag, og lange opslagstider var ikke et problem.

En kort forklaring på, hvordan hver klasse blev designet og implementeret følger.

Kortklasse

Kortklassen er meget enkel: den indeholder værdier, der signalerer farven og værdien. Det kan også have henvisninger til GIF-billeder og lignende enheder, der beskriver kortet, herunder mulig simpel opførsel såsom animation (vend et kort) og så videre.

klasse Card implementerer CardConstants {public int color; offentlig int-værdi; offentlig String ImageName; } 

Disse kortobjekter gemmes derefter i forskellige vektorklasser. Bemærk, at kortværdierne, inklusive farve, er defineret i en grænseflade, hvilket betyder, at hver klasse i rammen kan implementeres, og på denne måde inkluderer konstanterne:

interface CardConstants {// interface-felter er altid offentlige statiske endelige! int HJERTER 1; int DIAMANT 2; int SPADE 3; int CLUBS 4; int JACK 11; int QUEEN 12; int KONGE 13; int ACE_LOW 1; int ACE_HIGH 14; } 

CardDeck-klasse

CardDeck-klassen har et internt Vector-objekt, der initialiseres med 52 kortobjekter. Dette gøres ved hjælp af en metode kaldet shuffle. Implikationen er, at hver gang du blander, starter du faktisk et spil ved at definere 52 kort. Det er nødvendigt at fjerne alle mulige gamle objekter og starte fra standardtilstanden igen (52 kortobjekter).

 public void shuffle () {// Nulstil altid dækvektoren og initialiser den fra bunden. deck.removeAllElements (); 20 // Indsæt derefter de 52 kort. Én farve ad gangen for (int i ACE_LOW; i <ACE_HIGH; i ++) {Card aCard new Card (); aCard.color HJERTER; aCard.value i; deck.addElement (aCard); } // Gør det samme for CLUBS, DIAMONDS og SPADES. } 

Når vi tegner et kortobjekt fra CardDeck, bruger vi en tilfældig talgenerator, der kender det sæt, hvorfra det vælger en tilfældig position inde i vektoren. Med andre ord, selvom kortobjekterne er ordnet, vælger den tilfældige funktion en vilkårlig position inden for omfanget af elementerne inde i vektoren.

Som en del af denne proces fjerner vi også det faktiske objekt fra CardDeck-vektoren, når vi sender dette objekt til Hand-klassen. Vector-klassen kortlægger den virkelige situation for et kortspil og en hånd ved at give et kort:

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

Bemærk, at det er godt at fange eventuelle undtagelser relateret til at tage et objekt fra Vector fra en position, der ikke er til stede.

Der er en hjælpemetode, der gentager alle elementerne i vektoren og kalder en anden metode, der vil dumpe en ASCII-værdi / farveparstreng. Denne funktion er nyttig, når du debugger både klasserne Deck og Hand. Tællingsfunktionerne for vektorer bruges meget i klassen Hand:

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

Håndklasse

Hand-klassen er en rigtig arbejdshest i denne ramme. Det meste af den krævede adfærd var noget, der var meget naturligt at placere i denne klasse. Forestil dig, at folk holder kort i hænderne og foretager forskellige operationer, mens de ser på kortgenstandene.

For det første har du også brug for en vektor, da det i mange tilfælde er ukendt, hvor mange kort der bliver samlet op. Selvom du kunne implementere en matrix, er det også godt at have en vis fleksibilitet her. Den mest naturlige metode, vi har brug for, er at tage et kort:

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

CardHand er en vektor, så vi tilføjer bare kortobjektet i denne vektor. I tilfælde af "output" -operationer fra hånden har vi imidlertid to tilfælde: en, hvor vi viser kortet, og en, hvor vi begge viser og trækker kortet fra hånden. Vi er nødt til at implementere begge dele, men ved hjælp af arv skriver vi mindre kode, fordi tegning og visning af et kort er et specielt tilfælde fra bare at vise et kort:

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

Med andre ord er draw case et showcase med den yderligere adfærd at fjerne objektet fra håndvektoren.

Ved skrivning af testkode for de forskellige klasser fandt vi et stigende antal tilfælde, hvor det var nødvendigt at finde ud af forskellige specialværdier i hånden. For eksempel havde vi nogle gange brug for at vide, hvor mange kort af en bestemt type der var i hånden. Eller standard ess lav værdi af en skulle ændres til 14 (højeste værdi) og tilbage igen. I alle tilfælde blev adfærdsstøtten delegeret tilbage til Hand-klassen, da det var et meget naturligt sted for sådan adfærd. Igen var det næsten som om en menneskelig hjerne var bag hånden og foretog disse beregninger.

Opregningsfunktionen i vektorer kan bruges til at finde ud af, hvor mange kort af en bestemt værdi der var til stede i håndklassen:

 offentlige int NCards (int værdi) {int n 0; Enumeration enum cardHand.elements (); while (enum.hasMoreElements ()) {tempCard (Card) enum.nextElement (); // = tempCard defineret hvis (tempCard.value = værdi) n ++; } returner n; } 

På samme måde kan du gentage kortobjekterne og beregne den samlede sum af kort (som i 21-testen) eller ændre værdien på et kort. Bemærk, at alle objekter som standard er referencer i Java. Hvis du henter det, du synes er et midlertidigt objekt og ændrer det, ændres den aktuelle værdi også inde i det objekt, der er gemt af vektoren. Dette er et vigtigt spørgsmål at huske på.

RuleSet klasse

RuleSet-klassen er som en regelbog, som du tjekker nu og da, når du spiller et spil; den indeholder al adfærd vedrørende reglerne. Bemærk, at de mulige strategier, som en spilspiller kan bruge, er baseret på feedback fra brugergrænsefladen eller på simpel eller mere kompleks kunstig intelligens (AI) -kode. Alt, hvad RuleSet bekymrer sig om, er at reglerne følges.

Andre adfærd relateret til kort blev også placeret i denne klasse. For eksempel oprettede vi en statisk funktion, der udskriver kortværdioplysningerne. Senere kunne dette også placeres i kortklassen som en statisk funktion. I den nuværende form har RuleSet-klassen kun en grundlæggende regel. Det tager to kort og sender information tilbage om, hvilket kort der var det højeste:

 offentlig int højere (kort en, kort to) {int hvilken 0; hvis (one.value = ACE_LOW) one.value ACE_HIGH; hvis (two.value = ACE_LOW) two.value ACE_HIGH; // I denne regel indstilles den højeste værdi vinder, vi tager ikke // hensyn til farven. hvis (one.value> two.value) hvilken 1; hvis (one.value <two.value) hvilken 2; hvis (one.value = two.value) hvilken 0; // Normaliser ACE-værdierne, så det, der blev sendt ind, har de samme værdier. hvis (one.value = ACE_HIGH) one.value ACE_LOW; hvis (two.value = ACE_HIGH) two.value ACE_LOW; returner hvilken; } 

Du skal ændre essværdierne, der har den naturlige værdi fra 1 til 14, mens du udfører testen. Det er vigtigt at ændre værdierne til en bagefter for at undgå eventuelle problemer, da vi i denne ramme antager, at ess altid er et.

I tilfælde af 21 underklassede vi RuleSet for at oprette en TwentyOneRuleSet-klasse, der ved, hvordan man finder ud af, om hånden er under 21, nøjagtigt 21 eller over 21. Det tager også højde for de essværdier, der kunne være enten en eller 14 og forsøger at finde ud af den bedst mulige værdi. (For flere eksempler, se kildekoden.) Det er dog op til spilleren at definere strategierne; i dette tilfælde skrev vi et simpelt AI-system, hvor hvis din hånd er under 21 efter to kort, tager du et kort mere og stopper.

Sådan bruges klasser

Det er ret ligetil at bruge denne ramme:

 myCardDeck nyt 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 enten at kommentere eller bruge DEBUG-flag. testHandValues ​​(); testCardDeckOperations (); testCardValues ​​(); testHighestCardValues ​​(); test21 (); 

De forskellige testprogrammer er isoleret i separate statiske eller ikke-statiske medlemsfunktioner. Opret så mange hænder, som du vil, tag kort, og lad affaldssamlingen slippe af med ubrugte hænder og kort.

Du ringer til RuleSet ved at give hånd- eller kortobjektet, og baseret på den returnerede værdi kender du resultatet:

 DebugClass.DebugStr ("Sammenlign det andet kort i hånd A og hånd B"); int vinder myRules.higher (handA.show (1), = handB.show (1)); hvis (vinder = 1) o.println ("Hånd A havde det højeste kort."); ellers hvis (vinder = 2) o.println ("Hånd B havde det højeste kort."); ellers o.println ("Det var uafgjort."); 

Eller i tilfælde af 21:

 int resultat myTwentyOneGame.isTwentyOne (handC); hvis (resultat = 21) o.println ("Vi fik Enogtyve!"); ellers hvis (resultat> 21) o.println ("Vi mistede" + resultat); ellers {o.println ("Vi tager et andet kort"); // ...} 

Test og fejlretning

Det er meget vigtigt at skrive testkode og eksempler, mens du implementerer den aktuelle ramme. På denne måde ved du hele tiden, hvor godt implementeringskoden fungerer; du indser fakta om funktioner og detaljer om implementering. Hvis vi fik mere tid, ville vi have implementeret poker - en sådan testtilfælde ville have givet endnu mere indsigt i problemet og ville have vist, hvordan man omdefinerede rammen.