TweetFollow Us on Twitter

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;
}
 

Community Search:
MacTech Search:

Software Updates via MacUpdate

OnyX 3.8.6 - Maintenance and optimizatio...
OnyX is a multifunction utility that you can use to verify the startup disk and the structure of its system files, to run miscellaneous maintenance and cleaning tasks, to configure parameters in the... Read more
Bean 3.3.3 - Fast and uncluttered word p...
Bean is no longer being actively developed, but will be updated as necessary to patch bugs and maintain OS X compatibility Bean is lean, fast, and uncluttered. If you get depressed at the thought of... Read more
Acorn 6.6.2 - Bitmap image editor.
Acorn is a new image editor built with one goal in mind - simplicity. Fast, easy, and fluid, Acorn provides the options you'll need without any overhead. Acorn feels right, and won't drain your bank... Read more
Tor Browser 10.0 - Anonymize Web browsin...
The Tor Browser Bundle is an easy-to-use portable package of Tor, Vidalia, Torbutton, and a Firefox fork preconfigured to work together out of the box. It contains a modified copy of Firefox that... Read more
EtreCheck Pro 6.3.2 - For troubleshootin...
EtreCheck is an app that displays the important details of your system configuration and allow you to copy that information to the Clipboard. It is meant to be used with Apple Support Communities to... Read more
Macs Fan Control 1.5.8.22 - Monitor and...
Macs Fan Control allows you to monitor and control almost any aspect of your computer's fans, with support for controlling fan speed, temperature sensors pane, menu-bar icon, and autostart with... Read more
Iridient Developer 3.4 - Powerful image-...
Iridient Developer (was RAW Developer) is a powerful image-conversion application designed specifically for OS X. Iridient Developer gives advanced photographers total control over every aspect of... Read more
Motion 5.4.7 - Create and customize Fina...
Motion is designed for video editors, Motion 5 lets you customize Final Cut Pro titles, transitions, and effects. Or create your own dazzling animations in 2D or 3D space, with real-time feedback as... Read more
calibre 5.0.1 - Complete e-book library...
Calibre is a complete e-book library manager. Organize your collection, convert your books to multiple formats, and sync with all of your devices. Let Calibre be your multi-tasking digital librarian... Read more
Compressor 4.4.8 - Adds power and flexib...
Compressor adds power and flexibility to Final Cut Pro X export. Customize output settings, work faster with distributed encoding, and tap into a comprehensive set of delivery features. Features:... Read more

Latest Forum Discussions

See All

Blastomancer is a comfortable puzzle gam...
Blastomancer arrived on Google Play for Android at the weekend. It’s a 2D puzzle game involving cartoon stick figure wizards and lots of bombs. [Read more] | Read more »
Undercrawl is a procedurally generated r...
Undercrawl is a roguelike dungeon crawler from indie developer Monster Shop Games. It's a genre that's popular in gaming in general but features even more frequently on mobile devices since the shorter, 'run' style of playthrough suits playing in... | Read more »
Distract Yourself With These Great Mobil...
There’s a lot going on right now, and I don’t really feel like trying to write some kind of pithy intro for it. All I’ll say is lots of people have been coming together and helping each other in small ways, and I’m choosing to focus on that as I... | Read more »
BTS Universe Story, the social game that...
Netmarble's highly anticipated social game, BTS Universe Story, is available now for iOS and Android. It's the second collaboration between the hugely successful mobile developer and the K-pop superstars following BTS World. [Read more] | Read more »
The 5 Best Mobile Games Like Hades
Supergiant Games finally released Hades upon the world this week, and we’re loving it. The game plays to all of the studio’s strengths while still retaining a strong sense of identity. It also just so happens to play rather well using the Steam... | Read more »
A Year of Apple Arcade: The Good, The Ba...
Apple Arcade has persisted for just over a year at this point, and although that means I've been busy ranking and re-ranking every game on the service for just about as long, I haven't done much reflection on the service as a whole. [Read more] | Read more »
Animal Restaurant anniversary event team...
Animal idle simulator Animal Restaurant is celebrating its first-year anniversary with a crossover event with popular YouTube series Aaron’s Animals. [Read more] | Read more »
Raziel: Dungeon Arena is a hack 'n...
Raziel: Dungeon Arena is available now on mobile and will appeal to fans of both comic books and old school dungeon crawlers. Not only will you hack 'n' slash your way through mobs of enemies but there's also fully-narrated animated comic to enjoy... | Read more »
Steam Link Spotlight - Hades
Steam Link Spotlight is a feature where we look at PC games that play exceptionally well using the Steam Link app. Our last entry was on Disco Elysium. Read about how it plays using Steam Link over here. | Read more »
Microsoft has acquired ZeniMax Media and...
In the latest of a series of blockbuster moves, Microsoft has now acquired Zenimax Media and its subsidiary, Bethesda Softworks, for $7.5 billion. [Read more] | Read more »

Price Scanner via MacPrices.net

AT&T now offering $1000 discount on Apple...
AT&T is offering a $1000 discount on any Apple iPhone 11 Pro or 11 Pro Max with trade-in and new line of service. Discount applied monthly over a 30 month period. Eligible iPhone trade-ins are... Read more
New 2020 12.9″ iPad Pros on sale for up to $8...
Apple reseller Expercom has new 2020 Apple 12.9″ iPad Pros on sale today for $60-$85 off MSRP, with prices starting at $939. Not every model is in stock, but you can still order an iPad at these... Read more
Apple offers clearance 2019 27″ 5K iMacs for...
Apple has Certified Refurbished 2019 27″ 5K iMacs available starting at $1439 and up to $520 off their original MSRP. Apple’s one-year warranty is standard and shipping is free. The following... Read more
Weekend’s Best MacBook Deal: These 2020 13″ M...
Apple has a full line of Certified Refurbished 2020 13″ 1.4GHz 4-Core Touch Bar MacBook Pros available starting at $1099 and up to $230 off original MSRP. Apple’s one-year warranty is included,... Read more
Clearance 8-core iMac Pro available for $3819...
Apple has Certified Refurbished, clearance, 27″ 3.2GHz 8-Core iMac Pros available $3819 including free shipping. Their price is $1180 off the original MSRP of new models. A standard Apple one-year... Read more
How The Upcoming Mac Transition To Apple Sili...
FEATURE: 09.25.20 – Apple’s plan to transition all of its desktop and notebook computers away from Intel processors to Apple silicon, chips designed by the company itself, has been eclipsed by the... Read more
New low price! Apple Watch SE for only $269
B&H Photo is reporting limited stock of Apple’s new Apple Watch SE GPS models for $10 off MSRP and including free shipping. Their $269 price for the 40mm model is the lowest price we’ve seen so... Read more
Lowest price anywhere: New 13″ 2.0GHz MacBook...
Amazon has new 2020 13″ 2.0GHz/512GB MacBook Pros with 10th generation Intel processors back in stock on sale today for $200 off Apple’s MSRP. Shipping is free. Be sure to purchase the MacBook Pro... Read more
Apple Pro Display XDR with Nano-Texture Glass...
Amazon Apple Premier Partner GatorTec has the Apple Pro Display XDR with Nano-Texture Glass on sale for $5599 shipped, on Amazon. Their price is $400 off Apple’s MSRP, and it’s the cheapest price... Read more
Get a 2019 13″ MacBook Air for only $779 toda...
Apple has clearance, Certified Refurbished, 2019 13″ 1.6GHz/128GB MacBook Airs available again for $779. Each MacBook features a new outer case, comes with a standard Apple one-year warranty, and is... Read more

Jobs Board

Senior Product Manager, *Apple* Platforms -...
…technical/operational expertise of our employees. **Role Details:** The Senior Product Manager, Apple Platform will report to the Senior Director of Product and Read more
Tier 2 Technical Support Analyst - ( *Apple*...
…Support Analystiwho will analyze and determine user software needs on all Apple devices (first support contact), Windows devices, and support printers in the Read more
Medical Assistant - Internal Medicine *Apple...
Schedule & Location Full Time Day (80 Hours every two weeks) Apple Hill Medical Center 25 Monument Road York PA Job Description: Under the general supervision of the Read more
*Apple* Technology Journalist - Valnet Freel...
…freelance basis. Valnet Inc. is looking for journalists with strong knowledge of Apple technology for our website MakeUseOf.com MakeUseOf is one of the largest Read more
Bookseller- *Apple* Shop Support - Penn Sta...
…combination of education, certification and experience will be considered. + A+ and/or Apple certified or able to achieve certification within 60 days required. + Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.