Προγραμματισμός

Μηχανή καρτών σε Java

Όλα ξεκίνησαν όταν παρατηρήσαμε ότι υπήρχαν πολύ λίγες εφαρμογές παιχνιδιών με κάρτες ή μικροεφαρμογές γραμμένες στην Java. Αρχικά σκεφτήκαμε να γράψουμε μερικά παιχνίδια και ξεκινήσαμε με τον υπολογισμό του βασικού κώδικα και των τάξεων που απαιτούνται για τη δημιουργία παιχνιδιών με κάρτες. Η διαδικασία συνεχίζεται, αλλά τώρα υπάρχει ένα αρκετά σταθερό πλαίσιο για χρήση για τη δημιουργία διαφόρων λύσεων παιχνιδιού με κάρτες. Εδώ περιγράφουμε πώς σχεδιάστηκε αυτό το πλαίσιο, πώς λειτουργεί και τα εργαλεία και κόλπα που χρησιμοποιήθηκαν για να το καταστήσουν χρήσιμο και σταθερό.

Φάση σχεδιασμού

Με αντικειμενοστρεφή σχεδίαση, είναι εξαιρετικά σημαντικό να γνωρίζετε το πρόβλημα μέσα και έξω. Διαφορετικά, είναι δυνατό να αφιερώσετε πολύ χρόνο στο σχεδιασμό τάξεων και λύσεων που δεν χρειάζονται ή δεν θα λειτουργούν σύμφωνα με συγκεκριμένες ανάγκες. Στην περίπτωση παιχνιδιών με κάρτες, μία προσέγγιση είναι να απεικονιστεί τι συμβαίνει όταν ένα, δύο ή περισσότερα άτομα παίζουν χαρτιά.

Μια τράπουλα συνήθως περιέχει 52 φύλλα σε τέσσερα διαφορετικά κοστούμια (διαμάντια, καρδιές, κλαμπ, μπαστούνια), με τιμές που κυμαίνονται από το deuce έως τον king, συν τον άσο. Αμέσως προκύπτει ένα πρόβλημα: ανάλογα με τους κανόνες του παιχνιδιού, οι άσοι μπορεί να είναι είτε η χαμηλότερη αξία κάρτας, η υψηλότερη ή και τα δύο.

Επιπλέον, υπάρχουν παίκτες που παίρνουν φύλλα από το τράπουλα στο χέρι και διαχειρίζονται το χέρι με βάση κανόνες. Μπορείτε είτε να δείξετε τις κάρτες σε όλους τοποθετώντας τις στο τραπέζι ή να τις δείτε ιδιωτικά. Ανάλογα με το συγκεκριμένο στάδιο του παιχνιδιού, μπορεί να έχετε Ν αριθμό καρτών στο χέρι σας.

Η ανάλυση των σταδίων με αυτόν τον τρόπο αποκαλύπτει διάφορα μοτίβα. Χρησιμοποιούμε τώρα μια προσέγγιση με βάση την υπόθεση, όπως περιγράφεται παραπάνω, η οποία τεκμηριώνεται στο Ivar Jacobson's Αντικειμενοστρεφής Μηχανική Λογισμικού. Σε αυτό το βιβλίο, μία από τις βασικές ιδέες είναι η μοντελοποίηση τάξεων βάσει πραγματικών καταστάσεων. Αυτό καθιστά πολύ πιο εύκολο να κατανοήσουμε πώς λειτουργούν οι σχέσεις, τι εξαρτάται από το τι και πώς λειτουργούν οι αφαιρέσεις.

Έχουμε μαθήματα όπως CardDeck, Hand, Card και RuleSet. Ένα CardDeck θα περιέχει 52 αντικείμενα Κάρτας στην αρχή και το CardDeck θα έχει λιγότερα αντικείμενα Κάρτας καθώς αυτά έχουν σχεδιαστεί σε ένα αντικείμενο Χεριού. Τα αντικείμενα χειρός μιλούν με ένα αντικείμενο RuleSet που έχει όλους τους κανόνες σχετικά με το παιχνίδι. Σκεφτείτε ένα RuleSet ως εγχειρίδιο παιχνιδιού.

Διάνυσμα μαθήματα

Σε αυτήν την περίπτωση, χρειαζόμασταν μια ευέλικτη δομή δεδομένων που χειρίζεται δυναμικές αλλαγές εισόδου, οι οποίες εξάλειψαν τη δομή δεδομένων Array. Θέλαμε επίσης έναν εύκολο τρόπο να προσθέσουμε ένα στοιχείο εισαγωγής και να αποφύγουμε πολλή κωδικοποίηση εάν είναι δυνατόν. Υπάρχουν διάφορες διαθέσιμες λύσεις, όπως διάφορες μορφές δυαδικών δέντρων. Ωστόσο, το πακέτο java.util έχει μια κλάση Vector που εφαρμόζει μια σειρά αντικειμένων που μεγαλώνει και συρρικνώνεται σε μέγεθος όπως απαιτείται, κάτι που ακριβώς χρειαζόμασταν. (Οι λειτουργίες του φορέα Vector δεν εξηγούνται πλήρως στην τρέχουσα τεκμηρίωση. Αυτό το άρθρο θα εξηγήσει περαιτέρω πώς μπορεί να χρησιμοποιηθεί η κλάση Vector για παρόμοιες παρουσίες λίστας δυναμικών αντικειμένων.) Το μειονέκτημα με τις κλάσεις Vector είναι πρόσθετη χρήση μνήμης, λόγω πολλής μνήμης η αντιγραφή έγινε πίσω από τα παρασκήνια. (Για αυτόν τον λόγο, οι συστοιχίες είναι πάντα καλύτερες · έχουν στατικό μέγεθος, οπότε ο μεταγλωττιστής θα μπορούσε να βρει τρόπους βελτιστοποίησης του κώδικα). Επίσης, με μεγαλύτερα σύνολα αντικειμένων, ενδέχεται να έχουμε ποινές σχετικά με τους χρόνους αναζήτησης, αλλά το μεγαλύτερο Vector που θα μπορούσαμε να σκεφτούμε ήταν 52 καταχωρήσεις. Αυτό εξακολουθεί να είναι λογικό για αυτήν την περίπτωση και οι μεγάλοι χρόνοι αναζήτησης δεν ήταν ανησυχητικοί.

Ακολουθεί μια σύντομη εξήγηση για το πώς σχεδιάστηκε και εφαρμόστηκε κάθε τάξη.

Κατηγορία καρτών

Η κλάση καρτών είναι πολύ απλή: περιέχει τιμές που σηματοδοτούν το χρώμα και την τιμή. Μπορεί επίσης να έχει δείκτες για εικόνες GIF και παρόμοιες οντότητες που περιγράφουν την κάρτα, συμπεριλαμβανομένης πιθανής απλής συμπεριφοράς, όπως κινούμενα σχέδια (αναστροφή κάρτας) και ούτω καθεξής.

Η κάρτα Class εφαρμόζει CardConstants {public int color; δημόσια αξία int? δημόσια συμβολοσειρά ImageName; } 

Αυτά τα αντικείμενα της κάρτας αποθηκεύονται στη συνέχεια σε διάφορες κατηγορίες Vector. Σημειώστε ότι οι τιμές για τις κάρτες, συμπεριλαμβανομένου του χρώματος, καθορίζονται σε μια διεπαφή, πράγμα που σημαίνει ότι κάθε κλάση στο πλαίσιο θα μπορούσε να εφαρμοστεί και με αυτόν τον τρόπο περιλαμβάνουν τις σταθερές:

διεπαφή CardConstants {// τα πεδία διεπαφής είναι πάντα δημόσια στατικά τελικά! int HEARTS 1; int DIAMOND 2; int SPADE 3; int CLUBS 4; int JACK 11; int QUEEN 12; int KING 13; int ACE_LOW 1; int ACE_HIGH 14; } 

Κατηγορία CardDeck

Η κλάση CardDeck θα έχει ένα εσωτερικό αντικείμενο Vector, το οποίο θα προκαταρκτικοποιηθεί με 52 αντικείμενα κάρτας. Αυτό γίνεται χρησιμοποιώντας μια μέθοδο που ονομάζεται shuffle. Η συνέπεια είναι ότι κάθε φορά που ανακατεύετε, ξεκινάτε πράγματι ένα παιχνίδι, ορίζοντας 52 φύλλα. Είναι απαραίτητο να αφαιρέσετε όλα τα πιθανά παλιά αντικείμενα και να ξεκινήσετε ξανά από την προεπιλεγμένη κατάσταση (52 αντικείμενα κάρτας).

 public void shuffle () {// Πάντα μηδενίστε το διάνυσμα καταστρώματος και αρχικοποιήστε το από το μηδέν. deck.removeAllElements (); 20 // Στη συνέχεια, εισαγάγετε τα 52 φύλλα. Ένα χρώμα κάθε φορά για (int i ACE_LOW; i <ACE_HIGH; i ++) {Card aCard new Card (); aCard.color HEARTS; aCard.value i; deck.addElement (aCard); } // Κάντε το ίδιο για CLUBS, DIAMONDS και SPADES. } 

Όταν σχεδιάζουμε ένα αντικείμενο κάρτας από το CardDeck, χρησιμοποιούμε μια γεννήτρια τυχαίων αριθμών που γνωρίζει το σετ από το οποίο θα επιλέξει μια τυχαία θέση μέσα στο διάνυσμα. Με άλλα λόγια, ακόμη και αν τα αντικείμενα της Κάρτας ταξινομηθούν, η τυχαία συνάρτηση θα επιλέξει μια αυθαίρετη θέση εντός του πεδίου των στοιχείων εντός του διανύσματος.

Ως μέρος αυτής της διαδικασίας, αφαιρούμε επίσης το πραγματικό αντικείμενο από το διάνυσμα CardDeck καθώς μεταδίδουμε αυτό το αντικείμενο στην κλάση Χεριών. Η κλάση Vector χαρτογραφεί την πραγματική κατάσταση μιας τράπουλας και ενός χεριού περνώντας μια κάρτα:

 δημόσια κλήρωση κάρτας () {Card aCard null; int θέση (int) (Math.random () * (deck.size = ())); δοκιμάστε το {aCard (Card) deck.elementAt (position); } catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } deck.removeElementAt (θέση); επιστροφή aCard; } 

Σημειώστε ότι είναι καλό να πιάσετε τυχόν πιθανές εξαιρέσεις που σχετίζονται με τη λήψη ενός αντικειμένου από τον Διάνυσμα από μια θέση που δεν είναι παρούσα.

Υπάρχει μια βοηθητική μέθοδος που επαναλαμβάνει όλα τα στοιχεία του διανύσματος και καλεί μια άλλη μέθοδο που θα πετάξει μια συμβολοσειρά τιμής / χρώματος ASCII. Αυτή η δυνατότητα είναι χρήσιμη κατά τον εντοπισμό σφαλμάτων και των τάξεων Deck και Hand. Τα χαρακτηριστικά απαρίθμησης των διανυσμάτων χρησιμοποιούνται πολύ στην κατηγορία Hand:

 public void dump () {Enumeration enum deck.elements (); while (enum.hasMoreElements ()) {Κάρτα κάρτας (Κάρτα) enum.nextElement (); RuleSet.printValue (κάρτα); }} 

Μάθημα χεριών

Το Hand class είναι ένα πραγματικό άλογο εργασίας σε αυτό το πλαίσιο. Το μεγαλύτερο μέρος της συμπεριφοράς που απαιτείται ήταν κάτι που ήταν πολύ φυσικό να τοποθετηθεί σε αυτήν την τάξη. Φανταστείτε ανθρώπους να κρατούν κάρτες στα χέρια τους και να κάνουν διάφορες λειτουργίες κοιτάζοντας τα αντικείμενα της κάρτας.

Πρώτον, χρειάζεστε επίσης ένα διάνυσμα, καθώς είναι άγνωστο σε πολλές περιπτώσεις πόσες κάρτες θα παραληφθούν. Παρόλο που θα μπορούσατε να εφαρμόσετε έναν πίνακα, είναι καλό να έχετε και κάποια ευελιξία εδώ. Η πιο φυσική μέθοδος που χρειαζόμαστε είναι να πάρουμε μια κάρτα:

 δημόσια άκυρη λήψη (Card theCard) {cardHand.addElement (theCard) · } 

Χαρτί είναι ένα διάνυσμα, οπότε προσθέτουμε απλώς το αντικείμενο Κάρτας σε αυτό το διάνυσμα. Ωστόσο, στην περίπτωση των χειρισμών «εξόδου» από το χέρι, έχουμε δύο περιπτώσεις: μία στην οποία δείχουμε την κάρτα και μία στην οποία και οι δύο δείχνουμε και τραβάμε την κάρτα από το χέρι. Πρέπει να εφαρμόσουμε και τα δύο, αλλά χρησιμοποιώντας την κληρονομιά γράφουμε λιγότερο κώδικα επειδή το σχέδιο και η εμφάνιση μιας κάρτας είναι μια ειδική περίπτωση από την απλή εμφάνιση μιας κάρτας:

 δημόσια κάρτα Εμφάνιση (int θέση) {Card aCard null; δοκιμάστε το {aCard (Card) cardHand.elementAt (position); } catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } επιστρέψτε aCard; } 20 δημόσια κλήρωση κάρτας (θέση int) {Card aCard show (θέση). cardHand.removeElementAt (θέση); επιστροφή aCard; } 

Με άλλα λόγια, η θήκη κλήρωσης είναι μια υπόθεση, με την πρόσθετη συμπεριφορά της αφαίρεσης του αντικειμένου από το διάνυσμα Χεριού.

Γράφοντας κώδικα δοκιμής για τις διάφορες κατηγορίες, βρήκαμε έναν αυξανόμενο αριθμό περιπτώσεων στις οποίες ήταν απαραίτητο να μάθουμε για διάφορες ειδικές τιμές στο χέρι. Για παράδειγμα, μερικές φορές χρειαζόταν να γνωρίζουμε πόσες κάρτες ενός συγκεκριμένου τύπου ήταν στο χέρι. Ή η προεπιλεγμένη χαμηλή τιμή άσσου ενός έπρεπε να αλλάξει σε 14 (υψηλότερη τιμή) και να επιστρέψει ξανά. Σε κάθε περίπτωση, η υποστήριξη συμπεριφοράς μεταβιβάστηκε πίσω στην κατηγορία Hand, καθώς ήταν ένα πολύ φυσικό μέρος για μια τέτοια συμπεριφορά. Και πάλι, ήταν σχεδόν σαν ένας ανθρώπινος εγκέφαλος πίσω από το χέρι να κάνει αυτούς τους υπολογισμούς.

Η δυνατότητα απαρίθμησης των διανυσμάτων μπορεί να χρησιμοποιηθεί για να μάθετε πόσες κάρτες συγκεκριμένης τιμής υπήρχαν στην κλάση Χεριών:

 δημόσια int NCards (int αξία) {int n 0; Enumation enum cardHand.elements (); ενώ (enum.hasMoreElements ()) {tempCard (Card) enum.nextElement (); // = tempCard ορίζεται εάν (tempCard.value = value) n ++; } επιστροφή n; } 

Παρομοίως, θα μπορούσατε να επαναλάβετε τα αντικείμενα της κάρτας και να υπολογίσετε το συνολικό άθροισμα των καρτών (όπως στο τεστ 21) ή να αλλάξετε την αξία μιας κάρτας. Σημειώστε ότι, από προεπιλογή, όλα τα αντικείμενα είναι αναφορές σε Java. Εάν ανακτήσετε αυτό που νομίζετε ότι είναι ένα προσωρινό αντικείμενο και το τροποποιήσετε, η πραγματική τιμή αλλάζει επίσης μέσα στο αντικείμενο που αποθηκεύεται από το διάνυσμα. Αυτό είναι ένα σημαντικό ζήτημα που πρέπει να θυμάστε.

Κατηγορία RuleSet

Η τάξη RuleSet είναι σαν ένα βιβλίο κανόνων που ελέγχετε τώρα και μετά όταν παίζετε ένα παιχνίδι. Περιέχει όλη τη συμπεριφορά που αφορά τους κανόνες. Λάβετε υπόψη ότι οι πιθανές στρατηγικές που μπορεί να χρησιμοποιήσει ένας παίκτης βασίζονται είτε στα σχόλια του περιβάλλοντος εργασίας χρήστη είτε σε έναν απλό ή πιο σύνθετο κώδικα τεχνητής νοημοσύνης (AI). Το μόνο που ανησυχεί το RuleSet είναι ότι ακολουθούνται οι κανόνες.

Άλλες συμπεριφορές που σχετίζονται με κάρτες τοποθετήθηκαν επίσης σε αυτήν την τάξη. Για παράδειγμα, δημιουργήσαμε μια στατική λειτουργία που εκτυπώνει τις πληροφορίες για την αξία της κάρτας. Αργότερα, αυτό θα μπορούσε επίσης να τοποθετηθεί στην κατηγορία Κάρτα ως στατική λειτουργία. Στην τρέχουσα φόρμα, η τάξη RuleSet έχει έναν μόνο βασικό κανόνα. Παίρνει δύο κάρτες και στέλνει πληροφορίες για το ποια κάρτα ήταν η υψηλότερη:

 δημόσιο int υψηλότερο (Κάρτα ένα, Κάρτα δύο) {int anyone 0; εάν (one.value = ACE_LOW) one.value ACE_HIGH; εάν (two.value = ACE_LOW) two.value ACE_HIGH; // Σε αυτόν τον κανόνα ορίστε τις υψηλότερες νίκες αξίας, δεν λαμβάνουμε υπόψη // το χρώμα. εάν (one.value> two.value) ποιο 1; εάν (one.value <two.value) το οποίο 2; αν (one.value = two.value) ποιο 0; // Ομαλοποιήστε τις τιμές ACE, οπότε αυτό που πέρασε έχει τις ίδιες τιμές. εάν (one.value = ACE_HIGH) one.value ACE_LOW; εάν (two.value = ACE_HIGH) two.value ACE_LOW; επιστροφή ποιο; } 

Πρέπει να αλλάξετε τις τιμές άσσο που έχουν τη φυσική τιμή από ένα έως 14 κατά τη διάρκεια της δοκιμής. Είναι σημαντικό να αλλάξετε τις τιμές σε μία μετά για να αποφύγετε τυχόν προβλήματα, καθώς υποθέτουμε σε αυτό το πλαίσιο ότι οι άσοι είναι πάντα ένας.

Στην περίπτωση του 21, υποτάξαμε το RuleSet για να δημιουργήσουμε μια κλάση TwentyOneRuleSet που ξέρει πώς να υπολογίσει εάν το χέρι είναι κάτω από 21, ακριβώς 21 ή πάνω από 21. Λαμβάνει επίσης υπόψη τις τιμές άσσο που θα μπορούσαν να είναι είτε ένα είτε 14, και προσπαθεί να βρει την καλύτερη δυνατή τιμή. (Για περισσότερα παραδείγματα, συμβουλευτείτε τον πηγαίο κώδικα.) Ωστόσο, εναπόκειται στον παίκτη να καθορίσει τις στρατηγικές. σε αυτήν την περίπτωση, γράψαμε ένα απλό σύστημα τεχνητής νοημοσύνης όπου εάν το χέρι σας είναι κάτω από 21 μετά από δύο φύλλα, παίρνετε ένα ακόμη φύλλο και σταματάτε.

Πώς να χρησιμοποιήσετε τα μαθήματα

Είναι αρκετά απλό να χρησιμοποιήσετε αυτό το πλαίσιο:

 myCardDeck νέο CardDeck (); myRules νέο RuleSet (); hand Ένα νέο χέρι (); handB new Hand (); DebugClass.DebugStr ("Σχεδιάστε πέντε φύλλα το καθένα στο χέρι A και στο χέρι B"); για (int i 0; i <NCARDS; i ++) {handA.take (myCardDeck.draw ()); handB.take (myCardDeck.draw ()); } // Δοκιμαστικά προγράμματα, απενεργοποιήστε είτε σχολιάζοντας είτε χρησιμοποιώντας σημαίες DEBUG. testHandValues ​​(); testCardDeckOperations (); testCardValues ​​(); testHighestCardValues ​​(); δοκιμή21 (); 

Τα διάφορα προγράμματα δοκιμών απομονώνονται σε ξεχωριστές στατικές ή μη στατικές λειτουργίες μελών. Δημιουργήστε όσα χέρια θέλετε, πάρτε κάρτες και αφήστε τη συλλογή σκουπιδιών να απαλλαγεί από αχρησιμοποίητα χέρια και κάρτες.

Καλείτε το RuleSet παρέχοντας το αντικείμενο χεριού ή κάρτας και, με βάση την επιστρεφόμενη αξία, γνωρίζετε το αποτέλεσμα:

 DebugClass.DebugStr ("Συγκρίνετε το δεύτερο φύλλο στο χέρι A και το χέρι B"); int νικητής myRules.higher (handA.show (1), = handB.show (1)); if (νικητής = 1) o.println ("Το χέρι Α είχε το υψηλότερο φύλλο."); αλλιώς εάν (νικητής = 2) o.println ("Το χέρι Β είχε το υψηλότερο φύλλο."); αλλιώς o.println ("Ήταν ισοπαλία."); 

Ή, στην περίπτωση των 21:

 int αποτέλεσμα myTwentyOneGame.isTwentyOne (handC); if (αποτέλεσμα = 21) o.println ("Έχουμε είκοσι ένα!"); αλλιώς εάν (αποτέλεσμα> 21) o.println ("Χάσαμε" + αποτέλεσμα); αλλιώς {o.println ("Παίρνουμε άλλη κάρτα"); // ...} 

Δοκιμή και εντοπισμός σφαλμάτων

Είναι πολύ σημαντικό να γράψετε κώδικα δοκιμής και παραδείγματα κατά την εφαρμογή του πραγματικού πλαισίου. Με αυτόν τον τρόπο, γνωρίζετε πάντα πόσο καλά λειτουργεί ο κώδικας εφαρμογής. συνειδητοποιείτε γεγονότα σχετικά με τα χαρακτηριστικά και λεπτομέρειες σχετικά με την εφαρμογή Με δεδομένο περισσότερο χρόνο, θα έχουμε εφαρμόσει το πόκερ - μια τέτοια δοκιμαστική περίπτωση θα παρείχε ακόμη περισσότερες πληροφορίες για το πρόβλημα και θα έδειχνε πώς να επαναπροσδιορίσουμε το πλαίσιο.