COMPOSITION

Getting With the Program

by Charles L. Perkins

Get started in NeXTstep programming by building this easy game application

Two common myths surround object-oriented programming: The first is that it takes years of training to do it properly, the second is that object-oriented programming is no more than traditional good programming practices dressed up. Both are wrong.

Object-orientation is a new way of looking at problems, a new point of view. As such, traditional programmers can't just use it without guidance. But it doesn't take years Ð or even months Ð to begin using it.

In one sense, object-oriented programming is as old as programming itself. It codifies clever strategies hard won over the decades into a new, coherent whole. It goes beyond those strategies to suggest new ways of viewing the process of programming and of its result. (See "Object Lessons," NeXTWORLD Spring 1992, for background on object-oriented programming.)

The object-oriented approach promises to help programmers simply and quickly create programs that are easier to read, comprehend, maintain, and reuse. These promises translate into four key goals:

Object-oriented programming, though, differs in the approach it takes. Rather than requiring separate language facilities to achieve each of these goals, an object-orientated language's basic elements perform double duty, allowing programmers to restructure the way they think and program.

What is an object, anyway?
The fundamental unit of an object-oriented program is the object. Objects contain all the information about one small part of the program and all the procedures allowed to manipulate that information. This achieves part of the modularity, factoring, and encapsulation goals right away.

An object is also an instance (or member) of a class, which describes all the common behavior of its members. For example, NeXTstep objects in the class called Window display information on the NeXT computer's screen. Each window on the screen is represented by a different member, or instance, of the Window class.

Every class defines a set of messages that objects of that class can receive. Simple messages, like commands, tell objects what to do. If the programmer sends an orderOut: message to a window, for example, the window takes itself off the screen. Sending the window the message orderFront: puts the window on top of all of the other windows on the screen. Inside the object, a procedure (called a method) acts to carry out the message.

Much of an object-oriented program consists of objects sending messages to each other, and returning other objects as the result of those messages. Since all of the information is inside objects, and the only access to that information is through messages, we have both encapsulated and abstracted that information at the same time. Other objects can refer to the information by a message name, without knowing how that information is actually stored Ð or even where. This model generalizes naturally to messaging over a network, and to parallel computations.

Objective-C arranges its classes into a tree hierarchy, in which every class inherits the properties of its parent, or superclass, and is then free to make changes to the parent's behavior. For example, the NeXTstep Panel class is a subclass of the Window class. Panels are like windows except they are removed from the screen when the user clicks the mouse on another application, and they reappear on the screen when the user clicks the mouse on the Panel's application. The Panel class is itself subclassed to make the NeXTstep Menu class. Menus are like Panels, except they always float on top of all other windows, contain a set of menu cells, and can be made into submenus of other menus. Inheritance can contribute to all four goals.

Thinking like an object
Objects in the world around us are defined by their interactions Ð pull a doorknob and the door opens; drop a glass and it breaks. This simple model also underlies all object-oriented languages. If an application's goals represent tasks in the real world Ð like simulating an airplane, for example Ð then they can be naturally implemented in an object-oriented way. One class of object could represent the landing gear, another the fuel tank, and so on. When the simulation runs, the objects all interact by sending messages to each other, and the virtual airplane flies.

Surprisingly, many traditional computer applications can be thought of in the same way. Within any NeXTstep application, for example, each object that is visible on the screen Ð each object you can touch with the mouse Ð has an underlying object associated with it that "feels" your touch. When a button is pressed, a message called mouseDown: is sent to a button object, which then sends another message to its target object to carry out the requested action. It is the interaction of these active objects Ð each knowing how to display itself, modify itself, and interact with the other objects on the screen Ð that creates the whole NeXT environment.

Ergo, a game
Ergo is an implementation of a simple board game from a video arcade. The program is simple enough to be implemented quickly, yet clearly shows many of the characteristics of good object-oriented design.

Ergo is a little like Go, Othello, and Reversi. Players take turns moving a piece of their color either one or two squares at a time. A one-square move replicates the old piece, creating a new piece in the new square (see Figure 1). Two-square moves are jumps; the moving piece does not stay behind, but travels into the new square. A piece can't land on a square that is blocked or otherwise occupied (it can, however, jump over such squares). If, in its new position, the moved piece is directly adjacent to pieces of the opposite color, it captures those pieces by switching them to its color.

Figure 1 Ð The Rules of Ergo

Three legal moves are shown at left: White jumps (grey arrow), black replicates (white arrow), or black jumps and captures (black arrow). After the latter, all six pieces in the lower left quadrant would be black.

Whenever you design a new NeXT application, always start by designing the most important part: the user interface. Often, you will have a good idea of what behavior you want for your users but an unclear idea of the internal details. In addition, Interface Builder is an excellent sandbox for trying out ideas and throwing out the bad ones without wasting much effort. Interface Builder will also create and organize all the files you'll need to build and maintain your application over time, so starting there makes sense.

In this case, the interface is dictated by the game we are writing. There must be a game board (a Window) and a whole array of squares, some filled and others not (see Figure 2). To move, the user clicks the mouse in a filled square, drags it to an empty square and releases the button, changing an empty square into a filled one. Each square should "feel" the mouse and know whether or not it is filled and whether it can legally be part of the current move.

(Notice that we have already begun to somewhat personify the squares, thinking of them as separate, active participants in the game. This is a valuable way to think about the communication between the parts of most object-oriented programs.)

Figure 2 Ð Creating a User Interface for Ergo

In Interface Builder, a large Window called The Board was created, as were a whole set of new subclasses of View. These classes had no contents yet, but were created so their names could be used as labels. Next, a whole array of CustomViews were pulled off the palette, named, and placed to form the squares of the board. (Each square will compute its row and column from where it is located in the window.) A subclass of Application, called ErgoApp, was created with a single outlet for holding onto the boardWindow (shown connected above) and was then made the FileÕs Owner. Finally, Interface Builder was used to create prototype .h and .m files for all the new classes to start off their implementation.

The next step is to think about the various classes of objects we will have in the game; finding natural classes is the most important step in the design effort. If you feel uncomfortable with the choices that you've made, scrap them and start over. It's always better to begin anew than to stick with classes that don't naturally fit your problem.

Since we want to represent the whole board, and some squares on the board have no pieces, we probably want our most abstract "piece objects" to be the squares of the board rather than the pieces. This allows us to have both empty and filled squares. Some squares cannot contain pieces Ð they're blocked Ð and they form a third class of square. Finally, filled squares can contain either black or white pieces. The resulting class hierarchy is shown in Figure 3.

Figure 3 Ð The Class Hierarchy and Protocol of Ergo

Each class (box) above is titled with its class name, is connected to its superclass, and is filled with the names of its key messages (these messages appear in bold in Listings 1Ð4). All of Ergo's classes are subclasses of View or Application, standard AppKit classes. (Most of the work of Ergo has already been done for us by NeXT, in the AppKit.) Classes with the prefix Abstract store common behavior but by convention do not expect to have instances. (EmptySquare objects will appear in a running copy of Ergo, but AbstractSquare objects will not.) The distinction between messages grouped in the upper rectangle of a class and the lower one is that the upper messages are thought of as being public, available to any and all other objects, whereas the lower messages are private, and are useful only to the other objects in Ergo that directly need them. This distinction is a useful comment to later readers of your object about which messages you consider safe for common use, and which may have undocumented side effects.

Notice that to represent the squares of the game board, we've created a new class: AbstractSquare, from which all other square classes will inherit characteristics. AbstractSquare is a subclass of the NeXT AppKit class called View. As a View subclass, objects of the class AbstractSquare (or any of its subclasses) will automatically be able to receive mouse events and draw themselves in windows.

Once you have a hierarchy of classes, the next step in designing any application is to flesh it out with a protocol of key messages that the objects will use for communication and that will drive the action of the application. Look at the protocol for Ergo in Figure 3. From the density of messages, you can see that the design focuses most of the behavior in the Abstract classes, some in ErgoApp (our subclass of the AppKit's Application object), and very little in the other peripheral classes, which will mostly inherit their behavior from their superclasses. Cooperation between objects from these various classes will drive the game forward.

Mouse behavior is mostly handled by the class AbstractFilledSquare, which we would expect since game moves involve manipulating filled squares. Note that the processes of capturing, checking adjacent squares, taking turns, and distinguishing between black and white squares are all suggested even within the simple protocol of Figure 3.

Finally, we must flesh out the details of the design by implementing it. Learning the tricks and idioms of object-oriented implementation is a matter of experience, but having a natural hierarchy of classes and a sense of freedom about the patterns of message-passing that you will allow between your objects takes you far along the road. In examining the parts of the implementation presented below, you can read between the lines to get a feeling for how the design choices were made. The results will often suggest the approaches underlying them.

Details, details
When any NeXTstep application begins, the program's Application object (or in this case, the program's ErgoApp object) automatically gets sent the appDidInit: message (see Listing 1). ErgoApp's appDidInit: method sets who goes first (white) and asks all of the squares on the board to initialize themselves by sending each square the same message. This distribution of a function across a whole set of objects is a common theme in object-oriented programming, and it is done often enough to provide a general message for it: makeSquaresPerform:.

The method for makeSquaresPerform: works because every square on the board Ð every View Ð is really a subview of the master View object that displays the board's window, called the contentView. Every View keeps a List of all of its subviews; to get that List, we simply send that View the subviews message. That's what the method for viewList in ErgoApp does. Since all the Views we have in the window are squares, this will forward the message to all the squares on the board.

In general, the squares in the Ergo game communicate by broadcasting their messages to every square on the playing board; the only squares that actually do anything are those that can help advance the task at hand. For example, most squares will respond to the broadcast message appDid Init by setting a tracking rectangle for themselves (see Listing 2), but BlockedSquares (see Listing 4) will do nothing.

Setting a tracking rectangle is the way to tell the WindowServer that you want to be told when the mouse enters and exits a particular region on the screen. Tracking rectangles let a View receive mouseEntered: and mouseExited: messages in addition to the usual mouseDown: and mouseUp: messages received as the user moves and presses the mouse in the View. Since BlockedSquares never wants to receive such messages (it doesn't matter if you move the mouse into a BlockedSquare), they simply override the default appDidInit to not set a tracking rectangle.

Once the tracking rectangles have been set, the rest of the game is entirely event-driven: Only when the user moves into a View or presses the mouse will anything further happen.

Make your move
To understand the handling of a move, you should know that each square can be highlighted, and each filled square can be selected. Highlighting means that a square can participate in the current move: A square automatically highlights when you move the mouse into a valid move. Highlighting is shown visually as a band of gray just inside the square's border. Selection indicates that a square contains the piece that is trying to move during this turn. It is shown visually as a large gray "X" in the center of the square.

As the user moves the mouse, all filled squares of the right color highlight when passed over. AbstractFilledSquare's method for mouseEntered: implements this behavior (see Listing 3). As long as either the square is selected, or it is the proper color and nothing is selected, the square will highlight itself. The actual highlighting is done by AbstractSquare's method for setIsHighlighted: (Listing 2), which sets the proper state, records it globally if necessary, and then redisplays the square (self) to show the new gray band (see the method for drawSelf:). This globally recorded state, handled by our application object (NXApp), is one of only two or three small bits of state information we were unable to distribute across the squares. Listing 2 also contains the default mouseExited: implementation, which, if highlighted, unhighlights the square. Placing this here allows the two subclasses below that use mouse movement to share this one implementation.

When the user clicks on the mouse button, any non-filled squares will ignore it (since they don't implement a mouseDown: method). But properly colored filled squares will handle a mouseDown: message (Listing 3) by selecting themselves. When the setIsSelected: message is sent, another piece of globally recorded state is changed. When the mouse now moves about, filled squares will again receive mouseEntered: messages, but this time isSomeoneSelected (Listing 1) will be true, so only the originally selected filled square will highlight (allowing a player to drop the piece back on the original square).

Now we would like all legal empty squares to highlight themselves so the user can see the legal squares to move to. For this, the other implementation of mouseEntered:, in EmptySquare (Listing 4), becomes important. In that method, the empty square asks all squares to check their adjacency with itself. If one of them finds that it is adjacent and has a legal move to the empty square sending the message, it will send that empty square back the message foundAdjacent (just below in Listing 4). This, in turn, causes that empty square (where we started) to highlight itself.

To see how checkAdjacenyWith: works, we'll begin in AbstractSquare (Listing 2). Most squares simply ignore the message. However, filled squares (Listing 3) override that version to ask themselves, "Am I selected and am I two squares away (or less) from the sender?" An affirmative answer means that the filled square has a legal move to the sending empty square, and it tells the empty square so. The empty square may get many affirmative answers, but this won't matter.

The user, who is still holding down and moving the mouse, can now see all the legal moves. Eventually the user releases the mouse button, which always sends a mouseUp: message to the same object that received the mouseDown:. This has to be a filled square. Thus, the rest of the rules of the game are implemented in the method for mouseUp: (Listing 3).

Mouse up!
When the mouse is released, the square that the user wanted to move into should still be highlighted. If that square is the same square that received the mouseUp: message, we do nothing Ð this is not a move, since the user pressed and released the mouse in the same square.

Otherwise, if the EmptySquare is still highlighted, we can perform a legal move. First, we remember the class of the highlighted square and how far we are away from it. Then we ask all the squares to perform a capture: with the highlighted square (an EmptySquare), after we have made it become: the same class that we are (this makes it a filled square of our color). We'll explore capturing in a moment. If we have jumped from more than one square away, we must vacate our old location (we become: the class of the empty square we just moved to, that is, we become empty). Otherwise, we need only deselect ourselves and stay where we are (we have replicated ourselves). Finally, we ask the ErgoApp object to letOtherColorMove.

The last two details of a move are capturing and taking turns. Most squares do nothing for a capture: (Listing 2). In AbstractFilledSquare (Listing 3), though, a filled square tests whether it should be captured by the sender, a matter of being a different color (class) and of being exactly one square away. If it should be captured, it will become: the same class as the sender (change to the sender's color). Many filled squares can be captured by the same sender in this way. It is a rather simple and elegant way to express the capture rule.

Turn-taking is handled by ErgoApp (Listing 1) via the message letOtherColorMove. The implementation inverts the answer that isWhiteMove will return and simply redisplays the board. Turn-taking alternates forever, since this version of the method does not check for the end of the game (see the sidebar on "Extending Ergo").Ê

Getting With the Program

On becoming
When programming in an object-oriented language, situations frequently arise in which one object wants to literally become another object. Often the objects will simply switch identities. Our become: (Listing 2) has an interesting implementation. Since we want to preserve the "identity" of the square, we record and pass along all the state information associated with the square, but assign them to a square of an entirely different class. In an unusual move, the original object then frees itself (by sending itself the free message) and returns the newly created object in its place.

The final point of some interest is the implementation of the subclasses BlackFilledSquare and WhiteFilledSquare. Rather than having an instance-dependent variable hold the black/white distinction, we use the class hierarchy to store the information directly. For example, in class WhiteFilledSquare, the color message returns NX_WHITE explicitly; in BlackFilledSquare the same message returns NX_BLACK. One nice side effect: Classes added later can choose to return a shade of gray without changing the rest of Ergo.

Hiding an object's state behind messages may seem frivolous at first, but there is power in the abstraction: We can choose to implement color's response as a state-variable lookup, a computed value, or as a constant (as it is here). Any of these implementations would behave identically under the protocol of messages we have defined; if the way color is implemented changes, nothing would have to be changed in the other objects that use the color message. Flexibility like this, which enhances the reusability of a class, begins to demonstrate the important benefits object orientation brings to application development, maintenance, and reuse.

The winning way
Whenever you design an application, start by asking yourself how the tasks could best be broken up and distributed to a whole host of small, cooperating objects, each of which does one thing well.

Whenever you set out to design an object for your application, ask yourself what other purposes it could be used for, how best it could be extended to more fully generalize the one function needed today, and what implementation will be clean and clear enough that you will be able to reuse it in the future.

The simple classes in Ergo have each tried to live up to these standards. As a bonus, you may discover that your application itself is now general enough to be reused by your users within the larger NeXT environment. Good object-oriented design can be applied at all levels.

Object-oriented thinking can help you achieve simple, natural, and elegant solutions. The old 90/10 rule should tell you that most of your program can afford the abstraction and clarity that a high-level, object-oriented design will bring. Don't settle for the familiar or traditional; that leads to programs that are unique, fragile, and difficult to share. Use object-oriented programming for your next project, and begin to build for the future.

Charles L. Perkins, a contributing editor to NeXTWORLD, has been a registered NeXT developer for the past three years.


A 60-Second Introduction to Objective-C

Objective-C is a hybrid of the languages SmallTalk and C. It starts with full C compatibility, and then adds a new data type and a way to define classes.

The new data type is called id; it is a pointer to any kind of object, the way C's void * is a pointer to any kind of structure.

The new syntax for defining classes looks like this:

@implementation ASillyClass      
/* starts a class definition */
    
- (int) add: (int) aNumber to: (int) anotherNumber { 
  return aNumber + anotherNumber;
}
- print: (int) someNumber { 
  printf("%d\n", someNumber);
  return self;      
}  /*self is the object that was sent the message */
@end      /* ends a class definition */

Individual methods begin with a dash ("-"). The first method takes two numbers and returns the result of them added together. The second method prints the value of its argument and "returns self," a common idiom.

Use a pair of square brackets to send a message to an Objective-C object. Here is an example, using the above class:

/* make an object of class ASillyClass */
id  obj = [[ASillyClass alloc] init];      
int x=3, y=4, z;
  
z = [obj add: x to: y];
printf("%d plus %d is ", x, y);
[obj print: z];

In the first line above, a message was sent to the result of the previous message; this is a common shorthand (e.g., [[self doThis] doThat]). You can also embed Objective-C messages inside regular C expressions:
z = 14 / [obj add: x to: y];

By convention, Objective-C programmers use capital letters to begin the names of classes and use lower case letters to begin the names of messages and variables. Although there is much more to Objective-C, this small introduction should be enough to get you started reading Listings 1-4.

Listing 1 Ð ErgoApp

Each Class.m file looks the same: Import the necessary .h files, define a sequence of implementations for public messages in alphabetical order, then the private messages. Key messages in Ergo's protocol will appear in bold, while comments will appear in italics. Good object-oriented programming is convoluted by its very nature, so expect to search back and forth across these listings as you read through the tutorial. A good object-oriented environment makes following these links between methods trivial, but the linear layout of text here prevents us from doing so.

File: ErgoApp.m
#import "ErgoApp.h"
#import < appkit/View.h> 
#import < objc/List.h> 

@implementation ErgoApp

- highlightedSquare {
  return highlightedSquare;
}
- (BOOL) isSomeoneSelected {
  return isSomeoneSelected;
}
- (BOOL) isWhiteMove {
  return isWhiteMove;
}
- letOtherColorMove {
  [self setIsWhiteMove: ![self isWhiteMove]];
  return [boardWindow display];
}
- makeSquaresPerform: (SEL) aMessage {
  return [[self viewList] makeObjectsPerform:               aMessage];
}
- makeSquaresPerform: (SEL) aMessage with:                  sender {
  return [[self viewList] makeObjectsPerform:               aMessage with: sender];
}
- setHighlightedSquare: aSquare {
  highlightedSquare = aSquare;
  return self;
}
- setIsSomeoneSelected: (BOOL) state {
  isSomeoneSelected = state;
  return self;
}
- setIsWhiteMove: (BOOL) state {
  isWhiteMove = state;
  return self;
}
/* Private methods: */
- appDidInit: sender {      
  isWhiteMove = YES;
  return [self makeSquaresPerform:               @selector(appDidInit)];
}  /* our return value is ignored, so any is OK */
- viewList {
  return [[boardWindow contentView] subviews];
}
@end

Listing 2 Ð AbstractSquare

Here we show both the .h and .m files for AbstractSquare. Note that several commonly used files are imported in the .h file. This allows all subclasses of this abstract class to import them automatically without thinking about it, since each subclass will ultimately import AbstractSquare.h. Note, too, the declaration of some local state information for holding the square's row, column, etc.

These are instance variables, which can hold different values in each instance (object) of the AbstractSquare class (or any of its subclasses). AbstractSquare is the most general of the classes implementing Ergo squares.

File: AbstractSquare.h
#import "ErgoApp.h"
#import < appkit/View.h> 
#import < appkit/nextstd.h> 
#import < dpsclient/psops.h> 

@interface AbstractSquare : View {
  short  row, column;
  BOOL  isHighlighted;
  int    trackingRectNumber;
}
- (short) column;
- (short) distanceTo: otherSquare;
- drawSelf: (const NXRect *) rects : (int) rectCount;
- (BOOL) isHighlighted;
- mouseExited: (NXEvent *) theEvent;
- (short) row;
- setIsHighlighted: (BOOL) state;

/* Private methods: */
- appDidInit;
- appDidInitInside: (BOOL) state;
- become: newSquareClass;
- capture: sender;
- checkAdjacencyWith: sender;
- free;
- freeTrackingRect;
- highlight: (const NXRect *) aRect;
- initFrame: (const NXRect *) frameRect;
- setTrackingRect: (const NXRect *) aRect inside: (BOOL) state;

@end

File: AbstractSquare.m
#import "AbstractSquare.h"

@implementation AbstractSquare

- (short) column {
  return column;
}
- (short) distanceTo: otherSquare {
  return MAX  (ABS([self row ]    
               - [otherSquare row ]),
        ABS([self column]  
               - [otherSquare column]));
}
- drawSelf: (const NXRect *) rects:(int) rectCount {
  [super drawSelf: rects : rectCount];
  NXDrawGrayBezel(& bounds, rects);
  if ([self isHighlighted])
    [self highlight: rects];
  return self;
}
- (BOOL) isHighlighted {
  return isHighlighted;
}
- mouseExited: (NXEvent *) theEvent {
  if ([self isHighlighted])
    [self setIsHighlighted: NO];
  return self;
}
- (short) row {
  return row;
}
- setIsHighlighted: (BOOL) state {
  if (isHighlighted = state)
    [NXApp setHighlightedSquare: self];
  return [self display];
}
/* Private methods: */
- appDidInit {
  return [self appDidInitInside: NO];
}
- appDidInitInside: (BOOL) state {
  return [self setTrackingRect: & frame inside:               state];
}
- become: newSquareClass {
  BOOL wasHighlighted = [self isHighlighted];
  id    theSuperView = [self superview];
  id    newSquare = [[newSquareClass alloc]   
          initFrame: & frame];

  [[[self freeTrackingRect] 
          removeFromSuperview] free];
  [theSuperView addSubview: newSquare];
  return [newSquare appDidInitInside: 
          wasHighlighted];
}
- capture: sender {
  return self;
}
- checkAdjacencyWith: sender {
  return self;
}
- free {
  if ([self isHighlighted])
    [NXApp setHighlightedSquare: nil];
  return [super free];
}
- freeTrackingRect {
  if (trackingRectNumber > 0)
    [[self window] discardTrackingRect: 
          trackingRectNumber];
  return self;
}
#define INSET      2
#define WIDTH      7

- highlight: (const NXRect *) aRect {
  NXRect    theRect = *aRect;

  NXInsetRect(& theRect, INSET, INSET);
  PSsetgray(NX_DKGRAY);
  NXFrameRectWithWidth(& theRect, WIDTH);
  return self;
}
- initFrame: (const NXRect *) frameRect {
  [super initFrame: frameRect];      
  /* the below only works with small borders */
  row      = frameRect-> origin.y /  
          frameRect-> size.height;      
  column   = frameRect-> origin.x /  
          frameRect-> size.width;
  return self;
}
- setTrackingRect: (const NXRect *) aRect inside: (BOOL) state {
  static int    rectNumber = 1;
  NXRect    winRect = *aRect;

  trackingRectNumber = rectNumber;
  [[self superview] convertRect: & winRect               toView: nil];
  [[self window] setTrackingRect: & winRect
          inside: state /* cursor is in/out */      
        owner: self tag: rectNumber++               left: NO right: NO];
  return self;
}
@end

Listing 3 Ð AbstractFilledSquare

This class is an important one. Most mouse-event-driven messages are dispatched into this class, and all legal moves begin here.

File: AbstractFilledSquare.m
#import "AbstractFilledSquare.h"
#import "EmptySquare.h"      
/* for foundAdajacent message declaration */

@implementation AbstractFilledSquare

- (BOOL) acceptsFirstMouse {
  return YES;
}
- (float) color {      
  return [self subclassResponsibility: _cmd], 0; 
}          /* ", 0" for -Wall */
#define FRACTION  0.6

- drawSelf: (const NXRect *) rects : (int) rectCount {
  double  cx = NX_MIDX(& bounds), 
      cy = NX_MIDY(& bounds);

  [super drawSelf: rects : rectCount];
  PSsetgray([self color]);
  PSarc(cx, cy, FRACTION * 
           bounds.size.width / 2, 0, 360);
  PSfill( );
  if ([self isSelected])
    [self select: cx : cy];
  return self;
}
- (BOOL) isMyMove {
  return [NXApp isWhiteMove] == [self isWhite];
}
- (BOOL) isSelected {
  return isSelected;
}
- (BOOL) isWhite {
  return [self subclassResponsibility: _cmd], NO;
}
- mouseDown: (NXEvent *) theEvent {
  if ([self isMyMove])
    [self setIsSelected: YES];
  return self;
}
- mouseEntered: (NXEvent *) theEvent {    
  if ([self isSelected] || ([self isMyMove]            & &  ![NXApp isSomeoneSelected]))
    [self setIsHighlighted: YES];
  return self;
}  /* above, could check if ANY move is legal */
- mouseUp: (NXEvent *) theEvent {
  id  emptySquare = [NXApp highlightedSquare]; 
  
  if (self != emptySquare & &  [self isSelected]      
        & &  [emptySquare isHighlighted]) {
    id  emptyClass = [emptySquare class];
    short  jump = [self distanceTo: 
              emptySquare];

    [NXApp makeSquaresPerform:                 @selector(capture:)
          with: [emptySquare become:  
            [self class]]];
    if (jump >  1)
      [self become: emptyClass];
    else
      [self setIsSelected: NO];
    return [NXApp letOtherColorMove];  
  }  /* above, hope ANY return value is OK */
  return [self setIsSelected: NO];
}
- setIsSelected: (BOOL) state {
  [NXApp setIsSomeoneSelected: 
          isSelected = state];
  return [self display];
}
/* Private methods: */
- capture: sender {
  if ([self class] != [sender class] 
        & &  [self distanceTo: sender] == 1)
    return [self become: [sender class]];
  return self;
}
- checkAdjacencyWith: sender {      
  if ([self isSelected] & &  [self distanceTo: sender]           < = 2)
    [sender foundAdjacent];
  return self;
}  /* can extend above for a toroidal board */
- free {
  if ([self isSelected])
    [NXApp setIsSomeoneSelected: NO];
  return [super free];
}
/* below should really be a % of width */
#define X_OFFSET  9      
#define X_WIDTH  4

- select: (double) cx : (double) cy {
  PSsetgray(NX_DKGRAY);
  PSsetlinewidth(X_WIDTH);
  PSmoveto(  cx -   X_OFFSET,  cy +  X_OFFSET   );
  PSlineto(  cx +  X_OFFSET,  cy -   X_OFFSET   );
  PSmoveto(  cx +  X_OFFSET,  cy +  X_OFFSET   );
  PSlineto(  cx -   X_OFFSET,  cy -   X_OFFSET   );
  PSstroke( );
  return self;
}
@end

Listing 4 Ð The Peripheral Classes

Here are all the peripheral classes that implement Ergo squares. They inherit almost all their behavior from their superclasses, and are the real examples of the power of inheritance. Almost nothing needs to be done in their implementations; the class hierarchy of Ergo does all the work.

File: EmptySquare.m
#import "EmptySquare.h"

@implementation EmptySquare

- mouseEntered: (NXEvent *) theEvent {
  [NXApp makeSquaresPerform:             @selector(checkAdjacencyWith:)
           with: self];
  return self;
}  /* above, don't do return [...]; someone may               depend on return self */

/* Private methods: */
- foundAdjacent {  
  return [self setIsHighlighted: YES];
}  /* we were found to be adjacent by the above */
@end

File: BlockedSquare.m
#import "BlockedSquare.h"

@implementation BlockedSquare

#define FRACTION    0.5
#define BORDER    ((1 - FRACTION) / 2)

- drawSelf: (const NXRect *) rects : (int) rectCount {
  NXRect block = {  bounds.origin.x + BORDER 
            * bounds.size.width,
          bounds.origin.y + BORDER 
            * bounds.size.height,
        FRACTION   * bounds.size.width,
        FRACTION   * bounds.size.height };

  [super drawSelf: rects : rectCount];
  PSsetgray(NX_DKGRAY);
  NXRectFill(&block);
  return self;
}
/* Private methods: */
- appDidInit {
  return self;      
}  /* does nothing (sets no tracking rectangle) */
@end

File: BlackFilledSquare.m
#import "BlackFilledSquare.h"

@implementation BlackFilledSquare

- (float) color {
  return NX_BLACK;
}
- (BOOL) isWhite {
  return NO;
}
@end

File: WhiteFilledSquare.m
#import "WhiteFilledSquare.h"

@implementation WhiteFilledSquare

- (float) color {
  return NX_WHITE;
}
- (BOOL) isWhite {
  return YES;
}
@end

Extending Ergo, or How to Get Your Own Copy

Face it: Ergo needs work. The game should display the number of white- and black-filled squares after each move, detect the end of the game, have a clock to limit the total time taken by each player, have sound effects and music, and more. Each of these extensions is extremely easy to add to the existing structure of Ergo, if you think in an object-oriented way.

Some interesting extensions to the game have already been included in the versions of Ergo you can get below. These include an Interface Builder palette-based version of Ergo, in which new boards can be created in seconds from a palette of pieces, and a version that allows multiple boards to be loaded at once. All the versions of Ergo below come with additional comments and the full sources (all the .m, .h, .nib, etc. files).

If you have ftp access to the Internet, Ergo is available in all of the main archive sites (cs.orst.edu and sonata.cc.purdue.edu are two). If you have only e-mail access, send a message with the subject "help" to the address archive-server@cc.purdue.edu and follow the instructions mailed back. If this fails, send a message to ergo@nextworld.com asking for Ergo and we will send you a copy. Floppy disks will be available on a limited basis: Write to Ergo Floppy Offer, NeXTWORLD Magazine, 501 Second St., San Francisco, CA 94107. If all else fails, you can type in the source code included in Listings 1Ð4. It is a complete listing of Ergo (except for a few trivial .h files that are mirrors of the .m files). Use Figure 2 to help you create the .nib file.