Feb 98 Challenge
Volume Number: 14 (1998)
Issue Number: 2
Column Tag: Programmer's Challenge
Programmer's Challenge
by Bob Boonstra, Westford, MA
Image Decoder
This month's Challenge is much easier to state than it will be to implement. Your task this month is to write a decoder for Graphics Interchange Format images. GIF is a data stream-oriented file format used to define the transmission protocol of LZW-encoded bitmap data. Your code will read a file containing a GIF and draw the image in an offscreen graphics world. There are two versions of the GIF specification; this Challenge includes only the earlier and more common GIF87a format, not the GIF89a format. The format description is too long to include here, but you can find the GIF87a specification at:
ftp://ftp.ncsa.uiuc.edu/misc/file.formats/graphics.formats/gif87a.doc
Other useful information on graphics formats in general is available at:
http://www.cis.ohio-state.edu/hypertext/faq/usenet/graphics/fileformats-faq/top.html
The prototype for the code you should write is:
#include <QDOffscreen.h>
#include <stdio.h>
GWorldPtr ReadImage(FILE *inputFile);
The inputFile will have been opened for you in binary mode by the calling routine. ReadImage should read and parse the image, create a GWorld with the appropriate color table, draw the image in the GWorld, and return a GWorld pointer to the calling routine. The solution that correctly decodes a variety of images in the least amount of execution time will be the winner.
I thought for some time about the Compuserve / Unisys / LZW patent controverty before selecting this topic for the Challenge. The LZW algorithm used in GIF compression is patented by Unisys, and Unisys requires that any commercial software product that uses this algorithm, including software that reads GIF images, requires a license from Unisys. Freely distributed software does not require a license. Since the Challenge doesn't result in a software product, much less a commercial product, I decided to go ahead with this exploration of decoding efficiency.
This will be a native PowerPC Challenge, using the latest CodeWarrior environment. Solutions must be coded in C, C++, or Pascal. This Challenge was suggested by Peter Lewis, who earns 2 Challenge points for the suggestion.
Three Months Ago Winner
Congratulations to Jeff Mallett (Boulder Creek, CA) for submitting the winning entry to the Pente(tm) Challenge. The objective was to accumulate, in minimum execution time, the most points in a round-robin tournament of the board game Pente(tm). With only three entries, the tournament required only six games for each player to compete against each other player twice, once playing first and once playing second. Both the winning entry and the third-place entry by Randy Boring employed an alpha-beta search technique to find the best move, and both entries were successful in winning most of their games. Jeff's entry, however, was more efficient about pruning the search tree and therefore required significantly less execution time than Randy's entry. Since a point was deducted for each second of execution time, the winning entry lost significantly fewer points for inefficiency. In addition, the scoring algorithm in Jeff's AddStone routine takes advantage of all three methods of scoring points: captures, winning by placing 5 stones in a row, and leaving sequences of 4-in-a-row on the board at the conclusion of a game. His solution earned more points as a result, even though it won the same number of games as Randy's.
The table below lists the number of tournament wins, points earned, cumulative score, code size, data size, and programming language for each entry. The number in parentheses after the entrant's name is the total number of Challenge points earned in all Challenges to date prior to this one.
Name Wins Points Score Code Data Language
Jeff Mallett (74) 3 33 32.03 4872 7923 C
Sebastian Maurer 0 10 9.97 4860 208 C++
Randy Boring (66) 3 23 -374.90 7672 2649 C
Top 20 Contestants
Here are the Top Contestants for the Programmer's Challenge. The numbers below include points awarded over the 24 most recent contests, including points earned by this month's entrants. The scores have been adjusted to correct an error made in assigning points for the Stratego Challenge printed in the November issue.
Rank Name Points
1. Munter, Ernst 200
2. Boring, Randy 73
3. Cooper, Greg 61
4. Lewis, Peter 59
5. Mallett, Jeff 50
6. Nicolle, Ludovic 48
7. Murphy, ACC 34
8. Gregg, Xan 33
9. Antoniewicz, Andy 24
10. Day, Mark 20
11. Higgins, Charles 20
12. Larsson, Gustav 20
13. Studer, Thomas 20
14. Gundrum, Eric 15
15. Hart, Alan 14
16. O'Connor, Turlough 14
17. Picao, Miguel Cruz 14
18. Saxton, Tom 12
19. Cutts, Kevin 11
20. 8-way tie 10
There are three ways to earn points: (1) scoring in the top 5 of any Challenge, (2) being the first person to find a bug in a published winning solution or, (3) being the first person to suggest a Challenge that I use. The points you can win are:
1st place 20 points
2nd place 10 points
3rd place 7 points
4th place 4 points
5th place 2 points
finding bug 2 points
suggesting Challenge 2 points
Here is Jeff's winning solution:
MyPente.c
Copyright © 1997 Jeff Mallett
//
// Do a depth=1 search to sort moves. Then do a higher
// depth alpha-beta search to select the best move.
//
// Plays for score rather than to win. For example,
// doesn't try to win by getting 5 captures since
// there is no score incentive for this. It could be
// retuned to play for a win instead.
#include "Pente.h"
#define SEARCH_DEPTH 4
enum {
_FRIEND = 0x0001,
_ENEMY = 0x0002,
_EMPTY = 0x0004,
_WALL = 0x0008
};
#define BORDER 1
#define BORDERS 2
#define INFINITY 32600
#define NA 0
#define MAX_CELLS (31 + BORDERS) * (31 + BORDERS)
#define ESTIMATE_PLUS 1
#define WIN 2000
#define CAPTURE_SCORE 240
#define VULNERABLE 150
static short CHAIN_SCORE2[3][3] = { //[color][open]
{ NA, NA, NA },
{ -1, -9, -2 }, // FRIEND
{ 1, 9, 2 } // ENEMY
};
static short CHAIN_SCORE3[3][3] = { //[color][open]
{ NA, NA, NA },
{ 3, 16, 40 }, // FRIEND
{ -2,-10,-30 } // ENEMY
};
static short CHAIN_SCORE4[3][3] = { //[color][open]
{ NA, NA, NA },
{ 230, 240, 250 }, // FRIEND
{ -150,-155,-160 } // ENEMY
};
static short CHAIN_SCORE5[3] = { //[color]
NA, WIN, -WIN
};
// 1 2 3 4
static short BLOCK_SCORE[5] = { NA, 0, 14, 14, 22};
static short THREATS[] = {
NA, NA,
25, // 2: tria
30, // 3: half-open four
350, // 4: double trias
370, // 5: tria + half-open four
450, // 6: tessera or 2 half-open fours
500, 500, 500, 500, 500, 500, 500, 500, 500, 500
};
static short gPreScore[MAX_CELLS], gEstimates[MAX_CELLS];
static short *gEstimatesStart, *gEstimatesEnd;
static short gBoard[MAX_CELLS], *gBoardStart, *gBoardEnd;
static short *gFirstStone, *gLastStone;
static short *gKillers[SEARCH_DEPTH], gPreviousThreats;
static short gDirections[8], *gDirectionsEnd;
static short gMoveNum, gScore, gStartDepth;
static short gBoardHalfSize, gSideLength, gBoardMax;
static short gCumCapturesFriend, gCumCapturesEnemy;
static short gAdjustment, gSE;
#define ADJUST(a) ( (a) + gAdjustment )
#define TRANSLATE(x, y) \
(gSideLength * ADJUST(y) + ADJUST(x))
#define GET_INDEX(p) ((p) - gBoard)
#define GET_X(i) ( ((i) % gSideLength) - gAdjustment )
#define GET_Y(i) ( ((i) / gSideLength) - gAdjustment )
#define UPDATE_ENDPOINTS(pSq, a, b) \
if (pSq < a) a = pSq; \
if (pSq > b) b = pSq
#define OPPONENT(side) (3 - (side))
#define OCCUPIED(x) ((x) & (_FRIEND | _ENEMY))
#define EMPTY(x) ((x) == _EMPTY)
// gChanges -- Array of unsigned longs containing data to
// undo moves
// list of:
// <pointer to position> <old position value>
// terminated by a OL
static unsigned long gChanges[256], *gChangesEnd;
#define PUSH(x) *(gChangesEnd++) = (x)
#define START_SAVE PUSH(0L)
#define PUSH_SQ(pSq) \
{ PUSH((long)*(pSq)); PUSH((unsigned long)(pSq)); }
#define POP *(--gChangesEnd)
#define TOP *gChangesEnd
static short * ChooseNextMove();
static short AddStone(short alpha, short beta, short *pSq,
short color, short depth,
short capturesFriend, short capturesEnemy,
short *firstStone, short *lastStone);
static long MyFindCaptures(Capture capture[], short *pSq,
short us, short them);
static Boolean MyFindFive(short *pSq, short color);
InitPente
void InitPente(
long boardHalfSize /* e.g., 9 for a 19x19 board */
/* all coordinates between -boardHalfSize
and +boardHalfSize */
) {
short i, j, *pSq;
gBoardHalfSize = boardHalfSize;
gSideLength = boardHalfSize * 2 + 1 + BORDERS;
gSE = gSideLength + 1;
gDirections[0] = -gSE; // NW
gDirections[1] = -gSideLength; // N
gDirections[2] = -gSideLength + 1; // NE
gDirections[3] = -1; // W
gDirections[4] = gSE; // SE
gDirections[5] = gSideLength; // S
gDirections[6] = gSideLength - 1; // SW
gDirections[7] = 1; // E
gDirectionsEnd = &gDirections[8];
gBoardMax = gSideLength * gSideLength;
i = (gSideLength + 1) * BORDER;
gBoardStart = &gBoard[i];
gBoardEnd = &gBoard[gBoardMax - i];
gEstimatesStart = &gEstimates[i];
gEstimatesEnd = &gEstimates[gBoardMax - i];
gFirstStone = gBoardEnd;
gLastStone = gBoardStart;
pSq = gBoard;
do {
*pSq = _WALL;
} while (++pSq != gBoardStart);
do {
*pSq = _EMPTY;
} while (++pSq != gBoardEnd);
do {
*pSq = _WALL;
} while (++pSq < &gBoard[gBoardMax]);
for (i = BORDER + boardHalfSize * 2 + 1;
i<gBoardMax-gSideLength; i += gSideLength)
for (j=0; j<BORDERS; ++j)
gBoard[i+j] = _WALL;
gCumCapturesFriend = gCumCapturesEnemy = gMoveNum = 0;
gAdjustment = gBoardHalfSize + BORDER;
gStartDepth = SEARCH_DEPTH - 1;
}
Pente
void Pente(
Point opponentsMove, /* your opponent moved here */
Boolean playingFirst, /* ignore opponentMove */
Point *yourMove, /* return your move here */
Capture claimCaptures[], /* return captured pairs here */
long *numCaptures, /* return # of claimCaptures */
Boolean *claimVictory /* return true if you claim
victory with this move */
) {
Point move;
Capture opponentCaptures[8];
short *pSq, *pNewSq, *d;
short score, bestScore, i;
short *pBestMove;
*numCaptures = 0;
*claimVictory = false;
if (playingFirst) { // *** MOVE 1
move.h = move.v = 0;
pSq = &gBoard[TRANSLATE(0, 0)];
*pSq = _FRIEND;
UPDATE_ENDPOINTS(pSq, gFirstStone, gLastStone);
*yourMove = move;
gMoveNum = 1;
return;
}
i = TRANSLATE(opponentsMove.h, opponentsMove.v);
pSq = &gBoard[i];
*pSq = _ENEMY;
UPDATE_ENDPOINTS(pSq, gFirstStone, gLastStone);
++gMoveNum;
if (gMoveNum == 1) { // *** MOVE 2
move.h = move.v = -2;
pSq = &gBoard[TRANSLATE(-2, -2)];
*pSq = _FRIEND;
UPDATE_ENDPOINTS(pSq, gFirstStone, gLastStone);
*yourMove = move;
gMoveNum = 2;
return;
}
if (gMoveNum == 2) { // *** MOVE 3
if (opponentsMove.v < opponentsMove.h) {
if (opponentsMove.v < -opponentsMove.h) {
// top triangle
move.h = 0;
move.v = 4;
} else {
// right triangle
move.h = -4;
move.v = 0;
}
} else {
if (opponentsMove.v >= -opponentsMove.h) {
// bottom triangle
move.h = 0;
move.v = -4;
} else {
// left triangle
move.h = 4;
move.v = 0;
}
}
pSq = &gBoard[TRANSLATE(move.h, move.v)];
*pSq = _FRIEND;
UPDATE_ENDPOINTS(pSq, gFirstStone, gLastStone);
*yourMove = move;
gMoveNum = 3;
return;
}
gChangesEnd = gChanges;
gCumCapturesEnemy +=
MyFindCaptures(opponentCaptures, &gBoard[i],
_ENEMY, _FRIEND);
for (pSq = gEstimatesStart; pSq != gEstimatesEnd; ++pSq)
*pSq = 0;
for (pSq = gFirstStone; pSq <= gLastStone; ++pSq)
if (OCCUPIED(*pSq)) {
d = gDirections;
do {
pNewSq = pSq + *d;
if (EMPTY(*pNewSq)) {
gEstimates[GET_INDEX(pNewSq)] += ESTIMATE_PLUS;
pNewSq += *d;
if (EMPTY(*pNewSq)) {
gEstimates[GET_INDEX(pNewSq)] += ESTIMATE_PLUS;
}
}
} while (++d != gDirectionsEnd);
}
for (pSq = gBoardStart; pSq != gBoardEnd; ++pSq)
if (gEstimates[GET_INDEX(pSq)]) {
i = GET_INDEX(pSq);
gScore = gEstimates[i];
gPreScore[i] = AddStone(-INFINITY, INFINITY, pSq,
_FRIEND, 0, gCumCapturesFriend,
gCumCapturesEnemy, gFirstStone,
gLastStone);
}
for (i=0; i<SEARCH_DEPTH; ++i)
gKillers[i] = NULL;
bestScore = -INFINITY;
pBestMove = NULL;
pSq = ChooseNextMove();
while (pSq) {
i = GET_INDEX(pSq);
gScore = gEstimates[i];
score = AddStone(-INFINITY, -bestScore, pSq, _FRIEND,
gStartDepth, gCumCapturesFriend,
gCumCapturesEnemy, gFirstStone, gLastStone);
gEstimates[i] = -1; // searched
if (score > bestScore) {
bestScore = score;
pBestMove = pSq;
}
pSq = ChooseNextMove();
}
if (bestScore == -INFINITY) {
for (pSq = gBoardStart; pSq != gBoardEnd; ++pSq)
if (EMPTY(*pSq) && !gEstimates[GET_INDEX(pSq)]) {
gScore = 0;
score = AddStone(-INFINITY, -bestScore, pSq, _FRIEND,
gStartDepth, gCumCapturesFriend,
gCumCapturesEnemy, gFirstStone, gLastStone);
if (score > bestScore) {
bestScore = score;
pBestMove = pSq;
}
}
if (bestScore == -INFINITY) {
DebugStr("\p no move found - Pente");
return; //no move
}
}
*pBestMove = _FRIEND;
UPDATE_ENDPOINTS(pBestMove, gFirstStone, gLastStone);
i = GET_INDEX(pBestMove);
move.h = GET_X(i);
move.v = GET_Y(i);
*yourMove = move;
++gMoveNum;
// find captures
*numCaptures = MyFindCaptures(claimCaptures, pBestMove,
_FRIEND, _ENEMY);
gCumCapturesFriend += *numCaptures;
if (gCumCapturesFriend >= 5) {
*claimVictory = true;
} else {
*claimVictory = MyFindFive(pBestMove, _FRIEND);
}
}
TermPente
void TermPente(void) { }
ChooseNextMove
short * ChooseNextMove()
{
short i, *pSq;
short *found = NULL;
short high = -INFINITY;
for (pSq = gBoardStart; pSq != gBoardEnd; ++pSq) {
i = GET_INDEX(pSq);
if (gEstimates[i] > 0 && gPreScore[i] > high) {
found = pSq;
high = gPreScore[i];
}
}
return found;
}
AddStone
// Alpha-beta search routine
// returns score of how good it is for color
// alpha and beta apply for the opponent after the move
// is made
// gScore is absolute: + for _FRIEND
short AddStone(short alpha, short beta, short *pSq,
short color, short depth, short capturesFriend,
short capturesEnemy, short *firstStone,
short *lastStone)
{
register short *pNewSq, *d;
short x, bestScore, t, *pEnd;
short open, *killer;
short threats, blocks, vulnerable;
short opponent = OPPONENT(color);
short saveScore = gScore;
START_SAVE;
threats = blocks = vulnerable = 0;
d = gDirections;
do {
pNewSq = pSq + *d;
if (*pNewSq == color) { // Next to friend
if (d <= &gDirections[3]) {
x = 1;
open = 0;
for (pNewSq += *d; *pNewSq == color; pNewSq += *d)
++x;
if (EMPTY(*pNewSq))
open = 1;
for (pNewSq = pSq + *(d+4); *pNewSq == color;
pNewSq += *(d+4))
++x;
if (EMPTY(*pNewSq))
++open;
} else {
t = *(pSq + *(d-4));
if (t == color)
continue;
x = 1;
open = 0;
if (EMPTY(t))
open = 1;
for (pNewSq += *d; *pNewSq == color; pNewSq += *d)
++x;
if (EMPTY(*pNewSq))
++open;
}
switch (x) {
case 1: // 2-in-a-row
gScore += CHAIN_SCORE2[color][open];
if (open == 1)
++vulnerable;
break;
case 2: // 3-in-a-row
gScore += CHAIN_SCORE3[color][open];
if (open == 2)
threats += 2; // tria = 2
break;
case 3: // 4-in-a-row
gScore += CHAIN_SCORE4[color][open];
threats += open * 3; // tessera = 6, half-open = 3
break;
default: // 5-in-a-row (or more)
gScore += CHAIN_SCORE5[color];
threats = -99; // game over
break;
}
} else if (*pNewSq == opponent) { // Next to enemy
x = 1;
for (pNewSq += *d; *pNewSq == opponent; pNewSq += *d)
++x;
if (EMPTY(*pNewSq)) {
blocks += BLOCK_SCORE[x];
} else if (x == 2 && *pNewSq == color) {
t = CAPTURE_SCORE;
if (color != _FRIEND)
t = -t;
gScore += t;
if (color == _FRIEND) {
if (++capturesFriend >= 5) {
threats = -99; // game over
}
} else { // color == _ENEMY
if (++capturesEnemy >= 5) {
threats = -99; // game over
}
}
if (depth) {
pNewSq = pSq + *d;
PUSH_SQ(pNewSq);
*pNewSq = _EMPTY;
pNewSq += *d;
PUSH_SQ(pNewSq);
*pNewSq = _EMPTY;
}
}
}
} while (++d != gDirectionsEnd);
if (threats < 0) {
// Game over
bestScore = gScore;
if (color != _FRIEND)
bestScore = -bestScore;
goto RESTORE;
}
// Not forced to make a bad move
if (color == _FRIEND) {
if (gScore < saveScore)
gScore = saveScore;
} else if (gScore > saveScore)
gScore = saveScore;
if (depth) {
// Add stone
PUSH_SQ(pSq);
*pSq = color;
UPDATE_ENDPOINTS(pSq, firstStone, lastStone);
--depth;
gPreviousThreats = threats;
bestScore = -INFINITY;
// Killer move?
killer = gKillers[depth];
if (killer && EMPTY(*killer)) {
saveScore = gScore;
bestScore = AddStone(-beta, -alpha, killer,
opponent, depth, capturesFriend,
capturesEnemy, firstStone, lastStone);
gScore = saveScore;
if (bestScore > alpha) {
if (bestScore >= beta) {
bestScore = -bestScore;
goto RESTORE;
}
alpha = bestScore;
}
}
pEnd = lastStone + gSE;
if (pEnd >= gBoardEnd)
pEnd = gBoardEnd - 1;
pSq = firstStone - gSE;
if (pSq < gBoardStart)
pSq = gBoardStart;
do {
if (EMPTY(*pSq) && pSq != killer) {
d = gDirections;
do {
pNewSq = pSq + *d;
if (OCCUPIED(*pNewSq) &&
(*(pNewSq + *d) == *pNewSq ||
*(pSq - *d) == *pNewSq))
break;
} while (++d != gDirectionsEnd);
if (d != gDirectionsEnd) {
saveScore = gScore;
t = AddStone(-beta, -alpha, pSq, opponent,
depth, capturesFriend, capturesEnemy,
firstStone, lastStone);
gScore = saveScore;
if (t > bestScore) {
bestScore = t;
if (t > alpha) {
if (t >= beta) {
gKillers[depth] = pSq;
break;
}
if (depth >= gStartDepth-1)
gKillers[depth] = pSq;
alpha = t;
}
}
}
}
} while (++pSq <= pEnd);
if (bestScore == -INFINITY) {
++depth;
goto TERMINAL;
}
bestScore = -bestScore;
} else { // !depth
TERMINAL:
bestScore = gScore;
if (color != _FRIEND)
bestScore = -bestScore;
// Winning threats
if (gPreviousThreats >= 5) {
bestScore -= THREATS[gPreviousThreats];
} else if (threats >= 5) {
bestScore += THREATS[threats];
} else if (gPreviousThreats == 4) {
bestScore -= THREATS[gPreviousThreats];
} else if (threats == 4) {
bestScore += THREATS[threats];
} else if (gPreviousThreats) {
bestScore -= THREATS[gPreviousThreats];
} else if (threats) {
bestScore += THREATS[threats];
}
bestScore += blocks - vulnerable * VULNERABLE;
}
RESTORE:
while (POP) {
pSq = (short *)TOP;
*pSq = POP;
}
return bestScore;
}
MyFindCaptures
long MyFindCaptures(Capture capture[], short *pSq,
short us, short them)
{
short i, *p1, *p2;
short myCaptures = 0;
short *d = gDirections;
do {
p1 = pSq + *d;
if (*p1 == them) {
p2 = p1 + *d;
if (*p2 == them && *(p2 + *d) == us) {
*p1 = *p2 = _EMPTY;
i = GET_INDEX(p1);
capture[myCaptures].stone1.h = GET_X(i);
capture[myCaptures].stone1.v = GET_Y(i);
i = GET_INDEX(p2);
capture[myCaptures].stone2.h = GET_X(i);
capture[myCaptures].stone2.v = GET_Y(i);
++myCaptures;
}
}
} while (++d != gDirectionsEnd);
return myCaptures;
}
MyFindFive
Boolean MyFindFive(short *pSq, short color)
{
short x, *d, *pNewSq, i;
d = gDirections;
i = 0;
do {
x = 1;
for (pNewSq = pSq + *d; *pNewSq == color; pNewSq += *d)
++x;
for (pNewSq = pSq + *(d+4); *pNewSq == color;
pNewSq += *(d+4))
++x;
if (x >= 5)
return true;
++d;
} while (++i < 4);
return false;
}