Nifty Uno

<< Employee | LabTrailIndex | Quicksort >>

by Stephen Davies -- University of Mary Washington

The Uno! assignment is intended to address this concern by introducing good-spirited competition within the class. Uno! is a simple but popular card game known to children (and adults) around the world. It involves players being dealt hands of cards and then playing them in a round-robin fashion, trying to be the first to "go out" (i.e., play all their cards.) The strategy required to play effectively is modest, but there are a surprising number of choices a player unconsciously makes as they play. (Should I match the up card's rank, or color? Should I play my wild card now or hold it? is the person after me ahead, or behind, and would it be wise to use a penalty card on them? What should I change the color to? etc.)

A Java simulator program (provided) can simulate thousands of consecutive Uno! games in seconds. It implements all the rules of the standard game, including the scoring, with only one thing missing: pluggable strategy methods for each player. Students have two weeks to formulate their strategies and write their code for choosing a (legal) card to play from a hand, and choosing which color to "call" if they play a wild. Each of these strategies (which is an implementation of a simple Java interface with two methods) is then "plugged in" to the simulator, which can simulate four (or any number) of players competing against each other for 50,000 (or any number of) straight games. A visual scoreboard displays the running totals as the game progresses. An in-class, bracket-style competition is suggested for the Friday after the programs are due, so that students can watch their programs compete against their classmates' and try to advance to the Uno Final Four.

davies-uno.zip

http://nifty.stanford.edu/2012/davies-uno/instructions.html

blueJ notes:

  1. Make a New Folder and Name it "Uno Project"
  2. Unzip the davies-uno.zip so the "uno" folder will be in the "Uno Project" folder
  3. Drag the files "players.text" and "draw4.jpg" from the "uno" folder to the "Uno Project" folder
  4. Start BlueJ, Use the "Open Non-BlueJ..." from the Project menu and select the "Uno Project" folder
  5. Compile everything in the "uno" folder
  6. To run the main method of text based "UnoSimulation" simulator use {"1" } to run 1 game. You can run the main method with the arguments {"100", "quiet"} too.
  7. To run the main method of the Graphical "GraphicalUnoSimulation " simulator try the arguments {"50000"} to play 50,000 games.
  8. Edit the "players.txt" file to change which to player strategies use.

The original assignment from Dr. Davies is here

You need to write your strategy in the play() and callColor() methods.

Card.java


package uno;

/**
 * <p>A Card in an Uno deck. Each Card knows its particular type, which is
 * comprised of a 3-tuple (color, rank, number). Not all of these values
 * are relevant for every particular type of card, however; for instance,
 * wild cards have no color (getColor() will return Color.NONE) or number
 * (getNumber() will return -1).</p>
 * <p>A Card knows its forfeit cost (<i>i.e.</i>, how many points it counts
 * against a loser who gets stuck with it) and how it should act during
 * game play (whether it permits the player to change the color, what
 * effect it has on the game state, etc.)</p>
 * @since 1.0
 */
public class Card {

    /**
     * For terminals that support it, setting PRINT_IN_COLOR to true will
     * annotate toString() calls with ANSI color codes. This is known to
     * work on Ubuntu Linux, and known not to work in DOS terminal and
     * NetBeans.
     */
    public static final boolean PRINT_IN_COLOR = false;

    private UnoPlayer.Color color;
    private UnoPlayer.Rank rank;
    private int number;

    /**
     * Constructor for non-number cards (skips, wilds, etc.)
     */
    public Card(UnoPlayer.Color color, UnoPlayer.Rank rank) {
        this.color = color;
        this.rank = rank;
        this.number = -1;
    }

    /**
     * Constructor for number cards.
     */
    public Card(UnoPlayer.Color color, int number) {
        this.color = color;
        this.rank = UnoPlayer.Rank.NUMBER;
        this.number = number;
    }

    /**
     * Constructor to explicitly set entire card state.
     */
    public Card(UnoPlayer.Color color, UnoPlayer.Rank rank, int number) {
        this.color = color;
        this.rank = rank;
        this.number = number;
    }

    /**
     * Render this Card object as a string. Whether the string comes out
     * with ANSI color codes is controlled by the PRINT_IN_COLOR static
     * class variable.
     */
    public String toString() {
        String retval = "";
        if (PRINT_IN_COLOR) {
            switch (color) {
                case RED:
                    retval += "\033[31m";
                    break;
                case YELLOW:
                    retval += "\033[33m";
                    break;
                case GREEN:
                    retval += "\033[32m";
                    break;
                case BLUE:
                    retval += "\033[34m";
                    break;
                case NONE:
                    retval += "\033[1m";
                    break;
            }
        }
        else {
            switch (color) {
                case RED:
                    retval += "R";
                    break;
                case YELLOW:
                    retval += "Y";
                    break;
                case GREEN:
                    retval += "G";
                    break;
                case BLUE:
                    retval += "B";
                    break;
                case NONE:
                    retval += "";
                    break;
            }
        }
        switch (rank) {
            case NUMBER:
                retval += number;
                break;
            case SKIP:
                retval += "S";
                break;
            case REVERSE:
                retval += "R";
                break;
            case WILD:
                retval += "W";
                break;
            case DRAW_TWO:
                retval += "+2";
                break;
            case WILD_D4:
                retval += "W4";
                break;
        }
        if (PRINT_IN_COLOR) {
            retval += "\033[37m\033[0m";
        }
        return retval;
    }

    /**
     * Returns the number of points this card will count against a player
     * who holds it in his/her hand when another player goes out.
     */
    public int forfeitCost() {
        if (rank == UnoPlayer.Rank.SKIP || rank == UnoPlayer.Rank.REVERSE ||
            rank == UnoPlayer.Rank.DRAW_TWO) {
            return 20;
        }
        if (rank == UnoPlayer.Rank.WILD || rank == UnoPlayer.Rank.WILD_D4) {
            return 50;
        }
        if (rank == UnoPlayer.Rank.NUMBER) {
            return number;
        }
        System.out.println("Illegal card!!");
        return -10000;
    }

    /**
     * Returns true only if this Card can legally be played on the up card
     * passed as an argument. The second argument is relevant only if the
     * up card is a wild.
     * @param c An "up card" upon which the current object might (or might
     * not) be a legal play.
     * @param calledColor If the up card is a wild card, this parameter
     * contains the color the player of that color called.
     */ 
    public boolean canPlayOn(Card c, UnoPlayer.Color calledColor) {
        if (rank == UnoPlayer.Rank.WILD ||
            rank == UnoPlayer.Rank.WILD_D4 ||
            color == c.color ||
            color == calledColor ||
            (rank == c.rank && rank != UnoPlayer.Rank.NUMBER) ||
            number == c.number && rank == UnoPlayer.Rank.NUMBER && c.rank == UnoPlayer.Rank.NUMBER)
        {
            return true;
        }
        return false;
    }

    /**
     * Returns true only if playing this Card object would result in the
     * player being asked for a color to call. (In the standard game, this
     * is true only for wild cards.)
     */
    public boolean followedByCall() {
        return rank == UnoPlayer.Rank.WILD || rank == UnoPlayer.Rank.WILD_D4;
    }

    /**
     * This method should be called immediately after a Card is played,
     * and will trigger the effect peculiar to that card. For most cards,
     * this merely advances play to the next player. Some special cards
     * have other effects that modify the game state. Examples include a
     * Skip, which will advance <i>twice</i> (past the next player), or a
     * Draw Two, which will cause the next player to have to draw cards.
     * @param game The Game being played, whose state may be modified by
     * this card's effect.
     * @throws EmptyDeckException Thrown only in very exceptional cases
     * when a player must draw as a result of this card's effect, yet the
     * draw cannot occur because of un-shufflable deck exhaustion.
     */
    void performCardEffect(Game game) throws EmptyDeckException {
        switch (rank) {
            case SKIP:
                game.advanceToNextPlayer();
                game.advanceToNextPlayer();
                break;
            case REVERSE:
                game.reverseDirection();
                game.advanceToNextPlayer();
                break;
            case DRAW_TWO:
                nextPlayerDraw(game);
                nextPlayerDraw(game);
                game.advanceToNextPlayer();
                game.advanceToNextPlayer();
                break;
            case WILD_D4:
                nextPlayerDraw(game);
                nextPlayerDraw(game);
                nextPlayerDraw(game);
                nextPlayerDraw(game);
                game.advanceToNextPlayer();
                game.advanceToNextPlayer();
                break;
            default:
                game.advanceToNextPlayer();
                break;
        }
    }

    private void nextPlayerDraw(Game game) throws EmptyDeckException {
        int nextPlayer = game.getNextPlayer();
        Card drawnCard;
        try {
            drawnCard = game.deck.draw();
        }
        catch (EmptyDeckException e) {
            game.print("...deck exhausted, remixing...");
            game.deck.remix();
            drawnCard = game.deck.draw();
        }
        game.h[nextPlayer].addCard(drawnCard);
        //game.println("  Player #" + nextPlayer + " draws " + drawnCard + ".");
        game.println("  " + game.h[nextPlayer].getPlayerName() + " draws " +
            drawnCard + ".");
    }

    /**
     * Returns the color of this card, which is Color.NONE in the case of
     * wild cards.
     */
    public UnoPlayer.Color getColor() {
        return color;
    }

    /**
     * Returns the rank of this card, which is Rank.NUMBER in the case of
     * number cards (calling getNumber() will retrieve the specific
     * number.)
     */
    public UnoPlayer.Rank getRank() {
        return rank;
    }

    /**
     * Returns the number of this card, which is guaranteed to be -1 for
     * non-number cards (cards of non-Rank.NUMBER rank.)
     */
    public int getNumber() {
        return number;
    }

}

GameState.java


package uno;

import java.util.List;

/**
 * <p>A GameState object provides programmatic access to certain (legal)
 * aspects of an Uno game, so that interested players can take advantage of
 * that information. Note that not all aspects of a game's state
 * (<i>e.g.</i>, the direction of play, whose turn it is next, the actual
 * cards in each player's hand (!), etc.) are reflected in the GameState
 * object -- only those for which it makes sense for a player to have
 * access.</p>
 * @since 2.0
 */
public class GameState {

    private Game theGame;
    private int[] numCardsInHandsOfUpcomingPlayers;
    private UnoPlayer.Color[] mostRecentColorCalledByUpcomingPlayers;
    private int[] totalScoreOfUpcomingPlayers;

    /**
     * (Blank constructor, used only during testing.)
     */
    GameState() {
        numCardsInHandsOfUpcomingPlayers = new int[4];
        mostRecentColorCalledByUpcomingPlayers = new UnoPlayer.Color[4];
        totalScoreOfUpcomingPlayers = new int[4];
    }

    /**
     * Instantiate a new GameState object whose job it is to provide safe
     * access to the Game object passed.
     */
    GameState(Game game) {

        numCardsInHandsOfUpcomingPlayers =
            new int[game.scoreboard.getNumPlayers()];
        mostRecentColorCalledByUpcomingPlayers =
            new UnoPlayer.Color[game.scoreboard.getNumPlayers()];
        totalScoreOfUpcomingPlayers =
            new int[game.scoreboard.getNumPlayers()];

        if (game.direction == Game.Direction.FORWARDS) {
            for (int i=0; i<game.h.length; i++) {
                numCardsInHandsOfUpcomingPlayers[i] =
                    game.h[(game.currPlayer + i + 1) %
                        game.scoreboard.getNumPlayers()].size();
                totalScoreOfUpcomingPlayers[i] =
                    game.scoreboard.getScore((game.currPlayer + i + 1) %
                        game.scoreboard.getNumPlayers());
                mostRecentColorCalledByUpcomingPlayers[i] =
                    game.mostRecentColorCalled[(game.currPlayer + i + 1) %
                        game.scoreboard.getNumPlayers()];
            }
        }
        else {
            for (int i=0; i<3; i++) { // FIXTHIS
                numCardsInHandsOfUpcomingPlayers[i] =
                    game.h[(game.currPlayer - i - 1 +
                        game.scoreboard.getNumPlayers()) %
                        game.scoreboard.getNumPlayers()].size();
                totalScoreOfUpcomingPlayers[i] =
                    game.scoreboard.getScore((game.currPlayer - i - 1 +
                        game.scoreboard.getNumPlayers()) %
                        game.scoreboard.getNumPlayers());
                mostRecentColorCalledByUpcomingPlayers[i] =
                    game.mostRecentColorCalled[(game.currPlayer - i - 1 +
                        game.scoreboard.getNumPlayers()) %
                        game.scoreboard.getNumPlayers()];
            }
        }
        theGame = game;
    }

    /**
     * Return an array of ints indicating the number of cards each player
     * has remaining. The array is ordered so that index 0 has the count
     * for the player who (barring action cards that might change it) will
     * play next, index 1 the player who (barring action cards) will play
     * second, etc.
     */
    public int[] getNumCardsInHandsOfUpcomingPlayers() {
        return numCardsInHandsOfUpcomingPlayers;
    }

    /**
     * Return an array of ints indicating the total overall score each
     * player has. The array is ordered so that index 0 has the count
     * for the player who (barring action cards that might change it) will
     * play next, index 1 the player who (barring action cards) will play
     * second, etc.
     */
    public int[] getTotalScoreOfUpcomingPlayers() {
        return numCardsInHandsOfUpcomingPlayers;
    }

    /**
     * Return the color most recently "called" (after playing a wild) by
     * each opponent. If a given opponent has not played a wild card this
     * game, the value will be Color.NONE. The array is ordered so that
     * index 0 has the count for the player who (barring action cards that
     * might change it) will play next, index 1 the player who (barring
     * action cards) will play second, etc.
     */
    public UnoPlayer.Color[] getMostRecentColorCalledByUpcomingPlayers() {
        return mostRecentColorCalledByUpcomingPlayers;
    }

    /**
     * Return a list of <i>all</i> cards that have been played since the
     * last time the deck was remixed. This allows players to "card count"
     * if they choose.
     */
    public List<Card> getPlayedCards() {
        if (theGame != null) {
            return theGame.deck.getDiscardedCards();
        }
        else {
            // testing only
            return new java.util.ArrayList<Card>();
        }
    }
}

UnoPlayer.java


package uno;

import java.util.List;

/**
 * <p>An interface that Uno-playing strategies implement in order to
 * compete in an Uno tournament. It consists of two methods which the
 * simulator calls each time a player's turn arises: play(), which chooses
 * a card from the hand according to some custom algorithm, and callColor()
 * (which is only called if the user chooses to play a wild) that asks the
 * player what color to "call."</p>
 * @since 1.0
 */
public interface UnoPlayer {

    public enum Color { RED, YELLOW, GREEN, BLUE, NONE }
    public enum Rank { NUMBER, SKIP, REVERSE, DRAW_TWO, WILD, WILD_D4 }

    /**
     * <p>This method is called when it's your turn and you need to
     * choose what card to play.</p>
     *
     * <p>The hand parameter tells you what's in your hand. You can call
     * getColor(), getRank(), and getNumber() on each of the cards it
     * contains to see what it is. The color will be the color of the card,
     * or "Color.NONE" if the card is a wild card. The rank will be
     * "Rank.NUMBER" for all numbered cards, and another value (e.g.,
     * "Rank.SKIP," "Rank.REVERSE," etc.) for special cards. The value of
     * a card's "number" only has meaning if it is a number card. 
     * (Otherwise, it will be -1.)</p>
     *
     * <p>The upCard parameter works the same way, and tells you what the 
     * up card (in the middle of the table) is.</p>
     *
     * <p>The calledColor parameter only has meaning if the up card is a
     * wild, and tells you what color the player who played that wild card
     * called.</p>
     *
     * <p>Finally, the state parameter is a GameState object on which you
     * can invoke methods if you choose to access certain detailed
     * information about the game (like who is currently ahead, what colors
     * each player has recently called, etc.)</p>
     *
     * <p>You must return a value from this method indicating which card you
     * wish to play. If you return a number 0 or greater, that means you
     * want to play the card at that index. If you return -1, that means
     * that you cannot play any of your cards (none of them are legal plays)
     * in which case you will be forced to draw a card (this will happen
     * automatically for you.)</p>
     */
    public int play(List<Card> hand, Card upCard, Color calledColor,
        GameState state);

    /**
     * <p>This method will be called when you have just played a
     * wild card, and is your way of specifying which color you want to 
     * change it to.</p>
     *
     * <p>You must return a valid Color value from this method. You must
     * not return the value Color.NONE under any circumstances.</p>
     */
    public Color callColor(List<Card> hand);

}

jsmith_UnoPlayer.java


package uno;

import java.util.List;

public class jsmith_UnoPlayer implements UnoPlayer {

    /**
     * play - This method is called when it's your turn and you need to
     * choose what card to play.
     *
     * The hand parameter tells you what's in your hand. You can call
     * getColor(), getRank(), and getNumber() on each of the cards it
     * contains to see what it is. The color will be the color of the card,
     * or "Color.NONE" if the card is a wild card. The rank will be
     * "Rank.NUMBER" for all numbered cards, and another value (e.g.,
     * "Rank.SKIP," "Rank.REVERSE," etc.) for special cards. The value of
     * a card's "number" only has meaning if it is a number card. 
     * (Otherwise, it will be -1.)
     *
     * The upCard parameter works the same way, and tells you what the 
     * up card (in the middle of the table) is.
     *
     * The calledColor parameter only has meaning if the up card is a wild,
     * and tells you what color the player who played that wild card called.
     *
     * Finally, the state parameter is a GameState object on which you can 
     * invoke methods if you choose to access certain detailed information
     * about the game (like who is currently ahead, what colors each player
     * has recently called, etc.)
     *
     * You must return a value from this method indicating which card you
     * wish to play. If you return a number 0 or greater, that means you
     * want to play the card at that index. If you return -1, that means
     * that you cannot play any of your cards (none of them are legal plays)
     * in which case you will be forced to draw a card (this will happen
     * automatically for you.)
     */
    public int play(List<Card> hand, Card upCard, Color calledColor,
        GameState state) {

        // THIS IS WHERE YOUR AMAZING CODE GOES
        return -1;
    }


    /**
     * callColor - This method will be called when you have just played a
     * wild card, and is your way of specifying which color you want to 
     * change it to.
     *
     * You must return a valid Color value from this method. You must not
     * return the value Color.NONE under any circumstances.
     */
    public Color callColor(List<Card> hand) {

        // THIS IS WHERE YOUR AMAZING CODE GOES
        return null;
    }

}

Main.java


package uno;

import java.util.List;

public class Main {

    public static void main(String[] args) {

        UnoPlayer jup = new jsmith_UnoPlayer();

        List<Card> hand = new java.util.ArrayList<Card>();
        hand.add(new Card(UnoPlayer.Color.RED, UnoPlayer.Rank.NUMBER, 4));
        hand.add(new Card(UnoPlayer.Color.GREEN, UnoPlayer.Rank.NUMBER, 7));
        hand.add(new Card(UnoPlayer.Color.GREEN, UnoPlayer.Rank.REVERSE, -1));
        hand.add(new Card(UnoPlayer.Color.BLUE, UnoPlayer.Rank.NUMBER, 2));
        hand.add(new Card(UnoPlayer.Color.BLUE, UnoPlayer.Rank.SKIP, -1));
        hand.add(new Card(UnoPlayer.Color.NONE, UnoPlayer.Rank.WILD, -1));

        int cardPlayed = jup.play(hand, 
            new Card(UnoPlayer.Color.RED, UnoPlayer.Rank.NUMBER, 7),
            UnoPlayer.Color.RED, new GameState());

        /* Let's see whether the card played was legit. */
        if (cardPlayed == -1) {
            System.out.println("Player did not think any card could be played." +
                "\nThis is an error, since cards 0, 1, and 5 are legal plays.");
        }
        else {
            if (cardPlayed == 0 || cardPlayed == 1 || cardPlayed == 5) {
                System.out.println("Player played " + hand.get(cardPlayed));
            }
            else {
                System.out.println("Player tried to play " + hand.get(cardPlayed) + ", which is an error.");
            }
        }
    }
}

TestCaseProcessor.java



package uno;

import java.io.*;
import java.util.*;

public class TestCaseProcessor {

    public String classname = "uno.jsmith_UnoPlayer";
    public String filename = "testCases.txt";

    private UnoPlayer thePlayer;

	public static void main(String args[]) {
		try {
            if (args.length != 1) {
                System.out.println("Usage: TestCaseProcessor UnoPlayerClassName.");
                System.exit(1);
            }
			new TestCaseProcessor(args[0]).doIt();
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}

	private TestCaseProcessor(String classname) throws Exception {
        this.classname = classname;
        thePlayer = (UnoPlayer) Class.forName("uno." + classname + "_UnoPlayer").newInstance();
	}

	private void doIt() throws Exception {
        int numHandsTested = 0;
        BufferedReader br = new BufferedReader(new FileReader(filename));
        String handLine = br.readLine();
        while (handLine != null) {
            Scanner handLineScanner = new Scanner(handLine).
                useDelimiter(",");
            ArrayList<Card> hand = new ArrayList<Card>();
            while (handLineScanner.hasNext()) {
                String cardString = handLineScanner.next();
                Scanner cardStringScanner = new Scanner(cardString);
                Card card = new Card(
                    UnoPlayer.Color.valueOf(cardStringScanner.next()),
                    UnoPlayer.Rank.valueOf(cardStringScanner.next()),
                    cardStringScanner.nextInt());
                hand.add(card);
            }

            String upCardLine = br.readLine();
            Scanner upCardLineScanner = new Scanner(upCardLine);
            Card upCard = new Card(
                UnoPlayer.Color.valueOf(upCardLineScanner.next()),
                UnoPlayer.Rank.valueOf(upCardLineScanner.next()),
                upCardLineScanner.nextInt());

            String calledColorLine = br.readLine();
            UnoPlayer.Color calledColor = UnoPlayer.Color.valueOf(calledColorLine);

            ArrayList<Integer> validPlays = new ArrayList<Integer>();
            String validPlaysLine = br.readLine();
            Scanner validPlaysScanner =
                new Scanner(validPlaysLine).useDelimiter(",");
            while (validPlaysScanner.hasNextInt()) {
                validPlays.add(new Integer(validPlaysScanner.nextInt()));
            }

            testHand(hand,upCard,calledColor,validPlays);
            numHandsTested++;

            if (numHandsTested < 100  ||  numHandsTested % 100 == 0) {
                System.out.println(numHandsTested + " test hands passed!");
            }

            br.readLine(); // consume --------- delimiter

            handLine = br.readLine();
        }
	}

    private void testHand(
        List<Card> hand, Card upCard, 
        UnoPlayer.Color calledColor,
        ArrayList<Integer> validPlays) throws Exception {

        int cardPlayed = 
            thePlayer.play(hand, upCard, calledColor, new GameState());

        if (!validPlays.contains(new Integer(cardPlayed))) {
            System.out.println("Whoops -- your play() method has an error!");
            System.out.println("You were given this hand:");
            for (int i=0; i<hand.size(); i++) {
                System.out.println("  " + i + ". " + hand.get(i));
            }

            System.out.println("and the up card was: " + upCard);
            if (upCard.getRank() == UnoPlayer.Rank.WILD ||
                upCard.getRank() == UnoPlayer.Rank.WILD_D4) {
                System.out.println("and the called color was: " + calledColor);
            }
            System.out.println("and you (wrongly) returned " + 
                cardPlayed + ".");
            System.out.print("Valid plays would have included: ");
            for (int i=0; i<validPlays.size(); i++) {
                System.out.print(validPlays.get(i));
                if (i<validPlays.size()-1) {
                    System.out.print(",");
                }
            }
            System.out.println();
            System.exit(3);
        }

        UnoPlayer.Color color = thePlayer.callColor(hand);

        if (color != UnoPlayer.Color.RED  &&  color != UnoPlayer.Color.BLUE  &&  color !=
            UnoPlayer.Color.GREEN  &&  color != UnoPlayer.Color.YELLOW) {

            System.out.println("Whoops -- your callColor() method has an error!");
            System.out.println("You were given this hand:");
            for (int i=0; i<hand.size(); i++) {
                System.out.println("  " + i + ". " + hand.get(i));
            }

            System.out.println("and the up card was: " + upCard);
            if (upCard.getRank() == UnoPlayer.Rank.WILD ||
                upCard.getRank() == UnoPlayer.Rank.WILD_D4) {
                System.out.println("and the called color was: " + calledColor);
            }
            System.out.println("and you (wrongly) returned " + color + ".");
            System.exit(4);
        }

    }
}

Attach:testCases.txt