תכנות מתקדם ב-Java/פולימורפיזם/תרגילים

מתוך ויקיספר, אוסף הספרים והמדריכים החופשי

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

כללי המשחק[עריכה]

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

מהלך המשחק[עריכה]

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

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

שחקנים[עריכה]

במשחק יכולים להשתתף כמה סוגי שחקנים:

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

הנחיות ורמזים[עריכה]

  • כדי לממש את סוגי השחקנים השונים, השתמשו במחלקה מופשטת או ממשק בשם Player, אותו/ה יממשו סוגי השחקנים השונים, כאשר כל שחקן שמשתתף במשחק ייוצג על ידי אובייקט כזה. בתוכנית הראשית, השתמשו במערך בודד שמכיל אובייקטים מטיפוס Player כדי להכיל את השחקנים השונים (וכל הנתונים הדרושים להם). על פי הצורך, ניתן ליצור תתי-מחלקות נוספת עבור קבוצות שונות של סוגי שחקנים (כאשר אתם באים לשקול זאת, חשבו: האם יש נקודות המשותפות רק לחלק מסוגי השחקנים?).
  • תזכורת: כדי לזהות האם אובייקט מסויים הוא מסוג כלשהו ניתן להשתמש במילה השמורה instanceof, למשל: if (currentPlayer instanceof CrazyPlayer) .... זה יכול לסייע אם תבחרו לממש תתי-מחלקות נוספות ביניהן תצטרכו להבדיל.
  • קרוב לוודאי שתזדקקו ל-switch כלשהו בזמן האתחול. אמנם, קיימות שיטות מתוחכמות יותר שמאפשרות להימנע לחלוטין משימוש ב-switch, אך אין חובה להשתמש בהן כאן (אם המימוש שלכם נכון, השינוי שיידרש במקרה של הוספת שחקן חדש יהיה זניח).
  • הגדירו במחלקה הראשית (זו שמריצה את המשחק) משתנה קבוע מטיפוס int שיגדיר מהו המספר המקסימלי שיכול המחשב לנחש. המספר המינימלי הוא 0.
  • שימוש בשיטות ריקות במחלקה יורשת נחשב במרבית המקרים להרגל רע. השתדלו להימנע מכך.
  • זהו תרגיל שעשוי לקחת זמן רב - הקדישו זמן לתכנון, ונסו לעשות זאת בכוחות עצמכם.


פתרון
// GuessGame.java

import java.util.Scanner;

public class GuessGame {

	/**
	 * Possible results for number guessing
	 */ 
	public enum Result {HIGH, LOW, EQUAL};

	/** 
	 * Max possible number
	 */
	public static final int RANGE = 1000;

	// This array contains all players in the game
	private Player[] _players;
	// This is the number that the computer guessed
	private int _number;

	/*
	 * Check a given number, compared to the computer's number: too high, too low, or the correct one 
	 */
	private Result check(int number) {
		if (number > _number) return Result.HIGH;
		if (number < _number) return Result.LOW;
		return Result.EQUAL;
	}

	/*
	 * Given a player type (represented by integer), returns new instance of this player type
	 */
	private Player getPlayer(int type) {
		switch(type) {
		case 0: 
			return new HumanPlayer();
		case 1:
			return new MonkeyPlayer();
		case 2:
			return new AIMediumPlayer();
		case 3:
			return new AISmartPlayer();
		default:
			return null;
		}
	}

	/*
	 * Prints error message and exits the program
	 */
	private void terminate(String msg) {
		System.err.println(msg+", aborting.");
		System.exit(0);
	}

	/*
	 * Refresh game data - the computer's number and the players' data
	 */
	private void refreshGameData() {
		_number = (int) (Math.random() * RANGE);
		for(int i = 0; i < _players.length; i++) {
			if(_players[i] instanceof AIPlayer) {
				((AIPlayer) _players[i]).init();
			}
		}
	}
	
	/**
	 * This method initializes all the required information for the game play
	 */
	public void init() {
		Scanner s = new Scanner(System.in);
		System.out.print("Enter number of players (1 - 6): ");
		int playersNumber = s.nextInt();
		if(playersNumber < 1 || playersNumber > 6) {
			terminate("Illegal number of players");
		}
		_players = new Player[playersNumber];
		for(int i = 0; i < _players.length; i++) {
			System.out.print("Enter name for player " + i + ": ");
			String name = s.next();
			System.out.print("Enter type for " + name + " (0 - 3): ");
			int type = s.nextInt();
			_players[i] = getPlayer(type);
			if(_players[i] == null) {
				terminate("No such type");
			}
			_players[i].setName(name);
		}
	}

	/**
	 * This method prints the game results
	 */
	public void printResults() {
		int maxPoints = 0;
		int winner = -1;
		System.out.println("Game results");
		for(int i = 0; i < _players.length; i++) {
			System.out.println(_players[i].getName()+"\t\t"+_players[i].getPoints());
			if(_players[i].getPoints() > maxPoints) {
				maxPoints = _players[i].getPoints();
				winner = i;
			}
		}
		if(winner == -1) {
			System.out.println("No winner!");
		}
		else {
			System.out.println("The winner is "+_players[winner].getName()+" with "+maxPoints+" points.");
		}
	}

	/*
	 * Announce the beginning of a game
	 */
	private void announceGame() {
		System.out.println("Welcome to the game! I'm thinking about a number between 0 and "+RANGE+".");
		System.out.println("Can you guess it?");
	}

	/*
	 * Ask the player if they want to play again. Returns true if the player want to play again, false if not
	 */
	private boolean playAgain() {
		System.out.print("Play again (y/n)? ");
		Scanner s = new Scanner(System.in);
		String res = s.next();
		if(res.equalsIgnoreCase("y")) return true;
		return false;
	}

	/*
	 * Returns a string given that describes a guess result
	 */
	private String announceResults(Result result) {
		switch(result) {
		case HIGH:
			return "and it was too high";
		case LOW:
			return "and it was too low";
		case EQUAL:
			return "and it was right";
		default:
			return "";
		}
	}

	/**
	 * Runs the game
	 */
	public void play() {
		boolean gameOn = true, playing;
		do {
			refreshGameData();
			announceGame();
			playing = true;
			while(playing) {
				for(int i = 0; i < _players.length && playing; i++) {
					int guess = _players[i].guess();
					Result result = check(guess);
					System.out.println(_players[i].getName()+" guessed "+guess+" "+announceResults(result));
					if(result != Result.EQUAL) {
						for(int j = 0; j < _players.length; j++) {
							if(_players[j] instanceof AIPlayer) {
								((AIPlayer) _players[j]).listen(guess, result);
							}
						}
					} else {
						System.out.println(_players[i].getName()+" wins!");
						_players[i].setPoints(_players[i].getPoints() + 1);
						playing = false;
					}
				}
			}
			gameOn = playAgain();
		} while(gameOn);
	}

	/**
	 * Driver
	 */
	public static void main(String[] args) {
		GuessGame game = new GuessGame();
		game.init();
		game.play();
		game.printResults();
	}

}

// Player.java

public abstract class Player {
	
	// Current player score
	private int _points;
	// Name of that player
	private String _name;

	/**
	 * Constructor
	 */
	public Player() {
		_points = 0;
	}

	/**
	 * @return Name of that player
	 */
	public String getName() {
		return _name;
	}

	/**
	 * Set the player name
	 */
	public void setName(String name) {
		_name = name;
	}
	
	/**
	 * @return Current player score
	 */
	public int getPoints() {
		return _points;
	}

	/**
	 * Set player's score
	 * @param points New score to set
	 */
	public void setPoints(int points) {
		_points = points;
	}

	/**
	 * Guess a number in the range
	 */
	public abstract int guess();

}

// HumanPlayer.java

import java.util.Scanner;

public class HumanPlayer extends Player {

	@Override
	public int guess() {
		System.out.print(getName()+ ", Enter your guess: ");
		Scanner s = new Scanner(System.in);
		return s.nextInt();
	}

}

// MonkeyPlayer.java

public class MonkeyPlayer extends Player {

	@Override
	public int guess() {
		return (int) (Math.random() * GuessGame.RANGE);
	}

}

// AIPlayer.java

public abstract class AIPlayer extends Player {

	// Minimum number in range
	protected int _min;
	// Maximum number in range
	protected int _max;
	
	@Override
	abstract public int guess();
	
	/**
	 * Listen to the result of some player's guess
	 * @param number Number that was guessed
	 * @param result Result of this guess
	 */
	public void listen(int number, GuessGame.Result result) {
		if(result == GuessGame.Result.HIGH) {
			_max = Math.min(_max, number);
		}
		else if(result == GuessGame.Result.LOW) {
			_min = Math.max(_min, number);
		}
	}
	
	/**
	 * Initialize this player values, that represents its knowledge on the current game state
	 */
	public void init() {
		_min = 0;
		_max = GuessGame.RANGE;
	}

}

// AIMediumPlayer.java

public class AIMediumPlayer extends AIPlayer {
	
	@Override
	public int guess() {
		return _min + (int) (Math.random() * (_max - _min));
	}

}

// AISmartPlayer.java

public class AISmartPlayer extends AIPlayer {


	@Override
	public int guess() {
		if((int) (Math.random()*2)==0)
		return _max-1;
		return _min+1;
	}

}

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

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