PROGRAMMER's CHALLENGE

by Bob Boonstra

Farewell

As I announced last month, this will be my last Programmer's Challenge column. I thought I would close out my tenure with a retrospective on the past seven and one-half years and the contestants, the Challenge problems, the evolution of the column, and my experiences writing it.

When I first started participating in the Challenge back in 1993, the Macintosh computer and the development environments that supported it were very different from what they are today. All entries were all 680x0 code. We used compilers from both Metrowerks and Symantec, admitting code in Pascal as well as C. When I was fortunate enough to fare well in the contests, it was due to both a reasonably good algorithm and to well-tuned C code, tweaked by analyzing the disassembled code and fooling the compiler into squeezing out an instruction or two.

By October 1995, a few months after I started writing the column, the PowerPC had been out long enough that we were able to convert the Challenge to PPC code without losing too many potential contestants. The move to PowerPC led to the demise of the annual assembly language Challenges, as fewer people were able to compete with a compiler at generating PPC code. We continued to accept Pascal solutions as long as possible, but with the demise of Symantec and the deprecation of the Metrowerks Pascal compiler, Pascal was no longer a practical option. At the request of readers, we structured some problems to accommodate entries in other development environments like REALbasic, but we only received a few entries using these environments. During the heyday of BeOS (may it rest in peace), we even ran a few Challenges using that operating system.

Over time, the power of my test machine grew, from a 6100/60 that I clock-chipped to run at 80 MHz, to a 7200/90, to an 8500 that was gradually upgraded with a sequence of processor upgrades, to the G4/500 that I use today, with a variety of PowerBooks thrown in for good measure. Sadly, the dual 1.25 GHz processor system currently on order will not arrive in time for use in the Challenge.

The Challenge had always accepted solutions via email, but it originally also allowed solutions to be submitted by physical mail, although, to the best of my knowledge, no one ever submitted one this way. Mike Scanlin and I both answered questions about the problems via email. I became concerned that not everyone had access to these answers. In December, 1995, Neil Ticktin and I decided to create an email distribution list for the Challenge, and that list became so essential that I cannot imagine running the column without it. Besides enabling us to provide answers to questions for everyone, the list allowed us to distribute the problem on a regular schedule, avoiding publication delays with the physical magazine that often resulted in the problem being printed after solutions were due!

The Challenge prizes also evolved over time. Originally the prize was $50 cash and a "Programmer's Challenge WINNER" t-shirt. For me, the t-shirt provided bragging rights, and the cash would provide (part of) an evening out with my spouse, ever patient with the hours consumed solving Challenge problems. At some point, the t-shirts went away, and the prize was increased to $100, redeemable at DevDepot. No longer useful at restaurants, perhaps, but the credit would buy twice as many toys!

The Challenge began to be dominated by regular participants, whose participation and dedication were fantastic. At the same time, we became concerned that their dominance might be discouraging to new readers thinking about mounting a Challenge, so to speak. We experimented with splitting the prize, with half going to the traditional winner, and half going to the top placing contestant who hadn't won a Challenge recently. The prize was split in this fashion a few times, but we also had some newcomers that won the Challenge outright, winning the entire prize.

In September 1997, we began offering test code with most of the Challenges. Prior to then, each contestant had to supply his/her own test drivers and test data to develop and verify their solution. As the problems became more difficult over time, more and more effort was required to create test code. Although I had always needed to write code to evaluate Challenge entries, producing test code that was useable by others, and doing so early enough to be useful to contestants, proved to be increasingly difficult. In the minds of some readers, the test code became an entitlement, and at least one declined to participate unless test code was provided. On many occasions, readers submitted their own test code, and I would often incorporate or adapt their offerings into the final test code. I would be remiss if I failed to acknowledge the readers who also helped debug the test code, more frequently than I would care to admit. Ernst Munter, besides being a frequent and successful participant in the Challenge, was especially helpful in contributing to and debugging test code.

With the regular availability of test code, we needed a way to distribute it. We set up an ftp site at MacTech, but there were some startup problems that were difficult to resolve from 3000 miles away. So I set up a Challenge section at my personal site (http://www.windmillsw.com), and began posting the test code there. Eventually, I would post the problems, the test code, and eventually the winning solutions at both the MacTech site (http://www.mactech.com/progchallenge/>) and at my personal site.

With the addition of test code, the mailing list, and the web site, writing the column changed from being a one-weekend-a-month effort to a constant activity. Writing the column itself involved selecting a problem that was both interesting and solvable in a limited amount of time. Whether I succeeded at either is for you to judge, but my perception is that I became less successful at the latter over time. Ideas for problems came from many sources, including suggestions from readers, and even suggestions from my family. Sometimes the idea sounded great when I wrote the article, but less great when I sat down to write the test code and provide test data. The March, 1998, "Help Peter Get Home" Challenge comes to mind, where I needed to create airline schedules for a Traveling Salesperson style problem, and resorted to reverse engineering Official Airline Guide data. Test code for the November, 1999, Putting Green problem was also Challenging, as I needed to create a physics model for a golf ball rolling along the green. We ran a number of contests that pitted one contestant against another, competing in games from Stratego, to Trilite, to Chess. Problems included test-based Challenges (e.g., August, 2000, Longest Word Sort), research problems (e.g., December, 1999, Costas Arrays), graphics problems (e.g., August, 1999, 3D Fly-By), more physics problems (e.g., Caribbean Cruising in August, 2001), puzzles (e.g., the June, 1999, Tetraminx and June, 2000, R*bik Rotation Challenges), and general algorithmic Challenges (e.g., October, 2000, What Bills Did They Pay). On a few occasions, the problems attracted no entries (e.g., the September, 2001, Nassi-Schneiderman Challenge). And in at least one case, I think the problem was actually unsolvable (the July, 2000, RAID 5+ problem).

It is hard to say which problem might have been my favorite, but the May 2002, Jigsaw Puzzles may have been the most satisfying. It started as one of those problems that sounded great, but providing test data turned out to be very difficult. Fortunately, with the help of the Graphing Calculator, I was able to generate reasonably interesting puzzle pieces that actually resembled a real jigsaw puzzle.

The Challenge has always been focused on efficiency, and on the Macintosh. Occasionally I would receive suggestions for changing that focus. I would receive suggestions from PC readers who wanted to remove the Mac orientation. I heard from readers who wanted the problems to be focused on practical applications and not on efficiency. We tried to vary the problems so that there would be something of interest to many people, but without forgetting that MacTech is, after all, a Macintosh magazine, and without losing the interest in programming efficiency that has been part of the Challenge from the beginning.

If you haven't written for a print magazine before, you might not appreciate the importance of publication deadlines. When I first began writing the column, the article was due at MacTech mere days after the solutions were submitted. That resulted in a number of all-night sessions to complete the scoring and write the column, and began to interfere with the Real Job. Eventually Neil and I altered the format so that the column included the solution from three months earlier, instead of two months earlier, giving me a reasonable amount of time to evaluate entries. But, being a procrastinator by nature, and increasingly busy, I still found myself trying the patience of my editor and bumping right up against the deadline. In one case, January 2002, I actually missed the deadline, and the Challenge column did not appear in the magazine. My apologies to Jessica for making her life difficult, and my thanks for her patience over the years.

I would not have been able to write the column without the support of my wife, Sharon. Besides patiently putting up with all of the time spent on the Challenge, she helped me through many a mental block by suggesting a Challenge concept, and also proofread many of the columns, catching numerous errors. Any errors that remained, of course, are my responsibility, but they were far fewer than would have been the case without her help.

In all of my time authoring the column, to the best of my knowledge I misplaced only one solution, the November, 1995 entry by Greg Linden to the Enclosing Bounds Challenge. As long as I am apologizing, I owe one to Greg. Sorry about that!

Finally, I would like to express my appreciation to everyone who participated in the Challenge over the years. More than one hundred people have placed in the top three finishers for at least one Challenge, and I have listed them all later in the column. Special thanks go to our regular participants, Ernst, Tom, Randy, Xan, Willeke, Allen, Jeff, Jonathan, Sebastian, Bill, Gustav, Alan, Peter, Tony, Ludo, and many others. Our all-time champion, Ernst Munter, has participated in 83 Challenges, and won an astounding 30 of them. Many of you have become regular email correspondents, and - while I have not met most of you - have become friends. I hope you have enjoyed reading the column and participating in the Challenge as much as I have enjoyed authoring it. Please do keep in touch.

Winner of the October, 2002 Challenge

Congratulations to Jonny Taylor (Oxford, U.K.) for taking first place in the October, 2002, Area Challenge. This problem presented readers with a rectangular grid that was to be covered with rectangles of specified areas. Cells in the grid were either empty or contained an integer. Nonempty cells were to be covered by a rectangle with an area equal to the value in the cell. Each cell had to be covered by one and only one rectangle. The Challenge was to cover the grid with rectangles satisfying the area constraints as quickly as possible.

Jonny's solution is very well commented, and I will only go over the highlights of his strategy. Jonny calls the nonzero cells in the grid Anchors. Each Anchor has associated with it a number of Shapes, or rectangles that can legally cover that Anchor. One key to the speed of Jonny's solution seems to be the iteration of steps 1 and 2 described in his commentary. His solution calculates the intersection of all Shapes that can cover each Anchor. Since the rectangle covering an Anchor must contain all of the cells in this intersection, any Shape covering another Anchor cannot include a cell in this intersection. By iteratively calculating the Anchor intersections, eliminating Shapes that violate the intersection constraint, and thus enlarging the intersection for the Anchor associated with the eliminated Shape, Jonny reduces the size of the search needed to resolve the remaining ambiguities.

Alan Hart employs a recursive approach that caches a list of legal rectangles with a given cell as the top-left corner of the rectangle. His solution was about one-third slower than the winning entry. Ernst Munter used an exhaustive search, and Tom Saxton used a recursive approach. Both Ernst and Tom provided interesting optional graphical displays (not active during timed tests) of the progress of their solutions.

The table below lists, for each of the solutions submitted, the execution time in seconds required to solve my set of test cases. As usual, the number in parentheses after the entrant's name is the total number of Challenge points earned in all Challenges prior to this one.

Top Contestants ...

Normally, in this spot we would list the contestants who had accumulated 20 or more points during the past two years. This being my last Challenge column, I thought I would list everyone who had placed first, second, or third in any Challenge since the column started back in 1992. The table lists the total number of points earned since the beginning of the Challenge, with 20 points being awarded for each win, 10 for each 2nd place finish, and 7 / 4 / 2 points for each 3rd, 4th, and 5th place finish, respectively. My thanks to everyone who has participated over the years, and congratulations to those who have earned a win. I hope you have enjoyed participating as much as I have enjoyed running the Challenge.

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include "Area.h"
/*
The strategy I have used requires three main data structures:
Anchors Represent an item in the cells[] array which initially contained a
Nonzero number. Each Anchor has associated with it a (potentially
large) number of...
Shapes Represent the position of a rectangle of the appropriate area which
Overlaps its parent Anchor, but doesn't overlap any cells it shouldn't
do (such as other Anchors). There is one Shape for every legal
rectangle covering every Anchor. A Shape is "placed" when I know it
is the only shape that can overlap its Anchor without causing fatal
problems elsewhere in the grid.
Cells There is one MyCell structure for each cell in the problem. They track
The number of Shapes that overlap them, and have the head of a
linked list which includes every such overlapping Shape. This is
expensive to maintain, but is crucial to the strategy I use.
There is also a global list which is used once all Shapes have been
generated. Whenever the number of Shapes overlapping a cell drops to
1, the Cell is added to this list. This is a quick way of finding such
Cells when performing step 3 of the algorithm. Without this list,
overall execution time increases by a factor of nearly 8 (for the test
cases provided).
1. For each anchor in turn, I discover all possible shapes. I do NOT allocate a Shape
structure, though - that is very time- and memory-intensive. Instead I keep track of
the rectangle of cells which is overlapped by ALL of the possible shapes. I know
that for other anchors there is no point considering shapes which overlap those
cells.
2. Now I know these rectangles, I go back through the anchors considering which
shapes are still possible. Hopefully this will increase the common rectangle still
further, reducing the number of possible shapes for the other anchors, and so on. I
repeat this phase until there is no significant reduction in the number of possible
Shapes.
3. Now I actually generate the Shape structures and link them in to the cells which
they overlap
4. With the Shape structures created, it is possible to finish off step 2 more efficently.
Now I maintain a list of all Anchors whose common rectangle may have been
altered. I work through the list and reject any shapes belonging to OTHER Anchors
which overlap the new common rectangle. (note that those Anchors must then be
added to the list). When the list becomes empty, I know that I have obtained the
optimal common rectangles for each Anchor.
5. At the same time, I maintain a list of those cells which only one Shape overlaps.
Whenever a cell is added to that list, I can "place" that shape (which may cause
other cells/anchors to be added to the relevant lists for steps 4 and 5.
6. Next, I experiment with placing each possible Shape in turn, and do any step 5
processing that this generates. During this we may end up with a cell that cannot be
covered by any Shape. If that is the case, I reject the original Shape that I was
experimenting with. This stage is extremely time-consuming. In some cases it can
cause the execution time to double. However, it is the only way of solving some
large problems within a sensible amount of time - in one case it reduced the
execution time from being nowhere near a solution after 30 minutes to an execution
time of about one minute.
7. Eventually, we will be unable to place any more shapes in this way. I then record
the current state, choose an Anchor, and work systematically through all possible
Shapes that can still cover it, and repeat stage 5. If I find a cell that cannot be
covered by any Shape, I know there must have been a bad choice made earlier. We
restore the state that we just recorded, and move on to try the next possible Shape.
There are two limiting factors to this algorithm. The first is memory. Each shape that we allocate
memory for requires just over 12 * <area> bytes. It appears that the total area of shapes
allocated only grows slowly relative to the total area of the board. Taking into account the
additional storage required, I would therefore expect to require a total of around 40 * <area>
bytes of storage - i.e. 20 times the size of the input array. This seems a fairly reasonable
requirement, making an 800x800 problem solvable, and a 200x200 problem very practical.
The other problem is the complexity of the problem layout. My code deals with the test cases provided
very quickly. I also tried making bigger test cases by using the original test cases as tiles on
a 4x4 grid, making a test case 16 times as big. It handles the largest case (800x600) in around a
second (depending on the test machine). I then tried scaling the original test cases up by a factor
of 2 (so that all rectangles have 4 times the area). Unfortunately it fails to get anywhere near
solving some of the larger test cases in a reasonable amount of time. There are large areas where the
common rectangles only cover the anchor cells themselves, leaving far too much work to be done in
stage 6 of the algorithm.
Optimizations:
Memory pooling is a small performance gain
Garbage lists (see Anchor::TryAllShapes) speed up the code by several times
Maintaining the lists gCellList and gCommonRectList save a lot of unnecessary
scanning through arrays, and speed up the code by a factor of 10!
Any optimization which reduces the number of Shapes that we need to deal with will have a significant
effect. In particular, the commonRect processing sometimes takes quite a few passes for the effects
of a change to propagate across the grid. Because this processing is cut short, more shapes are
generated than is necessary
*/
//#define DEBUGGING
#ifdef DEBUGGING
#define ASSERT(CONDITION) do { if (!(CONDITION)) { Debugger(); exit(0); } } while(0)
#else
#define ASSERT(CONDITION) do { } while(0)
#endif
struct Shape;
struct Anchor;
struct MyCell;
struct ShapeListItem;
#define kMemPoolSize 10000000
#define kNumMemPools 100
enum
{
kBuildCommonRect = 1,
kRefineCommonRect,
kGenerateShapes
};
void NextPool(void);
Boolean DoAutomaticPlacements(void);
void PruneShapes(void);
Boolean ProcessCellList(void);
long PartitionSort(Anchor *anchorList[], long first, long last);
void DoQuickSort(Anchor *anchorList[], long first, long last);
Anchor *gAnchors;
MyCell *myCells;
long *cellsCovered;
long *lastXInScanline, *firstXInScanline;
long gShapeGenerationPhase;
long gGridWidth, gGridHeight, gNumAnchors;
MyCell *gCellList;
Anchor *gCommonRectList;
ShapeListItem *gCurrentGarbageList;
Boolean gAddToCellList;
Boolean gFailedFlag, gAbortOnFailure = false;
static char *poolPtrs[kNumMemPools];
static char *gMemPool = NULL;
static long gCurrentMemPoolNum = -1;
#define CELL_COVERED(X, Y) (cellsCovered[((Y) * gGridWidth + (X)) >> 5] &
(1 << (((Y) * gGridWidth + (X)) & 0x1F)))
#define SET_CELL_COVERED(X, Y) (cellsCovered[((Y) * gGridWidth + (X))
>> 5] |= (1 << (((Y) * gGridWidth + (X)) & 0x1F)))
#define SET_CELL_UNCOVERED(X, Y) (cellsCovered[((Y) * gGridWidth + (X)) >> 5] ^=
(1 << (((Y) * gGridWidth + (X)) & 0x1F)))
#define MY_CELL(X, Y) (myCells[(Y) * gGridWidth + (X)])
#define CELL(X, Y) (cells[(Y) * gGridWidth + (X)])
inline long MAX(long a, long b)
{
long temp = b - a;
ASSERT(b - (temp & (temp >> 31)) == ((a > b) ? a : b));
return(b - (temp & (temp >> 31)));
}
inline long MIN(long a, long b)
{
long temp = a - b;
ASSERT(b + (temp & (temp >> 31)) == ((a > b) ? b : a));
return(b + (temp & (temp >> 31)));
}
#define MAXIMIZE(A, B) (A) = MAX((A), (B));
#define MINIMIZE(A, B) (A) = MIN((A), (B));
void NextPool(void)
{
// Move on to the next memory pool for allocation of Shape structures
gCurrentMemPoolNum++;
if (gCurrentMemPoolNum == kNumMemPools)
DebugStr("\pNeed more pools");
// Check if the pool needs allocating or not. I do not bother releasing the pools after
// every call to Area. I do not see this as strictly a memory leak, since multiple calls
// to Area will not result in any more memory being allocated.
if (poolPtrs[gCurrentMemPoolNum] == NULL)
poolPtrs[gCurrentMemPoolNum] = (new char[kMemPoolSize]);
gMemPool = poolPtrs[gCurrentMemPoolNum];
if (gMemPool == NULL)
DebugStr("\pOut of memory");
}
struct ShapeListItem
{
Shape *theShape;
ShapeListItem *next;
ShapeListItem *prev;
ShapeListItem(void);
void Init(void)
{
theShape = NULL;
next = this;
prev = this;
}
void AddTo(Shape *parent, ShapeListItem *listHead);
void Remove(void);
Boolean InList(void) const { return(next != this); }
};
struct MyCell
{
long overlapCount;
ShapeListItem shapeListHead;
MyCell *next, *prev;
Boolean inList;
MyCell(void);
void MustBeCoveredBy(long index, Anchor *coveringAnchor);
void RemoveFromHead(void);
void IncrementOverlapCount(void) { overlapCount++; }
void AddToList(void);
void DecrementOverlapCount(void)
{
ASSERT(overlapCount > 0);
overlapCount--;
if (overlapCount <= 1)
AddToList();
}
};
struct Shape
{
Anchor *itsAnchor;
ShapeListItem anchorListItem, garbageListItem;
Shape *nextOfAllForAnchor;
long top, left;
long width, height;
void Init(long inLeft, long inTop, long inWidth,
long inHeight, Anchor *inAnchor);
void Reject(void);
void Reinstate(void);
void DoDecrement(void);
ShapeListItem cellListItems[];
};
struct Anchor
{
long anchorX, anchorY, area;
long numShapes, oneRecordSize;
Rect commonRect, oldCommonRect;
ShapeListItem shapeListHead;
Boolean placed, inCommonRectList;
Anchor *next;
Shape *firstOfAllShapes;
Anchor *nextInCommonRectList;
Anchor(long inX, long inY, long inArea, Anchor *inNext);
void GenerateShapes(void);
void UpdateCommonRect(void);
void GenerateShapesWithWidth(long width, long firstY,
long lastY);
void Place(void) { ASSERT(numShapes == 1); Place(shapeListHead.next->theShape); }
void Place(Shape *theShape);
Boolean TryAllShapes(void);
static int CompareAnchorsBySize(const void *a1,
const void *a2);
};
MyCell
MyCell::MyCell(void)
{
overlapCount = 0;
next = prev = NULL;
inList = false;
}
void MyCell::MustBeCoveredBy(long index, Anchor *coveringAnchor)
{
// Called when we know that the cell can only be covered by a shape based on the
// specified anchor
// (i.e. the cell is included in the anchor's commonRect
cellsCovered[index >> 5] |= (1 << (index & 0x1F));
// Any shapes that overlap this cell which are not based on this anchor can be
// rejected
for (ShapeListItem *shapeItem = shapeListHead.next;
shapeItem->theShape != NULL; )
{
ShapeListItem *nextShapeItem = shapeItem->next;
if (shapeItem->theShape->itsAnchor != coveringAnchor)
{
shapeItem->theShape->Reject();
if (!shapeItem->theShape->itsAnchor->inCommonRectList)
{
// Add the anchor to the list of ones whose commonRect needs updating
shapeItem->theShape->itsAnchor->nextInCommonRectList =
gCommonRectList;
gCommonRectList = shapeItem->theShape->itsAnchor;
gCommonRectList->inCommonRectList = true;
}
}
shapeItem = nextShapeItem;
}
}
void MyCell::RemoveFromHead(void)
{
// Remove this cell from the head of gCellList
ASSERT(inList);
ASSERT(prev == NULL);
if (next != NULL)
next->prev = NULL;
gCellList = next;
next = NULL;
prev = NULL;
inList = false;
}
void MyCell::AddToList(void)
{
// Add this cell to the list of ones which only have one Shape overlapping them
if ((gAddToCellList) &&
(!inList))
{
ASSERT(next == NULL);
ASSERT(prev == NULL);
ASSERT(gCellList != this);
next = gCellList;
if (next != NULL)
next->prev = this;
prev = NULL;
gCellList = this;
inList = true;
}
// Setting this flag will save us from doing unnecessary work when we have already
// got a cell which cannot be covered by any Shape at all
if (overlapCount == 0)
gFailedFlag = true;
}
ShapeListItem
ShapeListItem::ShapeListItem(void)
{
theShape = NULL;
next = this;
prev = this;
}
void ShapeListItem::AddTo(Shape *parent, ShapeListItem *listHead)
{
ASSERT(!InList());
theShape = parent;
ShapeListItem *theNext = listHead->next;
next = theNext;
listHead->next = this;
prev = listHead;
theNext->prev = this;
}
void ShapeListItem::Remove(void)
{
ASSERT(InList());
ShapeListItem *theNext = next;
ShapeListItem *thePrev = prev;
thePrev->next = theNext;
theNext->prev = thePrev;
next = this;
}
Shape
void Shape::Init(long inLeft, long inTop, long inWidth, long inHeight, Anchor *inAnchor)
{
long i = 0;
left = inLeft;
top = inTop;
width = inWidth;
height = inHeight;
itsAnchor = inAnchor;
nextOfAllForAnchor = inAnchor->firstOfAllShapes;
inAnchor->firstOfAllShapes = this;
anchorListItem.Init();
garbageListItem.Init();
for (i = width * height - 1; i >= 0 ; i--)
cellListItems[i].Init();
// Link to its parent anchor
Reinstate();
}
void Shape::Reject(void)
{
// Don't bother doing this if this shape layout had already been found not to work
if (gAbortOnFailure && gFailedFlag)
return;
// Remove this shape from every cell that it is linked to. Loop is unrolled for slight
// performance increase
for (long y = top, i = 0; y < top + height; y++)
{
MyCell *theCell;
long x;
for (x = left, theCell = &MY_CELL(x, y);
x < left + width;
x++, i++, theCell++)
{
theCell->DecrementOverlapCount();
cellListItems[i].Remove();
x++; i++; theCell++;
if (x == left + width)
break;
theCell->DecrementOverlapCount();
cellListItems[i].Remove();
x++; i++; theCell++;
if (x == left + width)
break;
theCell->DecrementOverlapCount();
cellListItems[i].Remove();
}
}
// Remove it from its owning Anchor
anchorListItem.Remove();
ASSERT(itsAnchor->numShapes > 0);
itsAnchor->numShapes--;
// Add it to the garbage list
if (gCurrentGarbageList != NULL)
garbageListItem.AddTo(this, gCurrentGarbageList);
}
void Shape::Reinstate(void)
{
// This shape is now a valid possibility again
// Link to its parent anchor
anchorListItem.AddTo(this, &itsAnchor->shapeListHead);
itsAnchor->numShapes++;
itsAnchor->placed = false;
// Link into the cells which it overlaps
for (long y = top, i = 0; y < top + height; y++)
{
MyCell *theCell;
long x;
for (x = left, theCell = &MY_CELL(x, y);
x < left + width;
x++, i++, theCell++)
{
theCell->IncrementOverlapCount();
cellListItems[i].AddTo(this, &theCell->shapeListHead);
}
}
}
Anchor
Anchor::Anchor(long inX, long inY, long inArea, Anchor *inNext)
{
anchorX = inX;
anchorY = inY;
area = inArea;
next = inNext;
numShapes = 0;
placed = false;
gNumAnchors++;
firstOfAllShapes = NULL;
inCommonRectList = false;
SET_CELL_COVERED(anchorX, anchorY);
oneRecordSize = (sizeof(Shape) + area * sizeof(ShapeListItem)) + 4;
oneRecordSize -= oneRecordSize % 4;
}
void Anchor::GenerateShapes(void)
{
long firstX, firstY, lastX, lastY, x, y;
if (gShapeGenerationPhase != kBuildCommonRect)
{
// We need to unmark the cells in commonRect while in this function
for (y = commonRect.top; y <= commonRect.bottom; y++)
for (x = commonRect.left; x <= commonRect.right; x++)
{
long index = y * gGridWidth + x;
cellsCovered[index >> 5] &= ~(1 << (index & 0x1F));
}
}
SetRect(&commonRect, 0, 0, 32767, 32767);
// Find the min/max y position for shapes
firstY = MAX(anchorY - area + 1, 0);
for (y = anchorY - 1; y >= firstY; y--)
if (CELL_COVERED(anchorX, y))
{
firstY = y + 1;
break;
}
lastY = MIN(anchorY + area, gGridHeight) - 1;
for (y = anchorY + 1; y <= lastY; y++)
if (CELL_COVERED(anchorX, y))
{
lastY = y - 1;
break;
}
// Scan out from this centre line row by row, obtaining a series of coordinates
// defining a convex area centred on the anchor cell - those cells which can be
// occupied by a shape overlapping the anchor cell
firstX = MAX(0, anchorX - area + 1);
lastX = MIN(gGridWidth, anchorX + area) - 1;
for (y = anchorY; y >= firstY; y--)
{
// To speed this loop up, I avoid reloading cellsCovered when we are reading
// from the same long several times in succession
unsigned long theMask =
(1 << ((y * gGridWidth + anchorX - 1) & 0x1F));
unsigned long theIndex = (y * gGridWidth + anchorX - 1) >> 5;
for (x = anchorX - 1; x >= firstX; x--)
{
if (cellsCovered[theIndex] & theMask)
ASSERT(CELL_COVERED(x, y));
else
ASSERT(!CELL_COVERED(x, y));
if (cellsCovered[theIndex] & theMask)
{
firstX = x + 1;
break;
}
theMask = theMask >> 1;
if (theMask == 0)
{
theMask = 0x80000000;
theIndex--;
}
}
firstXInScanline[y] = firstX;
theMask = (1 << ((y * gGridWidth + anchorX + 1) & 0x1F));
theIndex = (y * gGridWidth + anchorX + 1) >> 5;
for (x = anchorX + 1; x <= lastX; x++)
{
if (cellsCovered[theIndex] & theMask)
ASSERT(CELL_COVERED(x, y));
else
ASSERT(!CELL_COVERED(x, y));
if (cellsCovered[theIndex] & theMask)
{
lastX = x - 1;
break;
}
theMask = theMask << 1;
if (theMask == 0)
{
theMask = 0x00000001;
theIndex++;
}
}
lastXInScanline[y] = lastX;
}
firstX = firstXInScanline[anchorY];
lastX = lastXInScanline[anchorY];
for (y = anchorY + 1; y <= lastY; y++)
{
// There is no reason not to do the same optimization as above (using theMask
// and theIndex)
// - I must have just forgotten to do it
for (x = anchorX - 1; x >= firstX; x--)
{
if (CELL_COVERED(x, y))
{
firstX = x + 1;
break;
}
}
firstXInScanline[y] = firstX;
for (x = anchorX + 1; x <= lastX; x++)
{
if (CELL_COVERED(x, y))
{
lastX = x - 1;
break;
}
}
lastXInScanline[y] = lastX;
}
GenerateShapesWithWidth(1, firstY, lastY);
if (area != 1)
GenerateShapesWithWidth(area, firstY, lastY);
// Nothing clever here - just try all possible widths to see if they result in an integer
// height
for (long width = 2; width <= area / 2; width++)
if ((area % width) == 0)
GenerateShapesWithWidth(width, firstY, lastY);
ASSERT((gShapeGenerationPhase != kGenerateShapes) || (numShapes > 0));
if ((numShapes == 1) &&
(gShapeGenerationPhase == kGenerateShapes))
{
// There is only one shape possible that covers this anchor, so place it
// immediately
Place();
}
else
{
// Fill in the anchor pointer for the cells that will be covered by every
// possible shape
// Note that the right/bottom values for the rectangle give the first cell NOT
// enclosed
for (y = commonRect.top; y <= commonRect.bottom; y++)
for (x = commonRect.left; x <= commonRect.right; x++)
MY_CELL(x, y).MustBeCoveredBy(y * gGridWidth + x, this);
}
}
void Anchor::GenerateShapesWithWidth(long width, long firstY, long lastY)
{
long x, y;
long height = area / width;
// Now we have generated the scanlines showing which cells are free, generate the.
// shapes We only need to consider the top and bottom scanlines that each shape
// overlaps, due to the convex nature of the area enclosed by the scanlines
firstY = MAX(firstY, anchorY - height + 1);
lastY = lastY - height + 1;
lastY = MIN(lastY, anchorY);
for (y = firstY; y <= lastY; y++)
{
ASSERT(firstXInScanline[y] != -1);
ASSERT(lastXInScanline[y] != -1);
ASSERT(firstXInScanline[y + height - 1] != -1);
ASSERT(lastXInScanline[y + height - 1] != -1);
long firstX = firstXInScanline[y];
MAXIMIZE(firstX, MAX(firstXInScanline[y + height - 1],
anchorX - width + 1));
long lastX = lastXInScanline[y];
MINIMIZE(lastX, lastXInScanline[y + height - 1]);
lastX = lastX - width + 1;
MINIMIZE(lastX, anchorX);
if (firstX > lastX)
continue;
// Keep track of the area which all shapes overlap
MAXIMIZE(commonRect.top, y);
MINIMIZE(commonRect.bottom, y + height - 1);
MAXIMIZE(commonRect.left, lastX);
MINIMIZE(commonRect.right, firstX + width - 1);
if (gShapeGenerationPhase == kGenerateShapes)
{
// Create the Shape records
for (x = firstX; x <= lastX; x++)
{
if (((char*)gMemPool) + oneRecordSize -
(char*)poolPtrs[gCurrentMemPoolNum] > kMemPoolSize)
{
// Need more memory
NextPool();
}
((Shape*)gMemPool)->Init(x, y, width, height, this);
gMemPool += oneRecordSize;
}
}
}
}
void Anchor::Place(Shape * shapeToPlace)
{
// Settle on a final position for this anchor
ASSERT(!placed);
placed = true;
if (numShapes > 1)
{
// Remove the other shapes for this anchor - they will not now be used
for (ShapeListItem *shapeItem = shapeListHead.next;
shapeItem->theShape != NULL; )
{
ShapeListItem *nextShapeItem = shapeItem->next;
if (shapeItem->theShape != shapeToPlace)
{
shapeItem->theShape->Reject();
}
shapeItem = nextShapeItem;
}
}
ASSERT(numShapes == 1);
// Fill in the anchor pointer for the cells that will be covered by the shape
for (long y = shapeToPlace->top;
y < shapeToPlace->top + shapeToPlace->height; y++)
for (long x = shapeToPlace->left;
x < shapeToPlace->left + shapeToPlace->width; x++)
{
MyCell *theCell = &MY_CELL(x, y);
theCell->MustBeCoveredBy(y * gGridWidth + x, this);
}
}
Boolean Anchor::TryAllShapes(void)
{
/* This function is called when all logical methods have been tried. It just uses brute
force. It tries placing all possible shapes that will cover this anchor one after
the other. It considers all the repercussions of placing that shape (recursing via
the call to DoAutomaticPlacements if necessary). The conclusion will either be
that all cells could be covered (success) or that we failed - in which case we
know that some previous placement that was made must have been incorrect.
If a given placement doesn't work, we need to restore the initial state so we can
try the next placement. To speed this up, we do not forget about rejected shapes -
we add them to the global garbage list. When restoring, we reinstate any shapes
in that garbage list.
Note that I don't bother to restore the cellsCovered bitmap - that is not needed by
the time we get to this stage */
ShapeListItem *oldGarbageList = gCurrentGarbageList;
ShapeListItem localGarbageList;
MyCell *theCell;
gCurrentGarbageList = &localGarbageList;
// Work through all shapes. The anchor keeps a list of all shapes that are based
// around it. Unfortunately the order of the list changes as shapes are removed and
// reinstated. As a result, we can't iterate through that list, and have to use the list of
// all the shapes that were ever created for the anchor.
// This could be speeded up by removing from the list any shapes that have
// DEFINITELY been rejected for good.
for (Shape *theShape = firstOfAllShapes; theShape != NULL;
theShape = theShape->nextOfAllForAnchor)
{
if (!theShape->anchorListItem.InList())
continue;
ASSERT(gCellList == NULL);
Place(theShape);
if (DoAutomaticPlacements())
return(true);
// Failed - restore current state
while (localGarbageList.next != &localGarbageList)
{
ShapeListItem *theShapeToReinstate = localGarbageList.next;
theShapeToReinstate->Remove();
theShapeToReinstate->theShape->Reinstate();
}
// Although gCellList may not be empty, it should not contain any entries we
// need to do any work for
while (gCellList != NULL)
{
theCell = gCellList;
ASSERT(theCell->overlapCount > 1);
theCell->RemoveFromHead();
}
}
// If we get here then we have tried and failed with every possible shape to cover the
// anchor. Return failure
gCurrentGarbageList = oldGarbageList;
return(false);
}
void Anchor::UpdateCommonRect(void)
{
/* This function scans through all the Anchor's Shapes to recalculate commonRect.
Note that the calls to MustBeCoveredBy may call Reject for other anchors'
shapes, causing those anchors to require a call to this function. We don't call it
immediately, though. This would lead to enormous recursion and I think would
result in more calls to this function than are strictly necessary */
Rect newCommonRect = {0, 0, 32767, 32767};
ASSERT(gCommonRectList == this);
ASSERT(inCommonRectList);
gCommonRectList = gCommonRectList->nextInCommonRectList;
inCommonRectList = false;
for (ShapeListItem *shapeItem = shapeListHead.next;
shapeItem->theShape != NULL; shapeItem = shapeItem->next)
{
Shape *theShape = shapeItem->theShape;
MAXIMIZE(newCommonRect.left, theShape->left);
MAXIMIZE(newCommonRect.top, theShape->top);
MINIMIZE(newCommonRect.right, theShape->left +
theShape->width - 1);
MINIMIZE(newCommonRect.bottom, theShape->top +
theShape->height - 1);
}
// For any cells included in newCommonRect but not in commonRect, reject any
// other shapes overlapping
// that cell, and mark their anchors for update.
long x, y;
for (y = newCommonRect.top; y < commonRect.top; y++)
for (x = newCommonRect.left; x <= newCommonRect.right; x++)
MY_CELL(x, y).MustBeCoveredBy(y * gGridWidth + x, this);
for (y = commonRect.bottom + 1; y <= newCommonRect.bottom; y++)
for (x = newCommonRect.left; x <= newCommonRect.right; x++)
MY_CELL(x, y).MustBeCoveredBy(y * gGridWidth + x, this);
for (x = newCommonRect.left; x < commonRect.left; x++)
for (y = commonRect.top; y <= commonRect.bottom; y++)
MY_CELL(x, y).MustBeCoveredBy(y * gGridWidth + x, this);
for (x = commonRect.right + 1; x <= newCommonRect.right; x++)
for (y = commonRect.top; y <= commonRect.bottom; y++)
MY_CELL(x, y).MustBeCoveredBy(y * gGridWidth + x, this);
commonRect = newCommonRect;
}
int Anchor::CompareAnchorsBySize(const void *a1, const void *a2)
{
return((*(Anchor**)a1)->area - (*(Anchor**)a2)->area);
}
DoAutomaticPlacements
Boolean DoAutomaticPlacements(void)
{
// Starting from the current state, make any placements that have to be made.
// This means checking for Cells that only one remaining shape overlaps.
// Note that there is no point for checking separately for anchors that only
// have one valid shape left - that is covered by the Cell condition.
if (!ProcessCellList())
return(false);
// If there is an unplaced anchor, try each possible shape that will cover it
// The algorithm might be improved by processing the anchors in a specific order
for (Anchor *anchor = gAnchors; anchor != NULL;
anchor = anchor->next)
{
if (!anchor->placed)
return(anchor->TryAllShapes());
}
// If we get here then we have solved the problem
return(true);
}
PruneShapes
void PruneShapes(void)
{
/* This function is very similar to Anchor::TryAllShapes. The difference is that it
doesn't recurse.
The simple checks done so far haven't eliminated some of the shapes. There may
be quite a few remaining which cannot actually be placed without causing some
cells to be uncoverable. This function eliminates those shapes. It is by far the
slowest routine in the algorithm. In most small test cases it leads to a longer
running time than if we just skipped this stage (in a few cases it nearly doubles
it). However, in cases where there are large open expanses with lots of shapes
that could cover them, it is crucial (without it no solution is found in a reasonable
amount of time). Since I can't be sure of spotting such cases in advance, I am
forced to call this routine unconditionally */
ShapeListItem *oldGarbageList = gCurrentGarbageList;
ShapeListItem localGarbageList;
Anchor *anchor;
gCurrentGarbageList = &localGarbageList;
for (anchor = gAnchors; anchor != NULL; anchor = anchor->next)
{
if (anchor->numShapes <= 1)
continue;
// Work through all shapes. The anchor keeps a list of all shapes that are based
// around it. Unfortunately the order of the list changes as shapes are removed and
// reinstated. As a result, we can't iterate through that list, and have to use the list
// of all the shapes that were ever created for the anchor
for (Shape *theShape = anchor->firstOfAllShapes;
theShape != NULL;
theShape = theShape->nextOfAllForAnchor)
{
if (!theShape->anchorListItem.InList())
continue;
ASSERT(gCellList == NULL);
anchor->Place(theShape);
if (!ProcessCellList())
{
// This shape will cause contradictions. Reject it for good.
theShape->Reject();
theShape->garbageListItem.Remove();
}
// Restore current state
while (localGarbageList.next != &localGarbageList)
{
ShapeListItem *theShapeToReinstate = localGarbageList.next;
theShapeToReinstate->Remove();
theShapeToReinstate->theShape->Reinstate();
}
// Note that unlike in Anchor::TryAllShapes there may be genuine work
// to do with the cell-list, because we may have rejected theShape for good
ProcessCellList();
}
if (anchor->numShapes == 1)
anchor->Place();
ProcessCellList();
}
gCurrentGarbageList = oldGarbageList;
}
ProcessCellList
Boolean ProcessCellList(void)
{
gAbortOnFailure = true;
gFailedFlag = false;
while (gCellList != NULL)
{
MyCell *theCell = gCellList;
if (theCell->overlapCount == 0)
{
// This round has failed because theCell cannot be covered by any remaining
// Shapes
gAbortOnFailure = false;
return(false);
}
theCell->RemoveFromHead();
if (theCell->overlapCount > 1)
continue;
Shape *theShape = theCell->shapeListHead.next->theShape;
if (theShape->itsAnchor->placed)
continue;
ASSERT(theCell->overlapCount == 1);
theShape->itsAnchor->Place(theShape);
if (gFailedFlag == true)
{
gAbortOnFailure = false;
return(false);
}
}
gAbortOnFailure = false;
return(true);
}
DoQuickSort
void DoQuickSort(Anchor *anchorList[], long first, long last)
{
if(first < last)
{
long mid = PartitionSort(anchorList, first, last);
DoQuickSort(anchorList, first, mid);
DoQuickSort(anchorList, mid + 1, last);
}
}
PartitionSort
long PartitionSort(Anchor *anchorList[], long first, long last)
{
long itemIndex = first;
long ii=first-1,
jj=last+1;
while(1)
{
do { jj--; } while ((anchorList[jj]->area -
anchorList[itemIndex]->area) > 0);
do { ii++; } while ((anchorList[ii]->area -
anchorList[itemIndex]->area) < 0);
if (ii < jj)
{
Anchor *temp = anchorList[ii];
anchorList[ii] = anchorList[jj];
anchorList[jj] = temp;
}
else
{
return(jj);
}
}
}
Area
void Area(
short *cells,
/* index by [row*rectWidth + col[ */
short rectWidth,
short rectHeight,
Rect yourRects[]
)
{
long x, y;
Anchor *anchor;
Shape *theShape;
Anchor **anchorList;
// Initialize the memory pool if this is the first time we have been called
if (gCurrentMemPoolNum == -1)
{
for (long poolIndex = 0; poolIndex < kNumMemPools;
poolIndex++)
poolPtrs[poolIndex] = NULL;
NextPool();
}
myCells = new MyCell[rectWidth * rectHeight];
if (myCells == NULL)
DebugStr("\pOut of memory");
// Allocate a bitmap indicating which cells are covered by a Shape that has been
// placed already
cellsCovered = new long[(rectWidth * rectHeight) / 32 + 1];
if (cellsCovered == NULL)
DebugStr("\pOut of memory");
memset(cellsCovered, 0,
(4 * ((rectWidth * rectHeight) / 32 + 1)));
lastXInScanline = new long[rectHeight];
firstXInScanline = new long[rectHeight];
if (lastXInScanline == NULL)
DebugStr("\pOut of memory");
if (firstXInScanline == NULL)
DebugStr("\pOut of memory");
gGridWidth = rectWidth;
gGridHeight = rectHeight;
gCurrentGarbageList = NULL;
gAnchors = NULL;
gNumAnchors = 0;
gAddToCellList = false;
gCellList = NULL;
gCommonRectList = NULL;
anchorList = new Anchor*[rectWidth * rectHeight];
if (anchorList == NULL)
DebugStr("\pOut of memory");
long i = 0;
// Create the Anchors
for (x = 0; x < rectWidth; x++)
for (y = 0; y < rectHeight; y++)
if (CELL(x, y) != 0)
{
anchorList[i] = gAnchors =
new Anchor(x, y, CELL(x, y), gAnchors);
if (anchorList[i] == NULL)
DebugStr("\pOut of memory");
i++;
}
ASSERT(i == gNumAnchors);
/* It seems generally to be worth generating the shapes starting with the largest-area
anchor. Tests with random distributions of anchor cells for a range of rectangle
layouts show that the time taken is 20-80% that taken if the anchors are processed
in the order they were found. However in a few cases performance is worse (may
take 200% of the time). This generally happens where the largest-area anchor is
in an unconstrained space significantly bigger than its area (so there are many
possible shapes it could take). A more sophisticated solution might be able to spot
such unconstrained areas and try to shrink them by processing the surrounding
anchors first.
Generating the shapes is very memory- and time-consuming. On the first few
passes I don't actually allocate the Shape structures - I just keep track of each
anchor's commonRect. This reduces the number of shapes that I actually need to
generate on the final pass. In some cases it doesn't have much effect, but in others
it may reduce the number of shapes by a factor of 10 or more. */
DoQuickSort(anchorList, 0, gNumAnchors - 1);
gShapeGenerationPhase = kBuildCommonRect;
for (i = gNumAnchors - 1; i >= 0; i--)
anchorList[i]->GenerateShapes();
gShapeGenerationPhase = kRefineCommonRect;
for (long iter = 0; iter < 4; iter++)
for (i = gNumAnchors - 1; i >= 0; i--)
anchorList[i]->GenerateShapes();
gShapeGenerationPhase = kGenerateShapes;
for (i = gNumAnchors - 1; i >= 0; i--)
anchorList[i]->GenerateShapes();
// Look for any cells that only have one shape overlapping then - and place that
// shape
ASSERT(gCellList == NULL);
gCellList = NULL;
gAddToCellList = true;
for (x = 0; x < gGridWidth; x++)
for (y = 0; y < gGridHeight; y++)
{
MyCell *theCell = &MY_CELL(x, y);
ASSERT(theCell->overlapCount != 0);
if (theCell->overlapCount == 1)
{
theShape = theCell->shapeListHead.next->theShape;
if (!theShape->itsAnchor->placed)
theShape->itsAnchor->Place(theShape);
}
}
// Update the commonRect for each anchor in turn, propagating to other anchors as
// appropriate.
while (gCommonRectList != NULL)
{
gCommonRectList->UpdateCommonRect();
ProcessCellList();
}
// Work though every shape for every anchor. Try placing it, and if that leads to a
// contradiction,
// reject that shape
PruneShapes();
while (gCommonRectList != NULL)
{
gCommonRectList->UpdateCommonRect();
ProcessCellList();
}
// Logic can do no more for us. Use brute force to finish it off
if (!DoAutomaticPlacements())
{
printf("Failed to find a solution\n");
memset(yourRects, 0, gNumAnchors * sizeof(Rect));
return;
}
// When we get here we should have solved the problem
long numRects = 0;
for (anchor = gAnchors; anchor != NULL; numRects++)
{
Anchor *nextAnchor = anchor->next;
ASSERT(anchor->placed);
theShape = anchor->shapeListHead.next->theShape;
yourRects[numRects].left = theShape->left;
yourRects[numRects].top = theShape->top;
yourRects[numRects].right = theShape->left + theShape->width;
yourRects[numRects].bottom = theShape->top + theShape->height;
delete anchor;
anchor = nextAnchor;
}
delete[] myCells;
delete[] cellsCovered;
delete[] anchorList;
delete[] lastXInScanline;
delete[] firstXInScanline;
gMemPool = poolPtrs[0];
gCurrentMemPoolNum = 0;
}

Bob Boonstra

Community Search:

MacTech Search:

Software Updates via MacUpdate

MacFamilyTree 7.3.4 - Create and explore...

MacFamilyTree gives genealogy a facelift: it's modern, interactive, incredibly fast, and easy to use. We're convinced that generations of chroniclers would have loved to trade in their genealogy... Read more

Yummy FTP 1.10.2 - FTP/SFTP/FTPS client...

Yummy FTP is an FTP + SFTP + FTPS file transfer client which focuses on speed, reliability and productivity.
Whether you need to transfer a few files or a few thousand, schedule automatic backups, or... Read more

VueScan 9.5.08 - Scanner software with a...

VueScan is a scanning program that works with most high-quality flatbed and film scanners to produce scans that have excellent color fidelity and color balance. VueScan is easy to use, and has... Read more

Iridient Developer 3.0.1 - Powerful imag...

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

Air Video Server HD 2.1.0 - Stream video...

Air Video Server HD streams videos instantly from your computer on your iPhone, iPad, iPod touch or Apple TV. No need to worry about converting or transferring files.
We took everything that was... Read more

Duplicate Annihilator 5.7.5 - Find and d...

Duplicate Annihilator takes on the time-consuming task of comparing the images in your iPhoto library using effective algorithms to make sure that no duplicate escapes.
Duplicate Annihilator... Read more

BusyContacts 1.0.2 - Fast, efficient con...

BusyContacts is a contact manager for OS X that makes creating, finding, and managing contacts faster and more efficient. It brings to contact management the same power, flexibility, and sharing... Read more

Capture One Pro 8.2.0.82 - RAW workflow...

Capture One Pro 8 is a professional RAW converter offering you ultimate image quality with accurate colors and incredible detail from more than 300 high-end cameras -- straight out of the box. It... Read more

Backblaze 4.0.0.872 - Online backup serv...

Backblaze is an online backup service designed from the ground-up for the Mac.With unlimited storage available for $5 per month, as well as a free 15-day trial, peace of mind is within reach with... Read more

Little Snitch 3.5.2 - Alerts you about o...

Little Snitch gives you control over your private outgoing data.
Track background activity As soon as your computer connects to the Internet, applications often have permission to send any... Read more

Pie In The Sky: A Pizza Odyssey 1.0
Device: iOS Universal
Category: Games
Price: $2.99, Version: 1.0 (iTunes)
Description:
A game about delivering pizza. In space.
| Read more »

Chosen Gives Hopeful Singers, Songwriter...

If YouTube videos and reality TV shows like The Voice have taught us one thing, it’s that there are a lot of people out there who are anxious to show the world their talents. And if they’ve taught us a second thing, it’s that there’s an almost... | Read more »

Android's Popular OfficeSuite Now A...

Once only available for Android devices, OfficeSuite has finally landed on the app store. The Mobile Systems app lets you view, edit, create, and share Word, Excel, and PowerPoint documents as well as convert them to/from PDFs. It's touted as being... | Read more »

Warhammer: Arcane Magic is Coming Soon,...

Turbo Tape Games has announced that they're joining forces with Games Workshop to bring the turn-based strategy board game, Warhammer: Arcane Magic, to life on the iOS.
| Read more »

Fast & Furious: Legacy's Creati...

| Read more »

N-Fusion and 505's Ember is Totally...

| Read more »

These are All the Apple Watch Apps and G...

The Apple Watch is less than a month from hitting store shelves, and once you get your hands on it you're probably going to want some apps and games to install. Fear not! We've compiled a list of all the Apple Watch apps and games we've been able to... | Read more »

Appy to Have Known You - Lee Hamlet Look...

Being at 148Apps these past 2 years has been an awesome experience that has taught me a great deal, and working with such a great team has been a privilege. Thank you to Rob Rich, and to both Rob LeFebvre and Jeff Scott before him, for helping me... | Read more »

Hands-On With Allstar Heroes - A Promisi...

Let’s get this out of the way quickly. Allstar Heroes looks a lot like a certain other recent action RPG release, but it turns out that while it’s not yet available here, Allstar Heroes has been around for much longer than that other title. Now that... | Read more »

Macho Man and Steve Austin Join the Rank...

WWE Immortals, by Warner Bros. Interactive Entertainment and WWE, has gotten a superstar update. You'll now have access to Macho Man Randy Savage and Steve Austin. Both characters have two different versions: Macho Man Randy Savage Renegade or Macho... | Read more »

Price Scanner via MacPrices.net

Adobe Brings Powerful Layout-Design Capabilit...

Adobe today announced the availability of Adobe Comp CC, a free iPad app that enables rapid creation of layout concepts for mobile, Web and print projects. With Comp CC, designers can rough out and... Read more

Apple offering refurbished 27-inch 5K iMacs f...

The Apple Store is offering Apple Certified Refurbished 27″ 3.5GHz 5K iMacs for $2119 including free shipping. Their price is $380 off the price of new models, and it’s the lowest price available for... Read more

16GB iPad mini on sale for $199, save $50

Walmart has 16GB iPad minis (1st generation) available for $199.99 on their online store, including free shipping. Their price is $50 off MSRP. Online orders only.
Read more

The Apple Store has Apple Certified Refurbished 13″ 2.6GHz/128GB Retina MacBook Pros available for $979 including free shipping. Original MSRP for this model was $1299.
Read more

Save up to $600 with Apple refurbished Mac Pr...

The Apple Store is offering Apple Certified Refurbished Mac Pros for up to $600 off the cost of new models. An Apple one-year warranty is included with each Mac Pro, and shipping is free. The... Read more

Samsung Galaxy S 6 and Galaxy S 6 edge U.S. P...

Samsung Electronics America, Inc. has announced the Galaxy S 6 and Galaxy S 6 edge will be available in the U.S. beginning April 10, with pre-orders being accepted now.
“We have completely reimagined... Read more

13-inch 2.5GHz MacBook Pro (refurbished) avai...

The Apple Store has Apple Certified Refurbished 13″ 2.5GHz MacBook Pros available for $829, or $270 off the cost of new models. Apple’s one-year warranty is standard, and shipping is free:
- 13″ 2.... Read more

MacTech is a registered trademark of Xplain Corporation. Xplain, "The journal of Apple technology", Apple Expo, Explain It, MacDev, MacDev-1, THINK Reference, NetProfessional, Apple Expo, MacTech Central, MacTech Domains, MacNews, MacForge, and the MacTutorMan are trademarks or service marks of Xplain Corporation. Sprocket is a registered trademark of eSprocket Corporation. Other trademarks and copyrights appearing in this printing or software remain the property of their respective holders. Not responsible for typographical errors.

All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.