מנוע כרטיסים בג'אווה

הכל התחיל כאשר שמנו לב שיש מעט מאוד יישומי משחק קלפים או יישומונים שנכתבו בג'אווה. ראשית חשבנו לכתוב כמה משחקים, והתחלנו להבין את קוד הליבה והשיעורים הדרושים ליצירת משחקי קלפים. התהליך נמשך, אך כעת קיימת מסגרת יציבה למדי לשימוש ליצירת פתרונות משחק קלפים שונים. כאן אנו מתארים כיצד תוכננה מסגרת זו, כיצד היא פועלת, ואת הכלים והטריקים ששימשו להפיכתה ליעילה ויציבה.

שלב העיצוב

עם עיצוב מונחה עצמים, חשוב ביותר להכיר את הבעיה מבפנים ומבחוץ. אחרת, ניתן להשקיע זמן רב בתכנון שיעורים ופתרונות שאינם נחוצים או שלא יעבדו בהתאם לצרכים הספציפיים. במקרה של משחקי קלפים, גישה אחת היא לדמיין מה קורה כאשר אדם אחד, שניים או יותר משחקים קלפים.

חפיסת קלפים מכילה בדרך כלל 52 קלפים בארבע חליפות שונות (יהלומים, לבבות, מועדונים, עלים), עם ערכים הנעים בין דוס למלך, בתוספת האס. מיד נוצרת בעיה: בהתאם לחוקי המשחק, האסים יכולים להיות ערך הקלף הנמוך ביותר, הגבוה ביותר או שניהם.

יתר על כן, ישנם שחקנים שלוקחים קלפים מהחפיסה ביד ומנהלים את היד על פי כללים. באפשרותך להציג את הקלפים לכולם על ידי הנחתם על השולחן או להסתכל עליהם באופן פרטי. תלוי בשלב המסוים של המשחק, ייתכן שיהיה בידך מספר קלפים.

ניתוח השלבים בדרך זו מגלה דפוסים שונים. כעת אנו משתמשים בגישה מבוססת-מקרה, כמתואר לעיל, המתועדת בהנדסת תוכנה מונחה עצמים של איוור ג'ייקובסון . בספר זה, אחד הרעיונות הבסיסיים הוא לעצב שיעורים על בסיס מצבים אמיתיים. זה מקל הרבה יותר על הבנת האופן בו היחסים פועלים, מה תלוי במה ואיך פועלים ההפשטות.

יש לנו שיעורים כמו CardDeck, Hand, Card ו- RuleSet. CardDeck יכיל 52 אובייקטים של כרטיס בהתחלה, ו- CardDeck יכלול פחות אובייקטים של כרטיס מכיוון שאלו נמשכים לאובייקט יד. חפצי יד מדברים עם אובייקט RuleSet המכיל את כל הכללים הנוגעים למשחק. חשוב על RuleSet כעל ספר המשחק.

שיעורי וקטור

במקרה זה, היינו זקוקים למבנה נתונים גמיש המטפל בשינויי כניסה דינמיים, אשר ביטל את מבנה הנתונים של מערך. רצינו גם דרך קלה להוסיף אלמנט הוספה ולהימנע מקידוד רב במידת האפשר. ישנם פתרונות שונים, כגון צורות שונות של עצים בינאריים. עם זאת, בחבילה java.util יש מחלקה וקטורית המיישמת מערך של אובייקטים שגדלים ומתכווצים לגודל לפי הצורך, וזה היה בדיוק מה שהיינו צריכים. (הפונקציות של חברי הווקטור אינן מוסברות במלואן בתיעוד הנוכחי; מאמר זה יסביר עוד כיצד ניתן להשתמש במחלקה הווקטורית במקרים דומים של רשימות אובייקטים דינמיות.) החיסרון בשיעורי וקטור הוא שימוש נוסף בזיכרון, בגלל זיכרון רב. ההעתקה נעשית מאחורי הקלעים. (מסיבה זו, מערכים תמיד טובים יותר; הם סטטיים בגודלם,כך שהמהדר יוכל להבין דרכים לייעל את הקוד). כמו כן, עם קבוצות גדולות יותר של אובייקטים, אנו עשויים לקבל קנסות בנוגע לזמני החיפוש, אך הווקטור הגדול ביותר שיכולנו לחשוב עליו היה 52 רשומות. זה עדיין סביר במקרה זה, וזמני בדיקה ארוכים לא היו חשש.

להלן הסבר קצר על אופן תכנון והטמעה של כל שיעור.

שיעור קלפים

מחלקת הכרטיסים היא פשוטה מאוד: היא מכילה ערכים המסמנים את הצבע והערך. יתכן שיש בו גם מצביעים לתמונות GIF ויישויות דומות המתארות את הכרטיס, כולל התנהגות פשוטה אפשרית כגון אנימציה (להעיף כרטיס) וכן הלאה.

Class Card מיישם CardConstants {public int color; ערך אינטנסיבי ציבורי; שם מחרוזת ציבורי ציבורי; }

חפצי כרטיס אלה נשמרים אז בכיתות וקטור שונות. שים לב שהערכים של הכרטיסים, כולל הצבע, מוגדרים בממשק, מה שאומר שכל מחלקה במסגרת יכולה ליישם וכך לכלול את הקבועים:

ממשק CardConstants {// שדות ממשק הם תמיד גמר סטטי ציבורי! int לבבות 1; int DIAMOND 2; int SPADE 3; int CLUBS 4; int JACK 11; מלכה 12; int KING 13; int ACE_LOW 1; int ACE_HIGH 14; }

מחלקת CardDeck

בכיתה CardDeck יהיה אובייקט וקטור פנימי, שיאותחל מראש עם 52 אובייקטים של כרטיס. זה נעשה בשיטה הנקראת דשדוש. המשמעות היא שבכל פעם שאתה מדשדש, אתה אכן מתחיל משחק על ידי הגדרת 52 קלפים. יש צורך להסיר את כל האובייקטים הישנים האפשריים ולהתחיל ממצב ברירת המחדל שוב (52 אובייקטים של כרטיס).

shuffle public void public () {// אפס תמיד את וקטור הסיפון ואתחל אותו מאפס. deck.removeAllElements (); 20 // ואז הכניסו את 52 הקלפים. צבע אחד בכל פעם עבור (int i ACE_LOW; i <ACE_HIGH; i ++) {כרטיס aCard כרטיס חדש (); aCard.color HEARTS; aCard.value i; deck.addElement (aCard); } // עשו את אותו הדבר לגבי מועדונים, דיאמונדס וספיידים. }

כאשר אנו מציירים אובייקט כרטיס מה- CardDeck, אנו משתמשים במחולל מספרים אקראי שיודע את הסט שממנו יבחר מיקום אקראי בתוך הווקטור. במילים אחרות, גם אם חפצי הכרטיס מסודרים, הפונקציה האקראית תבחר מיקום שרירותי במסגרת האלמנטים בתוך הווקטור.

כחלק מתהליך זה, אנו מסירים גם את האובייקט בפועל מווקטור CardDeck כאשר אנו מעבירים אובייקט זה למחלקת היד. הכיתה הווקטורית ממפה את המצב האמיתי של חפיסת קלפים ויד על ידי העברת כרטיס:

משיכת כרטיס ציבורי () {Card aCard null; מיקום int (int) (Math.random () * (deck.size = ())); נסה את {aCard (Card) deck.elementAt (position); } לתפוס (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } deck.removeElementAt (position); החזר כרטיס; }

שים לב כי טוב לתפוס כל יוצא מן הכלל הקשור לקיחת אובייקט מהווקטור ממיקום שאינו קיים.

קיימת שיטת שירות שמתבצעת דרך כל האלמנטים בווקטור ומכנה שיטה אחרת שתזרוק מחרוזת ערך / צבע זוג ASCII. תכונה זו שימושית בעת ניפוי באגים בכיתות הסיפון וגם בכיתות היד. תכונות המניין של הווקטורים משמשות הרבה בכיתת היד:

dump dump ריק () {Enumeration enum deck.elements (); while (enum.hasMoreElements ()) {כרטיס כרטיס (כרטיס) enum.nextElement (); RuleSet.printValue (כרטיס); }}

כיתת ידיים

חוג היד הוא סוס עבודה אמיתי במסגרת זו. רוב ההתנהגות הנדרשת הייתה משהו שמאוד טבעי להציב בכיתה זו. דמיין אנשים שאוחזים קלפים בידיהם ועושים פעולות שונות תוך שהם מסתכלים על חפצי הכרטיס.

ראשית, אתה צריך גם וקטור, מכיוון שלא ידוע במקרים רבים כמה קלפים ייאספו. למרות שאתה יכול ליישם מערך, טוב שיש כאן גם גמישות מסוימת. השיטה הכי טבעית שאנחנו צריכים היא לקחת כרטיס:

לקיחת בטל פומבי (Card theCard) {cardHand.addElement (theCard); }

CardHandהוא וקטור, אז אנחנו רק מוסיפים את אובייקט הכרטיס לווקטור הזה. עם זאת, במקרה של פעולות ה"פלט "מהיד, יש לנו שני מקרים: אחד בו אנו מראים את הקלף, ואחד בו שנינו מראים ושואבים את הקלף מהיד. עלינו ליישם את שניהם, אך באמצעות ירושה אנו כותבים פחות קוד מכיוון שציור והצגת כרטיס הוא מקרה מיוחד מרק הצגת כרטיס:

הצגת כרטיס ציבורי (מיקום int) {כרטיס aCard null; נסה את {aCard (Card) cardHand.elementAt (position); } לתפוס (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } להחזיר כרטיס; } 20 שרטוטים של כרטיסים ציבוריים (מיקום int) {הצגת כרטיס כרטיס (מיקום); cardHand.removeElementAt (מיקום); החזר כרטיס; }

במילים אחרות, תיק הגרפיקה הוא מקרה ראווה, עם התנהגות נוספת של הסרת האובייקט מווקטור היד.

בכתיבת קוד מבחן לשיעורים השונים מצאנו מספר הולך וגדל של מקרים בהם היה צורך לברר אודות ערכים מיוחדים שונים ביד. לדוגמא, לפעמים היינו צריכים לדעת כמה קלפים מסוג מסוים נמצאים ביד. או שהערך הנמוך של ברירת המחדל של אס היה צריך להיות שונה ל- 14 (הערך הגבוה ביותר) ובחזרה. בכל מקרה התמיכה התנהגותית הועברה בחזרה לכיתת היד, מכיוון שזה היה מקום טבעי מאוד להתנהגות כזו. שוב, זה היה כמעט כאילו מוח אנושי עומד מאחורי היד שעושה את החישובים האלה.

ניתן להשתמש בתכונת המניין של הווקטורים בכדי לגלות כמה קלפים בעלי ערך ספציפי היו בשיעור יד:

כרטיסי int ציבורי NC (ערך int) {int n 0; Enumeration enum cardHand.elements (); while (enum.hasMoreElements ()) {tempCard (Card) enum.nextElement (); // = tempCard מוגדר אם (tempCard.value = value) n ++; } להחזיר n; }

באופן דומה, תוכל לחזור על עצמי הכרטיס ולחשב את סכום הקלפים הכולל (כמו במבחן 21), או לשנות את ערך הכרטיס. שים לב, כברירת מחדל, כל האובייקטים הם הפניות ב- Java. אם אתה מאחזר את מה שאתה חושב שהוא אובייקט זמני ומשנה אותו, הערך בפועל ישתנה גם בתוך האובייקט המאוחסן על ידי הווקטור. זהו נושא חשוב לזכור.

כיתת RuleSet

The RuleSet class is like a rule book that you check now and then when you play a game; it contains all the behavior concerning the rules. Note that the possible strategies a game player may use are based either on user interface feedback or on simple or more complex artificial intelligence (AI) code. All the RuleSet worries about is that the rules are followed.

Other behaviors related to cards were also placed into this class. For example, we created a static function that prints the card value information. Later, this could also be placed into the Card class as a static function. In the current form, the RuleSet class has just one basic rule. It takes two cards and sends back information about which card was the highest one:

 public int higher (Card one, Card two) { int whichone 0; if (one.value= ACE_LOW) one.value ACE_HIGH; if (two.value= ACE_LOW) two.value ACE_HIGH; // In this rule set the highest value wins, we don't take into // account the color. if (one.value > two.value) whichone 1; if (one.value < two.value) whichone 2; if (one.value= two.value) whichone 0; // Normalize the ACE values, so what was passed in has the same values. if (one.value= ACE_HIGH) one.value ACE_LOW; if (two.value= ACE_HIGH) two.value ACE_LOW; return whichone; } 

You need to change the ace values that have the natural value of one to 14 while doing the test. It's important to change the values back to one afterward to avoid any possible problems as we assume in this framework that aces are always one.

In the case of 21, we subclassed RuleSet to create a TwentyOneRuleSet class that knows how to figure out if the hand is below 21, exactly 21, or above 21. It also takes into account the ace values that could be either one or 14, and tries to figure out the best possible value. (For more examples, consult the source code.) However, it's up to the player to define the strategies; in this case, we wrote a simple-minded AI system where if your hand is below 21 after two cards, you take one more card and stop.

How to use the classes

It is fairly straightforward to use this framework:

 myCardDeck new CardDeck (); myRules new RuleSet (); handA new Hand (); handB new Hand (); DebugClass.DebugStr ("Draw five cards each to hand A and hand B"); for (int i 0; i < NCARDS; i++) { handA.take (myCardDeck.draw ()); handB.take (myCardDeck.draw ()); } // Test programs, disable by either commenting out or using DEBUG flags. testHandValues (); testCardDeckOperations(); testCardValues(); testHighestCardValues(); test21(); 

The various test programs are isolated into separate static or non-static member functions. Create as many hands as you want, take cards, and let the garbage collection get rid of unused hands and cards.

You call the RuleSet by providing the hand or card object, and, based on the returned value, you know the outcome:

 DebugClass.DebugStr ("Compare the second card in hand A and Hand B"); int winner myRules.higher (handA.show (1), = handB.show (1)); if (winner= 1) o.println ("Hand A had the highest card."); else if (winner= 2) o.println ("Hand B had the highest card."); else o.println ("It was a draw."); 

Or, in the case of 21:

 int result myTwentyOneGame.isTwentyOne (handC); if (result= 21) o.println ("We got Twenty-One!"); else if (result > 21) o.println ("We lost " + result); else { o.println ("We take another card"); // ... } 

Testing and debugging

חשוב מאוד לכתוב קוד מבחן ודוגמאות תוך יישום המסגרת בפועל. בדרך זו אתה יודע בכל עת עד כמה קוד ההטמעה עובד; אתה מבין עובדות על תכונות ופרטים על יישום. בהינתן זמן רב יותר היינו מיישמים פוקר - מקרה מבחן כזה היה מספק תובנה רבה יותר לבעיה והיה מראה כיצד להגדיר מחדש את המסגרת.