October 1998 Programmer's Challenge
Hearts
Mail solutions to:
progchallenge@mactech.com
Due Date EXTENDED: 11:59pm ET, Thursday, 8 October 1998
A number of past Challenges have been based on games where one player competed against another player. This month the Challenge is based on the four-handed game of Hearts. Hearts is a card game where one attempts to avoid taking tricks containing hearts or the queen of spades.
The prototype for the code you should write is:
#if defined(__cplusplus)
extern "C" {
#endif
#pragma enumsalwaysint on
typedef enum {kNoSuit=0, kSpade, kHeart, kDiamond, kClub} Suit;
typedef enum {kNoSpot=0, k2, k3, k4, k5, k6, k7, k8, k9, k10,
kJack, kQueen, kKing, kAce} Spot;
typedef enum {kPassLeft=0, kPassRight, kPassAcross, kNoPass} Pass;
typedef struct Card {
Suit suit;
Spot spot;
} Card;
pascal void InitTournament(
const UInt16 numPlayers,
/* number of players in the tournament, indexed by seat */
const UInt16 gameEndingScore
/* game is over when one player reaches this score */
);
pascal void InitGame(
const Uint32 playerID[4], /* Identifier for players in this hand, indexed by seat */
const UInt16 yourSeat /* your seat at the table, 0..3 */
);
pascal void SelectPass(
const Card dealtHand[13], /* cards you are dealt */
const Pass passDirection,
/* direction you are passing, rotates kPassLeft, kPassRight, kPassAcross, kNoPass */
UInt16 passedCards[3] /* index in dealtHand of cards you choose to pass */
);
pascal void PlayTrick(
const UInt16 trickNumber, /* 13 tricks per hand, 0..12 */
const UInt16 trickLeader, /* which player leads this trick */
const Card yourHand[13], /* your cards at the beginning of this trick */
const Card cardsPlayed[4], /* cards already played this trick, indexed by seat */
/* entries from [trickLeader] to [yourSeat-1 (mod 4)] are valid */
UInt16 *yourPlay /* index into yourHand of the card you choose to play */
);
pascal void TrickResults(
const Card lastTrick[4], /* cards played on previous trick, indexed by seat */
const UInt16 trickWinner /* which player won this trick */
);
pascal void HandResults(
const SInt16 pointsThisHand[4],
/* points earned by each player this hand, -26 .. 25 */
const SInt32 cumPoints[4] /* cumulative points earned by each player this game */
/* both pointsThisHand and cumPoints are indexed by seat number */
/* your points are pointsThisHand[yourSeat] */
);
#if defined(__cplusplus)
}
#endif
At the beginning of the tournament, your InitTournament routine will be called with the number of participating players (numPlayers) and the gameEndingScore. Before each game, your InitGame routine will be called to provide you with the playerID identifiers of the players involved in this game and your position at the table (yourSeat) relative to the other players. A game consists of a sequence of hands, each of which consists of 13 tricks. At the end of a hand, each player is awarded points based on the cards in the tricks he has taken, one point for each heart taken and 13 points for the queen of spades, with the objective being to take the fewest points. A player who captures all the hearts and the queen of spades, however, is said to "shoot the moon" and receives —26 points. At the end of each hand, your HandResults routine will be called to confirm the number of points received by each player (pointsThisHand) and the cumulative number of points earned by each player during this game (cumPoints).
At the beginning of a hand, each player is dealt 13 cards (dealtHand) and selects three cards to pass to another player. The passDirection rotates from passing left (kPassLeft, where player [i] passes to player [i+1], mod 4), to passing right (kPassRight, player [i] passes to player [i-1]), to passing across (kPassAcross, player [i] passes to player [i+2]), to not passing (kNoPass). Your SelectPass routine is called at the beginning of each hand with your dealtHand and the passDirection; you select the cards to be passed and return in passedCards the index in dealtHand of the cards you want to pass.
Each trick, your PlayTrick and TrickResults routines will be called. PlayTrick provides you with the seat index of the player leading this trick, the cards in your hand at the beginning of this trick, and the cards already played on this trick. On the first trick, yourHand will be the same as dealtHand, except that the cards you passed will be replaced by the cards that were passed to you. On subsequent tricks, yourHand will be unchanged except to remove the card you played on the previous trick (by changing the suit and spot fields to kNoSuit and kNoSpot, respectively). When PlayTrick is called, you select the card you wish to play and return in yourPlay its index in yourHand. After all four players have played, your TrickResults routine will be called to identify the winner of the trick (trickWinner) and all of the cards played on the lastTrick.
The two of clubs is led on the first trick of each hand — the test code will ensure that the PlayTrick routine of the player who has the two of clubs is called first on that trick. The player who won the previous trick leads the next trick. Hearts may not be led until a point card has been played on an earlier trick, unless a player has only hearts left in his hand. The queen of spades may be led at any time.
If the number of entries is reasonable, the tournament will have each player compete against every combination of other players in all possible seat arrangements. If the number of entries is large, the best four entries will be selected in some fair way, after which those entries will compete against one another in a final tournament. If fewer than four entries are submitted, I will round out the table with a simple-minded player that tries to avoid taking any points.
The winner will be the solution that wins the tournament by achieving the lowest score. The score will be the sum of the tournament points earned and a time penalty of one point per millisecond of execution time. The execution time of all of your routines (InitTournament, InitGame, SelectPass, PlayTrick, TrickResults, and HandResults) will count toward the time penalty.
This will be a native PowerPC Challenge, using the latest CodeWarrior environment. Solutions may be coded in C, C++, or Pascal. Thanks to Jim Lloyd for suggesting this Challenge.
Here are answers to a few questions asked about this Challenge on the Programmer's Challenge mailing list:
Q1:
In case some tournament players play in games that take a lot of hands
(and others play in games that take much fewer), are you going to normalize
the time taken? Say like seconds per hand (or trick)? It'd be a shame to
penalize players whose programs shoot the moon and--by lengthening the
game--incur more time penalty.
This could also be alleviated by the alternate scoring for "shoot the
moon": every opponent adds 26 points (instead of the moon-shooter removing
26). But "every opponent" means "every opponent in the current game" so it
might skew those players' results versus the other tournament players not
in the current game. Or that might be considered a fair penalty: letting
someone shoot the moon really costs you in the tournament.
... and ...
There is a small, but a finite possibility that "gameEndingScore"
(a positive number) is not reached for a very long time because every game
results in Shooting the Moon with -26 points, which eventually will wrap
around at -32768. At that point the big winner with all those successful moon
shots will be severely penalized when his negative score becomes positive.
According to some rules, moon shots result in the three opponents each getting
26 points. This would avoid the overflow problem, but makes a moon shot of
course three times as valuable.
A1: It's an interesting idea, but I've decided not to change the way the
scoring is done. I get complaints when I make a change like this to the
original published problem via the mailing list. As I said in the problem
statement, the time penalty is intended only to keep execution time
reasonable - for programs that run in reasonable time, it shouldn't be a
major factor.
Q2:
Is there a way to determine how many points in the tournament your
current-game opponents have? Each player seems only to be told the points
of his opponents in games he plays.
A2: No, there isn't, although the playerID let's you aggregate points from
the games you play in. I can see where having the tournament points would
be useful, but it isn't worth the wrath of the list if I add a parameter to
InitGame this late in the game.
After some correspondence, the questioner agreed with this decision:
"You can just explain to any others like me that the tournament is
conducted in seclusion. No talking to other players, no scores posted.
You are lead into the game room, see (and, over time, recognize) your
opponents for this game, play the hands, get the results, and are led out
of the room and back to your cell before the next game starts. In this
martial-arts-death-match-movie-like scenario you are lucky to be told the
total number of players!"
Q3:
Are the points recorded in HandResults() just game points or do they
include the time penalty points?
A3: Just game points.
Q4:
Does "point cards" include the queen of spades or just hearts? I.e. is
it OK to lead hearts once the queen of spades has been played?
The various authorities do not agree on this.
A4: Yes. In my misspent youth, leading hearts was allowed once the queen
was played, and I meant to allow this by saying "point card" instead of
"heart".
Q5:
Since:
>Hearts may not be led until a point card has been
>played on an earlier trick, unless a player has only hearts left in his
>hand.
and:
>The queen of spades may be led at any time.
One is left with the conclusion that if one has the lead and one's
cards are all Hearts plus the Queen of Spades, one's only play is
to lead the Queen of Spades.
Is this correct? There is often a house rule for this situation
basically making the Queen of Spades a Heart (only in this instance)
so any card may be lead, since one essentially has only 'Hearts'.
A5: No, what I meant is that if you have only point cards (hearts or Queen
of Spades), one may lead any point card.
Q6:
I guess it is legal to discard hearts on the first trick? ( another
variant among the authorities.)
A6: I remember this both ways. My problem statement does not prohibit
bleeding on the first trick, so I will allow it.
Q7:
Just curiosity: Why the "pascal" which is not portable and not compatible
with "ANSI keywords only"? (also, there are a few typos e.g. UInt32 for
UInt32)
A7: Occasionally I am criticized for not allowing Pascal solutions. Lately
I have been allowing them in the text but not including the Pascal calling
convention in the problem statement. Actually, I think it doesn't matter
with the PPC calling conventions, but I've included the "pascal" keyword
anyway. (And sorry about the Uint typos.)
Test code for this Challenge is now available.
You can get a head start on the Challenge by reading the
Programmer's Challenge mailing list. It will be posted to the list on
or before the 12th of the preceding month. To join, send an email to
listserv@listmail.xplain.com
with the subject "subscribe challenge-A". You can also join the
discussion list by sending a message with the subject "subscribe
challenge-D".
|