Play a game of Yahtzee

18

0

In the game Yahtzee, players take turns rolling 5 6-sided dice up to three times per turn, possibly saving dice between rolls, and then selecting a category they wish to use for their roll. This continues until there are no more categories (which happens after 13 turns). Then, players' scores are tallied, and the player with the highest score wins.

The categories are as follows ("sum of dice" means adding up the number of pips on the specified dice):

  • Upper Section
    • Aces: sum of the dice showing 1 pip
    • Twos: sum of the dice showing 2 pips
    • Threes: sum of the dice showing 3 pips
    • Fours: sum of the dice showing 4 pips
    • Fives: sum of the dice showing 5 pips
    • Sixes: sum of the dice showing 6 pips
  • Lower Section
    • Three of a Kind: 3 dice with same value, score is sum of all dice
    • Four of a Kind: 4 dice with same value, score is sum of all dice
    • Full House: 3 dice with one value and 2 with another, score is 25
    • Small Straight: 4 sequential dice, score is 30
    • Large Straight: 5 sequential dice, score is 40
    • Yahtzee: all 5 dice with same value, score is 50
    • Chance: any combination of dice, score is sum of all dice

There are a few rules about the category choices:

  • If a player chooses a category that does not match their roll, they receive a score of 0 for that category.
  • If a player earns a score of at least 63 in the upper section, they receive 35 bonus points.
  • If a player has rolled a Yahtzee but the Yahtzee category is already taken (by another Yahtzee - filling in 0 for a miss doesn't count), they receive a bonus of 100 points. This bonus is awarded for every Yahtzee after the first.
    • Additionally, the player must still choose to fill in a category. They must choose the upper section category corresponding to their roll (e.g. a roll of 5 6's must be placed in the Sixes category). If the corresponding upper section category has already been used, the Yahtzee may be used for a lower section category (in this case, choosing Full House, Small Straight, or Large Straight awards the normal amount of points rather than 0). If all of the lower section categories are taken, then the Yahtzee may be applied to an unused upper section category, with a score of 0.

The Challenge

In this challenge, the competitors will play 1000 games of Yahtzee. At the end of each game, the submission(s) that scored the highest will receive 1 point. After all of the games are finished, the submission with the most points will win. If there is a tie, additional games will be played with only the tied submissions until the tie is broken.

Controller

The complete controller code can be found on this GitHub repository. Here are the public interfaces with which players will be interacting:

public interface ScorecardInterface {

    // returns an array of unused categories
    Category[] getFreeCategories();

    // returns the current total score
    int getScore();

    // returns the current Yahtzee bonus
    int getYahtzeeBonus();

    // returns the current Upper Section bonus
    int getUpperBonus();

    // returns the current Upper Section total
    int getUpperScore();

}
public interface ControllerInterface {

    // returns the player's scorecard (cloned copy, so don't try any funny business)
    ScorecardInterface getScoreCard(Player p);

    // returns the current scores for all players, in no particular order
    // this allows players to compare themselves with the competition,
    //  without allowing them to know exactly who has what score (besides their own score),
    //  which (hopefully) eliminates any avenues for collusion or sabotage
    int[] getScores();

}
public enum Category {
    ACES,
    TWOS,
    THREES,
    FOURS,
    FIVES,
    SIXES,
    THREE_OF_A_KIND,
    FOUR_OF_A_KIND,
    FULL_HOUSE,
    SMALL_STRAIGHT,
    LARGE_STRAIGHT,
    YAHTZEE,
    CHANCE;

    // determines if the category is part of the upper section
    public boolean isUpper() {
        // implementation
    }

    // determines if the category is part of the lower section
    public boolean isLower() {
        // implementation
    }

    // determines if a given set of dice fits for the category
    public boolean matches(int[] dice) {
        // implementation
    }

    // calculates the score of a set of dice for the category
    public int getScore(int[] dice) {
        // implementation
    }

    // returns all categories that fit the given dice
    public static Category[] getMatchingCategories(int[] dice) {
        // implementation
    }
}
public class TurnChoice {

    // save the dice with the specified indexes (0-4 inclusive)
    public TurnChoice(int[] diceIndexes) {
        // implementation
    }

    // use the current dice for specified category
    public TurnChoice(Category categoryChosen) {
        // implementation
    }

}

public abstract class Player {

    protected ControllerInterface game;

    public Player(ControllerInterface game) {
        this.game = game;
    }

    public String getName() {
        return this.getClass().getSimpleName();
    }

    // to be implemented by players
    // dice is the current roll (an array of 5 integers in 1-6 inclusive)
    // stage is the current roll stage in the turn (0-2 inclusive)
    public abstract TurnChoice turn(int[] dice, int stage);

}

Additionally, there are some utility methods in Util.java. They are mainly there to simplify the controller code, but they can be used by players if they desire.

Rules

  • Players are not allowed to interact in any way except using the Scorecard.getScores method to see the current scores of all players. This includes colluding with other players or sabotaging other players via manipulating parts of the system that are not part of the public interface.
  • If a player makes an illegal move, they will not be allowed to compete in the tournament. Any issues that cause illegal moves must be resolved prior to the running of the tournament.
  • If additional submissions are made after the tournament is run, a new tournament will be run with the new submission(s), and the winning submission will be updated accordingly. I make no guarantee of promptness in running the new tournament, however.
  • Submissions may not exploit any bugs in the controller code that cause it to deviate from the actual game rules. Point out bugs to me (in a comment and/or in a GitHub issue), and I'll fix them.
  • Use of Java's reflection tools is forbidden.
  • Any language which runs on the JVM, or can be compiled to Java or JVM bytecode (such as Scala or Jython) can be used, so long as you supply any additional code needed to interface it with Java.

Final Comments

If there is any utility method you would like me to add to the controller, simply ask in the comments and/or make an issue on GitHub, and I'll add it, assuming it doesn't allow for rule breaking or expose information to which players are not privy. If you want to write it yourself and create a pull request on GitHub, even better!

Mego

Posted 2016-10-17T19:12:56.267

Reputation: 32 998

ACES? You mean ONES? These are dice, not cards. – mbomb007 – 2016-10-17T19:23:01.363

@mbomb007 Yahtzee scorecards call it aces.

– Mego – 2016-10-17T19:25:04.180

I don't recall seeing it called that when I've played it, but okay. – mbomb007 – 2016-10-17T19:25:58.093

Is there a method to get the score for a given category given a set of dice? – mbomb007 – 2016-10-17T19:43:53.353

@mbomb007 No, but I certainly can make one :) – Mego – 2016-10-17T19:44:35.283

Does this challenge support python? If so, how? – Magenta – 2016-10-17T20:04:55.040

@Magenta This challenge is primarily Java, but you are welcome to use Python (by way of Jython) if you write a wrapper to use the Jython from within Java.

– Mego – 2016-10-17T20:06:10.310

@Mego the koth challenges I have competed in are written in java, and can be accessed by native python. However, some use argv[0] as the I/O, and some use the python input(). I was wondering which one this uses, as I do not have the knowledge to write anything in Java. – Magenta – 2016-10-17T20:09:44.867

@Magenta This KotH challenge uses method calls for communication between the controller and the players. As such, you will need to use a language that runs on the JVM. – Mego – 2016-10-17T20:20:39.150

@Mego are we allowed to write a wrapper submission which implements the player class by calling another process? (That's how non-JVM languages were able to compete in the Wolf KotH.) – Martin Ender – 2016-10-17T20:39:11.200

@MartinEnder Sure, as long as the interface is callable from Java and doesn't break any of the rules. – Mego – 2016-10-17T20:40:11.260

@TNT You are absolutely correct. I will fix that now. – Mego – 2016-10-23T01:14:10.967

Part of the controller code is giving me issues again. I created a chat room for it (Did you get a notification? It's my first time using SE chat) since my description was a little lengthy for a comment. I'm not sure if I'm just confused about something or if it's another bug.

– TNT – 2016-10-31T03:53:27.627

Nice to see a post in PPCG that's not so focused on byte count. – James Murphy – 2016-11-02T02:45:26.427

Answers

4

DummyPlayer

package mego.yahtzee;
import java.util.Random;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class DummyPlayer extends Player {

    public DummyPlayer(ControllerInterface game) {
        super(game);
    }

    @Override
    public TurnChoice turn(int[] dice, int stage) {
        Category[] choices = game.getScoreCard(this).getFreeCategories();
        Category choice = choices[new Random().nextInt(choices.length)];
        if(IntStream.of(dice).allMatch(die -> die == dice[0])) {
            if(Stream.of(choices).filter(c -> c == Category.YAHTZEE).count() > 0) {
                choice = Category.YAHTZEE;
            } else if(Stream.of(choices).filter(c -> c == Util.intToUpperCategory(dice[0])).count() > 0) {
                choice = Util.intToUpperCategory(dice[0]);
            } else {
                choices = Stream.of(game.getScoreCard(this).getFreeCategories()).filter(c -> c.isLower()).toArray(Category[]::new);
                if(choices.length > 0) {
                    choice = choices[new Random().nextInt(choices.length)];
                } else {
                    choices = game.getScoreCard(this).getFreeCategories();
                    choice = choices[new Random().nextInt(choices.length)];
                }
            }
        }
        return new TurnChoice(choice);
    }

}

This player is here to serve as a basic outline for how to use the tools present in the Yahtzee controller. It chooses Yahtzee whenever possible, and makes random choices otherwise, while complying with the strict joker rules.

Mego

Posted 2016-10-17T19:12:56.267

Reputation: 32 998

1

Aces and Eights

Well, this took a lot longer than I would have liked thanks to how busy I've been lately.

package mego.yahtzee;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import static mego.yahtzee.Category.*;

public class AcesAndEights extends Player {
    private Category[] freeCategories, matchingCategories, usableCategories;

    public AcesAndEights(ControllerInterface game) {
        super(game);
    }

    @Override
    public TurnChoice turn(int[] dice, int stage) {
        List<Integer> holdIndices = new java.util.ArrayList<>();

        freeCategories = game.getScoreCard(this).getFreeCategories();

        matchingCategories = Category.getMatchingCategories(dice);
        Arrays.sort(matchingCategories);

        usableCategories = Arrays.stream(freeCategories)
                                 .filter(this::isMatchingCategory)
                                 .toArray(Category[]::new);
        Arrays.sort(usableCategories);

        if (isMatchingCategory(YAHTZEE))
            return doYahtzeeProcess(dice);

        if (isUsableCategory(FULL_HOUSE))
            return new TurnChoice(FULL_HOUSE);

        if (stage == 0 || stage == 1) {
            if (isMatchingCategory(THREE_OF_A_KIND)) {
                int num = 0;
                for (int i : dice) {
                    if (Util.count(Util.boxIntArray(dice), i) >= 3) {
                        num = i;
                        break;
                    }
                }
                for (int k = 0; k < 5; k++) {
                    if (dice[k] == num)
                        holdIndices.add(k);
                }
                return new TurnChoice(toIntArray(holdIndices.toArray(new Integer[0])));
            }

            if (isFreeCategory(LARGE_STRAIGHT) || isFreeCategory(SMALL_STRAIGHT)) {
                if (isUsableCategory(LARGE_STRAIGHT))
                    return new TurnChoice(LARGE_STRAIGHT);

                if (isMatchingCategory(SMALL_STRAIGHT)) {
                    if (!isFreeCategory(LARGE_STRAIGHT))
                        return new TurnChoice(SMALL_STRAIGHT);

                    int[] arr = Arrays.stream(Arrays.copyOf(dice, 5))
                                      .distinct()
                                      .sorted()
                                      .toArray();
                    List<Integer> l = Arrays.asList(Util.boxIntArray(dice));
                    if (Arrays.binarySearch(arr, 1) >= 0 && Arrays.binarySearch(arr, 2) >= 0) {
                        holdIndices.add(l.indexOf(1));
                        holdIndices.add(l.indexOf(2));
                        holdIndices.add(l.indexOf(3));
                        holdIndices.add(l.indexOf(4));
                    }
                    else if (Arrays.binarySearch(arr, 2) >= 0 && Arrays.binarySearch(arr, 3) >= 0) {
                        holdIndices.add(l.indexOf(2));
                        holdIndices.add(l.indexOf(3));
                        holdIndices.add(l.indexOf(4));
                        holdIndices.add(l.indexOf(5));
                    }
                    else {
                        holdIndices.add(l.indexOf(3));
                        holdIndices.add(l.indexOf(4));
                        holdIndices.add(l.indexOf(5));
                        holdIndices.add(l.indexOf(6));
                    }
                    return new TurnChoice(toIntArray(holdIndices.toArray(new Integer[0])));
                }
            }

            if (isFreeCategory(FULL_HOUSE)) {
                int o = 0, t = o;
                for (int k = 1; k <= 6; k++) {
                    if (Util.count(Util.boxIntArray(dice), k) == 2) {
                        if (o < 1)
                            o = k;
                        else
                            t = k;
                    }
                }

                if (o > 0 && t > 0) {
                    for (int k = 0; k < 5; k++) {
                        if (dice[k] == o || dice[k] == t)
                            holdIndices.add(k);
                    }
                    return new TurnChoice(toIntArray(holdIndices.toArray(new Integer[0])));
                }
            }
        }
        else {
            Arrays.sort(freeCategories, Comparator.comparingInt((Category c) -> c.getScore(dice))
                                                  .thenComparingInt(this::getPriority)
                                                  .reversed());
            return new TurnChoice(freeCategories[0]);
        }

        return new TurnChoice(new int[0]);
    }

    private TurnChoice doYahtzeeProcess(int[] dice) {
        if (isUsableCategory(YAHTZEE))
            return new TurnChoice(YAHTZEE);

        Category c = Util.intToUpperCategory(dice[0]);
        if (isUsableCategory(c))
            return new TurnChoice(c);

        Category[] arr = Arrays.stream(freeCategories)
                               .filter(x -> x.isLower())
                               .sorted(Comparator.comparing(this::getPriority)
                                                 .reversed())
                               .toArray(Category[]::new);
        if (arr.length > 0)
            return new TurnChoice(arr[0]);

        Arrays.sort(freeCategories, Comparator.comparingInt(this::getPriority));
        return new TurnChoice(freeCategories[0]);
    }

    private boolean isFreeCategory(Category c) {
        return Arrays.binarySearch(freeCategories, c) >= 0;
    }

    private boolean isMatchingCategory(Category c) {
        return Arrays.binarySearch(matchingCategories, c) >= 0;
    }

    private boolean isUsableCategory(Category c) {
        return Arrays.binarySearch(usableCategories, c) >= 0;
    }

    private int getPriority(Category c) {
        switch (c) {
            case YAHTZEE: return -3;        // 50 points
            case LARGE_STRAIGHT: return -1; // 40 points
            case SMALL_STRAIGHT: return -2; // 30 points
            case FULL_HOUSE: return 10;     // 25 points
            case FOUR_OF_A_KIND: return 9;  // sum
            case THREE_OF_A_KIND: return 8; // sum
            case SIXES: return 7;
            case FIVES: return 6;
            case FOURS: return 5;
            case THREES: return 4;
            case TWOS: return 3;
            case ACES: return 2;
            case CHANCE: return 1;          // sum
        }
        throw new RuntimeException();
    }

    private int[] toIntArray(Integer[] arr) {
        int[] a = new int[arr.length];
        for (int k = 0; k < a.length; k++)
            a[k] = arr[k];
        return a;
    }
}

The bot looks for patterns in the dice that could match certain categories and holds the necessary ones. It immediately chooses a high-priority matching category if one's found; otherwise it chooses a category that yields the highest score. Scores nearly 200 points per game on average.

TNT

Posted 2016-10-17T19:12:56.267

Reputation: 2 442