ftp.nice.ch/Attic/openStep/games/Empire.0.6.m.NIS.bs.tgz#/Empire.0.6/src/GameManager.m

This is GameManager.m in view mode; [Download] [Up]

//
// $Id: GameManager.m,v 1.21 1997/10/31 08:35:27 nygard Exp $
//

//
//  This file is a part of Empire, a game of exploration and conquest.
//  Copyright (C) 1996  Steve Nygard
//
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation; either version 2 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program; if not, write to the Free Software
//  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//
//  You may contact the author by:
//     e-mail:  nygard@telusplanet.net
//

#import "Empire.h"

RCSID ("$Id: GameManager.m,v 1.21 1997/10/31 08:35:27 nygard Exp $");

#import "City.h"
#import "EmPlayer.h"
#import "EmpireProtocols.h"
#import "GameManager.h"
#import "Human.subproj/Human.h"
#import "Map.h"
#import "ShipReportController.h"
#import "SNRandom.h"
#import "StatusController.h"
#import "Unit.h"
#import "WarReportController.h"
#import "World.h"

#define AGSReason(state1) [NSString stringWithFormat:@"Current game state is (%@).  Expected game state to be %@.", NSStringFromGameState (gameState), NSStringFromGameState (state1)]

#define AssertGameState(state1) NSAssert1 (gameState == state1, @"%@", AGSReason (state1))
#define AssertNotGameState(state1) NSAssert1 (gameState != state1, @"%@", AGSReason (state1))

//----------------------------------------------------------------------

NSString *NSStringFromGameState (GameState state)
{
    NSString *stateNames[] = { @"No Game", @"Establishing Game", @"Client Active",
                               @"Player 1 Turn", @"Player 2 Turn", @"Player 3 Turn", @"Game Over" };
    NSString *name;

    name = nil;
    if (state <= gs_game_over)
        name = stateNames[state];

    return name;
}

//======================================================================

#define GameManager_VERSION 1

@implementation GameManager

+ (void) initialize
{
    if (self == [GameManager class])
    {
        [self setVersion:GameManager_VERSION];
    }
}

//----------------------------------------------------------------------

- init
{
    [super init];

    berserk = NO;

    world = nil;

    activePlayerCount = 0;
    players[p_neutral] = nil;
    players[p_player1] = nil;
    players[p_player2] = nil;
    players[p_player3] = nil;

    playersActive[p_neutral] = NO;
    playersActive[p_player1] = NO;
    playersActive[p_player2] = NO;
    playersActive[p_player3] = NO;

    currentTurn = 0;
    gameState = gs_no_game;

    awaitingCookie = nil;

    attackingUnit = nil;
    defendingUnit = nil;
    defendingCity = nil;

    finalMaps[0] = nil;
    finalMaps[1] = nil;
    finalMaps[2] = nil;
    finalMaps[3] = nil;

    gameStatusController = [[StatusController alloc] initWithGameManager:self];
    [gameStatusController showPanel];

    return self;
}

//----------------------------------------------------------------------

- (void) dealloc
{
    Player p;

    //NSLog (@"dealloc'ing GameManager.");

    //[self logStatus];

    [self stopGame];

    SNRelease (world);
    SNRelease (awaitingCookie);

    for (p = p_neutral; p <= p_player3; p++)
    {
        SNRelease (players[p]);
        SNRelease (finalMaps[p]);
    }

    SNRelease (gameStatusController);
    SNRelease (warReportController);
    SNRelease (shipReportController);

    [super dealloc];
}
#if 0
- retain
{
    int retainCount = [self retainCount];
    
    NSLog (@"retaining GM from %d to %d", retainCount, retainCount + 1);
    return [super retain];
}

- autorelease
{
    DSTART;
    return [super autorelease];
}

- (void) release
{
    int retainCount = [self retainCount];
    
    NSLog (@"releasing GM from %d to %d", retainCount, retainCount - 1);
    [super release];
}
#endif
//======================================================================
// Interface Managment
//======================================================================

- (void) showWarReport:sender forPlayer:(EmPlayer *)player
{
    if (warReportController == nil)
        warReportController = [[WarReportController alloc] init];

    [warReportController warReportForPlayer:player];
}

//----------------------------------------------------------------------

- (void) showShipReport:sender forPlayer:(EmPlayer *)player
{
    if (shipReportController == nil)
        shipReportController = [[ShipReportController alloc] init];

    [shipReportController shipReportForPlayer:player];
}

//======================================================================
// Debugging
//======================================================================

// The "Crystal Ball" was part of my early development.  It kept an
// accurate map of the real positions of all units.  I haven't put it
// back since the distributed games have complicated things.

- (void) showCrystalBall:sender
{
}

//----------------------------------------------------------------------

- (void) removeCrystalBall:sender
{
}

//----------------------------------------------------------------------

- (void) setBerserk:(BOOL)flag
{
    berserk = flag;
}

//----------------------------------------------------------------------

- (BOOL) isBerserk
{
    return berserk;
}

//----------------------------------------------------------------------

- (void) toggleBerserk:sender
{
    berserk = (berserk == YES) ? NO : YES;
    [sender setTitle:(berserk == YES) ? @"Turn Berserk OFF" : @"Turn Berserk ON"];
}

//======================================================================
// Menu Actions
//======================================================================

//======================================================================
// World Information
//======================================================================

- (int) worldCityCount
{
    NSAssert (world != nil, @"No active world.");
    return [world worldCityCount];
}

//----------------------------------------------------------------------

- (City *) randomNeutralCity
{
    NSAssert (world != nil, @"No active world.");

    return [world randomNeutralCity];
}


//======================================================================
// Establish Game
//======================================================================

- (void) startGameWithMapNamed:(NSString *)mapName
{
    AssertGameState (gs_no_game);
    NSAssert (world == nil, @"There is already an active world.");

    world = [[World alloc] initWithMapNamed:mapName];
    NSAssert (world != nil, @"Could not create world.");

    [self setGameState:gs_establishing_game];
}

//----------------------------------------------------------------------

- (BOOL) addPlayer:(Player)number name:(NSString *)name type:(NSString *)playerType withEfficiencies:(int)pe:(int)ce
{
    City *city;

    NSAssert (number >= p_player1 && number <= p_player3
              && players[number] == nil && playersActive[number] == NO, @"Invalid player.");
    AssertGameState (gs_establishing_game);
    NSAssert (world != nil, @"No active world.");

    //NSLog (@"pe: %d, ce: %d", pe, ce);

    city = [self randomNeutralCity];
    NSAssert (city != nil, @"No more neutral cities.");

    players[number] = [[NSClassFromString (playerType) alloc] initWithName:name
                                                              number:number
                                                              world:[world worldMap]
                                                              capital:city
                                                              withEfficiencies:pe:ce
                                                              gameManager:self];
    playersActive[number] = YES;
    activePlayerCount++;

    [[NSNotificationCenter defaultCenter] postNotificationName:EMPlayerStateChangedNotification
                                          object:self];

    //NSLog (@"player:%d, map:%08lx", number, [players[number] map]);

    return YES;
}

//----------------------------------------------------------------------

- (void) beginGame
{
    AssertGameState (gs_establishing_game);

    //gameState = gs_player1_turn;

    NSAssert (awaitingCookie == nil, @"Expected nil cookie.");
    awaitingCookie = [[NSNumber numberWithUnsignedLong:42] retain];

    // Start next turn...
    [self tryToStart];
}

//----------------------------------------------------------------------

- (void) tryToStart
{
    AssertGameState (gs_establishing_game);

    [self turnDone:awaitingCookie];
}

//----------------------------------------------------------------------

// Server
- (void) stopGame
{
    int activePlayers;
    Player p;
    
    // What about:
    //   war_report_panel
    //   ship_report_panel
    //   world_map_panel
    //   wizard_window
    //   wizard_controller

    // Notify players that the game has been stopped.

    if (gameState == gs_client_active)
        return;

    activePlayers = [self activePlayers];

    for (p = p_player1; p <= p_player3; p++)
    {
        if (playersActive[p] == YES)
            [self storeFinalMapForPlayer:p];
    }

    for (p = p_player1; p <= p_player3; p++)
    {
        if (playersActive[p] == YES)
        {
            [self gameHasStopped:p activePlayers:activePlayers];
        }
    }
}

//----------------------------------------------------------------------

- (void) gameHasStopped:(Player)number activePlayers:(int)activePlayers
{
    NSAssert (players[number] != nil, @"We don't have that player.");

    [players[number] gameStopped:activePlayers];
    [self deactivatePlayer:number];
}

//----------------------------------------------------------------------

- (void) gameOver
{
}

//======================================================================
// Game State
//======================================================================

- (int) turnNumber
{
#if 0
    NSAssert (gameState == gs_player1_turn
              || gameState == gs_player2_turn
              || gameState == gs_player3_turn, @"No game in progress.");
#endif
    return currentTurn;
}

//----------------------------------------------------------------------

- (BOOL) gameInProgress
{
    return gameState != gs_no_game;
}

//======================================================================
// Turn Handling
//======================================================================

- (Player) nextPlayerTurn
{
    Player nextPlayer;

    AssertNotGameState (gs_no_game);

    switch (gameState)
    {
      case gs_player3_turn:
          currentTurn++;
          [[NSNotificationCenter defaultCenter] postNotificationName:EMTurnChangedNotification
                                                object:self];
      case gs_establishing_game:
          [self setGameState:gs_player1_turn];
          nextPlayer = p_player1;
          break;
          
      case gs_player1_turn:
          [self setGameState:gs_player2_turn];
          nextPlayer = p_player2;
          break;
          
      case gs_player2_turn:
          [self setGameState:gs_player3_turn];
          nextPlayer = p_player3;
          break;

      default:
          nextPlayer = p_neutral;
          NSLog (@"Invalid game state to end turn.");
    }

    //NSLog (@"The next player is %d", nextPlayer);

    return nextPlayer;
}

//----------------------------------------------------------------------
// Note: This is not called by the DistributedGameManger subclass.
//----------------------------------------------------------------------

- (void) turnDone:(NSNumber *)aCookie
{
    Player nextPlayer;
    int limit = 3;
    
    if ([awaitingCookie isEqual:aCookie] == NO)
    {
        //NSLog (@"awaiting: %@, got: %@", awaitingCookie, aCookie);
        NSLog (@"[1] Invalid cookie!");
        return;
    }

    NSAssert (activePlayerCount > 0, @"No active players.");

    [awaitingCookie release];
    awaitingCookie = [[NSNumber numberWithUnsignedLong:[[SNRandom instance] randomNumber]] retain];
#if 0
    NSLog (@"active player count: %d", activePlayerCount);
    NSLog (@"playersActive: %@ %@ %@ %@",
           playersActive[p_neutral] == YES ? @"Yes" : @"No",
           playersActive[p_player1] == YES ? @"Yes" : @"No",
           playersActive[p_player2] == YES ? @"Yes" : @"No",
           playersActive[p_player3] == YES ? @"Yes" : @"No");
#endif
    do
    {
        nextPlayer = [self nextPlayerTurn];
    }
    while (limit-- > 0 && (players[nextPlayer] == nil || playersActive[nextPlayer] == NO));

    if (playersActive[nextPlayer] == YES && players[nextPlayer] != nil)
    {
        [players[nextPlayer] yourTurn:currentTurn withCookie:awaitingCookie];
    }
    else
    {
        NSLog (@"No active players.");
    }
}

//======================================================================
// Movement and combat
//======================================================================

//----------------------------------------------------------------------
// This is a rather large method, but is probably the simplest way of
// doing what needs to be done.
//
// We check for the most specific cases first.
//----------------------------------------------------------------------

- (MoveResult) moveUnit:(Unit *)thisUnit inDirection:(Direction)dir
{
    static const int row_deltas[9] =    { -1, -1, -1,  0,  0,  0,  1,  1,  1 };
    static const int column_deltas[9] = { -1,  0,  1, -1,  0,  1, -1,  0,  1 };

    EMMapSize mapSize;
    EMMapLocation thisLocation = [thisUnit unitLocation];
    EMMapLocation targetLocation;

    EmPlayer *thisOwner = [thisUnit owner];
    Player thisPlayer = [thisOwner playerNumber];
    UnitType thisUnitType = [thisUnit unitType];

    MapToken targetToken;
    Terrain targetTerrain;
    Player targetPlayer;
    Icon targetIcon;
    UnitType targetUnitType;

    BOOL moveUnit = NO;
    BOOL tookTurn = NO;
    MoveResult moveResult;

    BOOL targetIsFoe;

    if ([thisUnit currentRange] <= 0)
        return mr_zero_range;

    NSAssert (world != nil, @"No active world.");
    mapSize = [[world worldMap] mapSize];

    targetLocation.row = thisLocation.row + row_deltas[dir];
    targetLocation.column = thisLocation.column + column_deltas[dir];

    //
    // Make sure we're not trying to move off of the map.
    //

    if (targetLocation.row < 0 || targetLocation.column < 0
        || targetLocation.row >= mapSize.height || targetLocation.column >= mapSize.width)
    {
        return mr_blocked;
    }

    //
    // Check terrain in target location
    //

    targetToken = [self canonTokenAtLocation:targetLocation];
    EMMapTokenComponents (targetToken, &targetTerrain, &targetPlayer, &targetIcon, NULL);
    targetUnitType = EMConvertIconToUnitType (targetIcon);

    //printf ("target_icon: %d, target_player: %d\n", target_icon, target_player);

    // No!  It could be a Unit in a remote process.  We'll let the central
    // GameManager arbitrate combat, and let the interested parties know the
    // results...

    targetIsFoe = targetPlayer != thisPlayer;

    if (targetTerrain == t_land && [thisUnit canBombard] == YES && targetIsFoe == YES
        && targetUnitType == u_army)
    {
        //NSLog (@"Ship(%@) is bombarding army.", thisUnit);

        moveResult = [self unit:thisUnit
                           attacksPlayer:targetPlayer
                           unitAtLocation:targetLocation
                           withBombardment:YES];
        tookTurn = YES;
        moveUnit = NO;
    }
    else if (targetTerrain == t_water && thisUnitType == u_army && targetIsFoe == YES
             && targetIcon != i_none && targetUnitType != u_submarine)
    {
        //NSLog (@"Army(%@) is attacking enemy on water.", thisUnit);

        moveResult = [self unit:thisUnit
                           attacksPlayer:targetPlayer
                           unitAtLocation:targetLocation
                           withBombardment:NO];
        tookTurn = YES;
        moveUnit = NO;
    }
    else if (targetUnitType == u_transport && targetIsFoe == NO && thisUnitType == u_army)
    {
        Unit *transport = [thisOwner primaryUnitAtLocation:targetLocation];

        //NSLog (@"Army(%@) is trying to board friendly transport.", thisUnit);
        //NSLog (@"transport(%@): %@", [transport unitName], transport);

        //if ([transport loadUnit:thisUnit] == YES)
        // The unit will be loaded on the transport when it is moved.  Here, we just
        // check if it will fit.
        
        if ([transport isFull] == NO)
        {
            moveResult = mr_moved;
            tookTurn = YES;
            moveUnit = YES;
        }
        else
        {
            moveResult = mr_blocked;
            tookTurn = NO;
            moveUnit = NO;
        }
    }
    else if (targetUnitType == u_carrier && targetIsFoe == NO && thisUnitType == u_fighter)
    {
        Unit *carrier = [thisOwner primaryUnitAtLocation:targetLocation];

        //NSLog (@"Fighter(%@) is trying to board friendly carrier.", thisUnit);
        //NSLog (@"Carrier(%@): %@", [carrier unitName], carrier);

        //if ([carrier loadUnit:thisUnit] == YES)
        if ([carrier isFull] == NO)
        {
            moveResult = mr_moved;
            tookTurn = YES;
            moveUnit = YES;

            // Clear remaing moves for this turn.
            [thisUnit skipMove];
        }
        else
        {
            moveResult = mr_blocked;
            tookTurn = NO;
            moveUnit = NO;
        }
    }
    else if (targetIcon != i_none && targetIsFoe == NO)
    {
        //NSLog (@"Unit(%@) is blocked by a friendly unit.", thisUnit);

        moveResult = mr_blocked;
        tookTurn = NO;
        moveUnit = NO;
    }
    else if (targetTerrain == t_city && targetIsFoe == YES && [thisUnit unitType] == u_army)
    {
        //NSLog (@"Army(%@) is attacking enemy city.", thisUnit);

        moveResult = [self unit:thisUnit
                           attacksPlayer:targetPlayer
                           cityAtLocation:targetLocation];
        //NSLog (@"Move result: %d", moveResult );
        tookTurn = YES;
        //moveUnit = moveResult == mr_victory;
        moveUnit = NO;
        if (moveResult == mr_victory)
        {
            [thisOwner destroyUnit:thisUnit wasDisbanded:YES];
            moveResult = mr_captured_city;
            moveUnit = YES; // Needed to remove old icon.
        }
    }
    else if (targetTerrain != t_city && [thisUnit canMoveOnTerrain:targetTerrain] == YES
             && targetIcon != i_none && targetIsFoe == YES)
    {
        // Attack enemy unit.
        //NSLog (@"Unit(%@) is attacking enemy unit.", thisUnit);

        moveResult = [self unit:thisUnit
                           attacksPlayer:targetPlayer
                           unitAtLocation:targetLocation
                           withBombardment:NO];
        tookTurn = YES;
        moveUnit = moveResult == mr_victory;
    }
    else if (targetTerrain == t_city && targetIsFoe == NO)
    {
        //NSLog (@"Unit(%@) is entering friendly city.", thisUnit);

        // Clear remaing moves for this turn.
        [thisUnit skipMove];

        moveResult = mr_moved;
        tookTurn = YES;
        moveUnit = YES;
    }
    else if ([thisUnit canMoveOnTerrain:targetTerrain] == YES)
    {
        //NSLog (@"Unit(%@) is NOT blocked by terrain.", thisUnit);

        moveResult = mr_moved;
        tookTurn = YES;
        moveUnit = YES;
    }
    else
    {
        //NSLog (@"Unit(%@) is blocked by terrain.", thisUnit);

        moveResult = mr_blocked;
        tookTurn = NO;
        moveUnit = NO;
    }

    // Player still needs map updated...

    if (tookTurn == YES)
    {
        //NSLog (@"Took turn.");
        [thisUnit moved];
    }

    //NSLog (@"thisUnit: %@, destroyed: %@\n", thisUnit, ([thisUnit hasBeenDestroyed] == YES) ? @"Yes" : @"No");

    // We need to remove the enemy icon first.
    if (moveResult == mr_victory && targetTerrain != t_city)
    {
        // Remove enemy unit from enemy map.

        //NSLog (@"Remove enemy unit from enemy map.");

        [self remove:targetIcon atLocation:targetLocation forPlayer:targetPlayer];
#if 0
        //NSLog (@"target player: %d, (%d,%d)", targetPlayer, targetLocation.row, targetLocation.column);
        [self explorePlayer:targetPlayer around3x3Location:targetLocation updateOtherPlayers:YES];
#endif
    }

    if (moveResult == mr_captured_city && targetTerrain == t_city)
    {
        //NSLog (@"Remove enemy city from enemy(%d) map.", targetPlayer);

        if (targetPlayer != p_neutral)
        {
            //[self setCityAtLocation:targetLocation toPlayer:p_neutral forPlayer:targetPlayer];
            [self setCityAtLocation:targetLocation toPlayer:thisPlayer forPlayer:targetPlayer];
        }

        //NSLog (@"Put our city in our map.");
        [self setCityAtLocation:targetLocation toPlayer:thisPlayer forPlayer:thisPlayer];
    }

    if (moveResult == mr_destroyed)
    {
        EMMapLocation location = [thisUnit unitLocation];

        //NSLog (@"Remove our unit from our map.");

        [self remove:[thisUnit icon] atLocation:location forPlayer:thisPlayer];
    }

    if (moveUnit == YES)
    {
        //Map *thisMap = [thisOwner map];

        //NSLog (@"Move unit.");
#if 0
        NSLog (@"Remove %d @ (%d,%d) [%@]", [thisUnit icon], thisLocation.row, thisLocation.column,
               EMFormatComponents ([thisMap tokenAtLocation:thisLocation]));
        NSLog (@"Put %d @ (%d,%d) [%@]", [thisUnit icon], targetLocation.row, targetLocation.column,
               EMFormatComponents ([thisMap tokenAtLocation:targetLocation]));
        NSLog (@"Now @ (%d,%d) [%@]", targetLocation.row, targetLocation.column,
               EMFormatComponents ([thisMap tokenAtLocation:targetLocation]));
#endif
        [self remove:[thisUnit icon] atLocation:thisLocation forPlayer:thisPlayer];
        [self put:[thisUnit icon] atLocation:targetLocation forPlayer:thisPlayer];
        [thisUnit setLocation:targetLocation];
    }

    if (moveResult == mr_victory || moveResult == mr_captured_city || moveResult == mr_destroyed || moveUnit == YES)
    {
        //NSLog (@"Update players.");

        [self explorePlayer:thisPlayer around3x3Location:thisLocation updateOtherPlayers:YES];
        [self explorePlayer:thisPlayer around3x3Location:targetLocation updateOtherPlayers:YES];
    }

    if (thisUnitType == u_fighter && [thisUnit remainingFuel] <= 0 && [thisUnit isInCity] == NO
        && [thisUnit isOnBoardShip] == NO) // Only for fighter
    {
        EMMapLocation location = [thisUnit unitLocation];

        //NSLog (@"Fighter ran out of fuel.");
        
        //NSLog (@"Fighter @ (%d,%d), target: (%d,%d)", location.row, location.column, targetLocation.row, targetLocation.column);

        // destroy unit
        [[thisUnit owner] destroyUnit:thisUnit wasDisbanded:NO];
        moveResult = mr_destroyed;

        [self showExplosions:5 forPlayer:thisPlayer atLocation:location];

        // Remove icon from map & update everyone.
        // Client game manager should have destroyed the unit.

        [self remove:[thisUnit icon] atLocation:location forPlayer:thisPlayer];
        [self explorePlayer:thisPlayer around3x3Location:location updateOtherPlayers:YES];
    }

    // Check for end of player/game at end of turn. Nah.

    if (moveResult == mr_captured_city)
    {
        BOOL gameOver;

        //NSLog (@"Checking for the end of player %d", targetPlayer);

        // Have defender check whether that was the last city.
        gameOver = [self checkForEndOfPlayer:targetPlayer];
    }

    return moveResult;
}

//======================================================================
// Etc.
//======================================================================

//======================================================================
// Map access/management
//======================================================================

//----------------------------------------------------------------------
// By having the Distributed Game Manager (DGM) implement these
// methods, some of the methods in this class don't need to be
// reimplemented in the DGM.  This is especially important for the
// methods that have complex logic.
//----------------------------------------------------------------------

// Needs to be overridden in DGM
- (Map *) mapForPlayer:(Player)number
{
    Map *map = nil;

    if (players[number] != nil)
        map = [players[number] map];

    return map;
}

//----------------------------------------------------------------------

- (Map *) fetchMapForPlayer:(Player)number
{
    return [self mapForPlayer:number];
}

//----------------------------------------------------------------------

// Needs to be overridden in DGM
- (void) set3x3Tokens:(MapToken *)tokens aroundLocation:(EMMapLocation)target forPlayer:(Player)number
{
    Map *map = [self mapForPlayer:number];

    NSAssert (map != nil, @"Player map was nil");

    [map set3x3Tokens:tokens aroundLocation:target];
    // In DGM, also set original copy...
}

//----------------------------------------------------------------------

- (void) remove:(Icon)icon atLocation:(EMMapLocation)target forPlayer:(Player)number
{
    Map *map = [self mapForPlayer:number];

    NSAssert (map != nil, @"Map was nil.");

    //NSLog (@"remove icon %d at %d,%d for player %d", icon, target.row, target.column, number);
    [map remove:icon atLocation:target];
}

//----------------------------------------------------------------------

- (void) put:(Icon)icon atLocation:(EMMapLocation)target forPlayer:(Player)number
{
    Map *map = [self mapForPlayer:number];

    NSAssert (map != nil, @"Map was nil.");

    //NSLog (@"put icon %d at %d,%d for player %d", icon, target.row, target.column, number);
    [map put:number:icon atLocation:target];
}

//----------------------------------------------------------------------

- (void) setCityAtLocation:(EMMapLocation)target toPlayer:(Player)newCityPlayer forPlayer:(Player)number
{
    Map *map = [self mapForPlayer:number];

    NSAssert (map != nil, @"Map was nil.");

    [map setCityAtLocation:target toPlayer:newCityPlayer];
}

//======================================================================
// Etc.
//======================================================================

//----------------------------------------------------------------------
// No need to override in DGM, thanks to mapForPlayer:
//----------------------------------------------------------------------

- (MapToken) canonTokenAtLocation:(EMMapLocation)target
{
    MapToken mapToken;
    Player p;
    Map *map;

    for (p = p_player1; p <= p_player3; p++)
    {
        map = [self mapForPlayer:p];
        
        if (map != nil)
        {
            mapToken = [map tokenAtLocation:target];
            if (EMPlayerComponent (mapToken) == p)
                return mapToken;
        }
    }

    NSAssert (world != nil, @"No active world.");
    return [[world worldMap] tokenAtLocation:target];
}

//----------------------------------------------------------------------
// No need to override in DGM, thanks to mapForPlayer:
//----------------------------------------------------------------------

- (int) playersAdjacentToLocation:(EMMapLocation)target
{
    MapToken tokens[9];
    BOOL playerFlags[4] = {NO, NO, NO, NO};
    Player p;
    int l;
    int adjacentPlayers = 0;
    Map *map;

    for (p = p_player1; p <= p_player3; p++)
    {
        map = [self mapForPlayer:p];
        
        if (map != nil)
        {
            [map get3x3Tokens:tokens aroundLocation:target];
            //NSLog (@"player: %d, tokens: %@", p, EMFormatNineComponents (tokens));
            for (l = 0; l < 9; l++)
            {
                if (EMPlayerComponent (tokens[l]) == p)
                {
                    playerFlags[p] = YES;
                    break;
                }
            }
        }
    }

    if (playerFlags[p_player1] == YES)
        adjacentPlayers |= ADJ_PLAYER1;
    if (playerFlags[p_player2] == YES)
        adjacentPlayers |= ADJ_PLAYER2;
    if (playerFlags[p_player3] == YES)
        adjacentPlayers |= ADJ_PLAYER3;

    //NSLog (@"players adjacent to (%d,%d): %x", target.row, target.column, adjacentPlayers);

    return adjacentPlayers;
}

//----------------------------------------------------------------------
// Simple version -- everything can see everything adjacent.
//----------------------------------------------------------------------

//----------------------------------------------------------------------
// The hasChanged shortcut isn't valid, since we can put/remove/set without
// notifying remote versions.
//
// Perhaps this "compression" of updates should be in the map.
//----------------------------------------------------------------------

// No need to override in DGM...
- (void) X_explorePlayer:(Player)number around3x3Location:(EMMapLocation)target updateOtherPlayers:(BOOL)flag
{
    MapToken player_tokens[4][9];
    MapToken original_tokens[9];
    int enemyIndex[4];
    Player p;
    int l;
    BOOL hasChanged;
    Map *map;

    // Get tokens for all players 

    map = [self mapForPlayer:number];
    NSAssert (map != nil, @"Player map was nil");
    NSAssert (world != nil, @"No active world.");

    [[world worldMap] get3x3Tokens:player_tokens[p_neutral] aroundLocation:target];
    [map get3x3Tokens:original_tokens aroundLocation:target];

    for (p = p_player1; p <= p_player3; p++)
    {
        enemyIndex[p] = -1;
        map = [self mapForPlayer:p];
        if (map != nil)
        {
            [map get3x3Tokens:player_tokens[p] aroundLocation:target];
            //tmp = p;
        }
    }
    
    // Calculate real positions of all units
    for (p = p_player1; p <= p_player3; p++)
    {
        // Would be better to keep track of what maps were nil, above.
        map = [self mapForPlayer:p];

        if (map != nil)
        {
            for (l = 0; l < 9; l++)
            {
                if (EMPlayerComponent (player_tokens[p][l]) == p)
                {
                    player_tokens[p_neutral][l] = player_tokens[p][l];
                    enemyIndex[p] = l;
                }
            }
        }
    }

    hasChanged = NO;
#if 0
    for (l = 0; l < 9; l++)
    {
        player_tokens[p_neutral][l] = EMChangeExploredComponent (player_tokens[p_neutral][l], YES);
        if (player_tokens[p_neutral][l] != original_tokens[l])
        {
            //NSLog (@"l: %d", l);
            hasChanged = YES;
        }
    }

    if (hasChanged == YES)
        ;
#endif

#if 0
    if (debug_flag == YES)
    {
        //NSLog (@"Player: %d @ (%d,%d)", number, target.row, target.column);

        //NSLog (@"or: %@", EMFormatNineComponents (original_tokens));
        //NSLog (@"p0: %@", EMFormatNineComponents (player_tokens[p_neutral]));
        //NSLog (@"p1: %@", EMFormatNineComponents (player_tokens[p_player1]));
        //NSLog (@"p2: %@", EMFormatNineComponents (player_tokens[p_player2]));
        //NSLog (@"p3: %@", EMFormatNineComponents (player_tokens[p_player3]));
#if 0
        NSLog (@"%08x %08x %08x %08x %08x %08x %08x %08x %08x",
               player_tokens[p_neutral][0], player_tokens[p_neutral][1], player_tokens[p_neutral][2],
               player_tokens[p_neutral][3], player_tokens[p_neutral][4], player_tokens[p_neutral][5],
               player_tokens[p_neutral][6], player_tokens[p_neutral][7], player_tokens[p_neutral][8]
                  );
        
        NSLog (@"%08x %08x %08x %08x %08x %08x %08x %08x %08x",
               player_tokens[p_player1][0], player_tokens[p_player1][1], player_tokens[p_player1][2],
               player_tokens[p_player1][3], player_tokens[p_player1][4], player_tokens[p_player1][5],
               player_tokens[p_player1][6], player_tokens[p_player1][7], player_tokens[p_player1][8]
                  );
        
        NSLog (@"%08x %08x %08x %08x %08x %08x %08x %08x %08x",
               player_tokens[p_player2][0], player_tokens[p_player2][1], player_tokens[p_player2][2],
               player_tokens[p_player2][3], player_tokens[p_player2][4], player_tokens[p_player2][5],
               player_tokens[p_player2][6], player_tokens[p_player2][7], player_tokens[p_player2][8]
                  );
        
        NSLog (@"%08x %08x %08x %08x %08x %08x %08x %08x %08x",
               player_tokens[p_player3][0], player_tokens[p_player3][1], player_tokens[p_player3][2],
               player_tokens[p_player3][3], player_tokens[p_player3][4], player_tokens[p_player3][5],
               player_tokens[p_player3][6], player_tokens[p_player3][7], player_tokens[p_player3][8]
                  );
        
        NSLog (@"%08x %08x %08x %08x %08x %08x %08x %08x %08x",
               original_tokens[0], original_tokens[1], original_tokens[2],
               original_tokens[3], original_tokens[4], original_tokens[5],
               original_tokens[6], original_tokens[7], original_tokens[8]
                  );
#endif
    }
#endif

    //
    // The hasChanged semantics aren't quite correct.  We want to update our lost token, but
    // in this cas hasChanged will be NO.
    //
    // We will just have to filter out no-effect changes when recording updates...
    //
    //
    
    {
        //NSLog (@"Has changed.");
        [self set3x3Tokens:player_tokens[p_neutral] aroundLocation:target forPlayer:number];
    }

    if (flag == YES)
    {
        int delta_row[9] = { -1, -1, -1,  0,  0,  0,  1,  1,  1 };
        int delta_col[9] = { -1,  0,  1, -1,  0,  1, -1,  0,  1 };

        for (p = p_player1; p <= p_player3; p++)
        {
            if (p != number && enemyIndex[p] >= 0)
            {
                EMMapLocation enemyLocation;
                int ei = enemyIndex[p];

                enemyLocation.row = target.row + delta_row[ei];
                enemyLocation.column = target.column + delta_col[ei];

                [self explorePlayer:p around3x3Location:enemyLocation updateOtherPlayers:NO];
            }
        }
    }
}

//----------------------------------------------------------------------
// The (NOT SO) simple version -- everything can see everything adjacent.
//----------------------------------------------------------------------
// This is a somewhat nightmarish method, and perhaps one of the most
// difficult to understand.
//
// The key is to realize that there is not a single source that has
// the real location of all units.  Instead, we can derive this
// information by combining all the player maps.  The icons of player N
// on the map of player N represent the real location of those icons.
//
// We first get the canonical location of all units, and then decide
// whether the player can see all the enemy units.
//
// The advantage of this is that we only have to distribute maps --
// we don't need to share lists of units, or provide access to units
// over the connection of a distributed game.
//
// There is also some cleverness happening with the visibility flags.
// I'd highly recommend drawing diagrams if you're trying to understand
// what this code is doing.  I've included two examples in the
// technical documentation.
//----------------------------------------------------------------------

// No need to override in DGM...
- (void) explorePlayer:(Player)number around3x3Location:(EMMapLocation)target updateOtherPlayers:(BOOL)flag
{
    MapToken player_tokens[4][9];
    MapToken original_tokens[9];
    int enemyIndex[4];
    Player p;
    int l;
    Map *map;
    int canSeeFlags[9];
    int lastUnitFlagList[9], realUnitFlagList[9];
    int unitFlags[UNIT_TYPE_COUNT] = {
        0,
        VIS_LAND,
        VIS_AIR,
        VIS_SEA,
        VIS_SUB,
        VIS_SEA,
        VIS_SEA,
        VIS_SEA,
        VIS_SEA,
        VIS_AIR,
//        VIS_SEA
    };

    // Get tokens for all players 

    map = [self mapForPlayer:number];
    NSAssert (map != nil, @"Player map was nil");
    NSAssert (world != nil, @"No active world.");

    [[world worldMap] get3x3Tokens:player_tokens[p_neutral] aroundLocation:target];
    [map get3x3Tokens:original_tokens aroundLocation:target];

    for (p = p_player1; p <= p_player3; p++)
    {
        enemyIndex[p] = -1;
        map = [self mapForPlayer:p];
        if (map != nil)
        {
            [map get3x3Tokens:player_tokens[p] aroundLocation:target];
            //tmp = p;
        }
    }
    
    // Calculate real positions of all units
    for (p = p_player1; p <= p_player3; p++)
    {
        // Would be better to keep track of what maps were nil, above.
        map = [self mapForPlayer:p];

        if (map != nil)
        {
            for (l = 0; l < 9; l++)
            {
                if (EMPlayerComponent (player_tokens[p][l]) == p)
                {
                    player_tokens[p_neutral][l] = player_tokens[p][l];
                    enemyIndex[p] = l;
                }
            }
        }
    }

    // Build up visiblity flags
    [self get3x3flags:canSeeFlags forPlayer:number aroundLocation:target];

    //NSLog (@"real icons: %@", EMFormatNineComponents (player_tokens[p_neutral]));
    //NSLog (@"last icons: %@", EMFormatNineComponents (player_tokens[number]));

    // Build up attribute flags -- not really, both last known and real..
    // Update player knowledge
    for (l = 0; l < 9; l++)
    {
        Player player;
        Icon icon;
        UnitType unitType;

        lastUnitFlagList[l] = 0;
        realUnitFlagList[l] = 0;

        EMMapTokenComponents (player_tokens[number][l], NULL, &player, &icon, NULL);
        unitType = EMConvertIconToUnitType (icon);
        if (player != number)
            lastUnitFlagList[l] = unitFlags[unitType];

        EMMapTokenComponents (player_tokens[p_neutral][l], NULL, &player, &icon, NULL);
        unitType = EMConvertIconToUnitType (icon);
        if (player != number)
            realUnitFlagList[l] = unitFlags[unitType];
    }

    //NSLog (@"real attrs: %@", EMFormatNineVisibilityFlags (realUnitFlagList));
    //NSLog (@"last attrs: %@", EMFormatNineVisibilityFlags (lastUnitFlagList));
    //NSLog (@"can see attrs: %@", EMFormatNineVisibilityFlags (canSeeFlags));

    for (l = 0; l < 9; l++)
    {
        Terrain realTerrain, lastTerrain;
        Player realPlayer, lastPlayer;
        Icon realIcon, lastIcon;
        UnitType realUnitType, lastUnitType;

        EMMapTokenComponents (player_tokens[number][l], &lastTerrain, &lastPlayer, &lastIcon, NULL);
        EMMapTokenComponents (player_tokens[p_neutral][l], &realTerrain, &realPlayer, &realIcon, NULL);
        
        realUnitType = EMConvertIconToUnitType (realIcon);
        lastUnitType = EMConvertIconToUnitType (lastIcon);

        // Everything can see cities.
        if (realTerrain == t_city)
        {
            player_tokens[number][l] = player_tokens[p_neutral][l];
        }

//        if (lastPlayer != number && lastIcon != realIcon && (unitFlags[lastUnitType] & canSeeFlags[l]))
        if (lastPlayer != number && lastIcon != realIcon && (lastUnitFlagList[l] & canSeeFlags[l]))
        {
            // remove the icon
            player_tokens[number][l] = EMCreateMapToken (realTerrain, p_neutral, i_none, YES);
        }

//        if (realPlayer != number && (unitFlags[realUnitType] & canSeeFlags[l]))
        if (realPlayer != number && (realUnitFlagList[l] & canSeeFlags[l]))
        {
            // put that icon
            player_tokens[number][l] = player_tokens[p_neutral][l];
        }

        // remove known no longer there
        // add known there
    }

    for (l = 0; l < 9; l++)
    {
        player_tokens[p_neutral][l] = EMChangeExploredComponent (player_tokens[p_neutral][l], YES);
        player_tokens[number][l] = EMChangeExploredComponent (player_tokens[number][l], YES);
    }

    //NSLog (@"new icons: %@", EMFormatNineComponents (player_tokens[number]));
    //NSLog (@"....................");

#if 0
    if (debug_flag == YES)
    {
        //NSLog (@"Player: %d @ (%d,%d)", number, target.row, target.column);

        //NSLog (@"or: %@", EMFormatNineComponents (original_tokens));
        //NSLog (@"p0: %@", EMFormatNineComponents (player_tokens[p_neutral]));
        //NSLog (@"p1: %@", EMFormatNineComponents (player_tokens[p_player1]));
        //NSLog (@"p2: %@", EMFormatNineComponents (player_tokens[p_player2]));
        //NSLog (@"p3: %@", EMFormatNineComponents (player_tokens[p_player3]));
#if 0
        NSLog (@"%08x %08x %08x %08x %08x %08x %08x %08x %08x",
               player_tokens[p_neutral][0], player_tokens[p_neutral][1], player_tokens[p_neutral][2],
               player_tokens[p_neutral][3], player_tokens[p_neutral][4], player_tokens[p_neutral][5],
               player_tokens[p_neutral][6], player_tokens[p_neutral][7], player_tokens[p_neutral][8]
                  );
        
        NSLog (@"%08x %08x %08x %08x %08x %08x %08x %08x %08x",
               player_tokens[p_player1][0], player_tokens[p_player1][1], player_tokens[p_player1][2],
               player_tokens[p_player1][3], player_tokens[p_player1][4], player_tokens[p_player1][5],
               player_tokens[p_player1][6], player_tokens[p_player1][7], player_tokens[p_player1][8]
                  );
        
        NSLog (@"%08x %08x %08x %08x %08x %08x %08x %08x %08x",
               player_tokens[p_player2][0], player_tokens[p_player2][1], player_tokens[p_player2][2],
               player_tokens[p_player2][3], player_tokens[p_player2][4], player_tokens[p_player2][5],
               player_tokens[p_player2][6], player_tokens[p_player2][7], player_tokens[p_player2][8]
                  );
        
        NSLog (@"%08x %08x %08x %08x %08x %08x %08x %08x %08x",
               player_tokens[p_player3][0], player_tokens[p_player3][1], player_tokens[p_player3][2],
               player_tokens[p_player3][3], player_tokens[p_player3][4], player_tokens[p_player3][5],
               player_tokens[p_player3][6], player_tokens[p_player3][7], player_tokens[p_player3][8]
                  );
        
        NSLog (@"%08x %08x %08x %08x %08x %08x %08x %08x %08x",
               original_tokens[0], original_tokens[1], original_tokens[2],
               original_tokens[3], original_tokens[4], original_tokens[5],
               original_tokens[6], original_tokens[7], original_tokens[8]
                  );
#endif
    }
#endif

    //
    // The hasChanged semantics aren't quite correct.  We want to update our lost token, but
    // in this cas hasChanged will be NO.
    //
    // We will just have to filter out no-effect changes when recording updates...
    //
    //
    
    {
        //NSLog (@"Has changed.");
        [self set3x3Tokens:player_tokens[number] aroundLocation:target forPlayer:number];
    }

    if (flag == YES)
    {
        int delta_row[9] = { -1, -1, -1,  0,  0,  0,  1,  1,  1 };
        int delta_col[9] = { -1,  0,  1, -1,  0,  1, -1,  0,  1 };

        for (p = p_player1; p <= p_player3; p++)
        {
            if (p != number && enemyIndex[p] >= 0)
            {
                EMMapLocation enemyLocation;
                int ei = enemyIndex[p];

                enemyLocation.row = target.row + delta_row[ei];
                enemyLocation.column = target.column + delta_col[ei];

                [self explorePlayer:p around3x3Location:enemyLocation updateOtherPlayers:NO];
            }
        }
    }
}

//----------------------------------------------------------------------

- (void) get3x3flags:(int *)flags forPlayer:(Player)number aroundLocation:(EMMapLocation)target
{
    MapToken ours[25];
    int can_see_flags[25];
    int merged_flags[9];
    int blarg[9][9] = {
        {  0,  1,  2,  5,  6,  7, 10, 11, 12},
        {  1,  2,  3,  6,  7,  8, 11, 12, 13},
        {  2,  3,  4,  7,  8,  9, 12, 13, 14},
        {  5,  6,  7, 10, 11, 12, 15, 16, 17},
        {  6,  7,  8, 11, 12, 13, 16, 17, 18},
        {  7,  8,  9, 12, 13, 14, 17, 18, 19},
        { 10, 11, 12, 15, 16, 17, 20, 21, 22},
        { 11, 12, 13, 16, 17, 18, 21, 22, 23},
        { 12, 13, 14, 17, 18, 19, 22, 23, 24}
    };
    int a, b;
    int unit_can_see_flags[UNIT_TYPE_COUNT] = {
        0,
        VIS_LAND | VIS_SEA | VIS_AIR,
        VIS_LAND | VIS_SEA | VIS_AIR,
        VIS_LAND | VIS_SEA | VIS_AIR,
        VIS_SUB | VIS_SEA,
        VIS_LAND | VIS_SEA | VIS_AIR,
        VIS_SUB | VIS_LAND | VIS_SEA | VIS_AIR,
        VIS_LAND | VIS_SEA | VIS_AIR,
        VIS_LAND | VIS_SEA | VIS_AIR,
        VIS_LAND | VIS_SEA | VIS_AIR,
//        VIS_SUB | VIS_LAND | VIS_SEA | VIS_AIR
    };

    Terrain terrain;
    Player player;
    Icon icon;

    [[self mapForPlayer:number] get5x5Tokens:ours aroundLocation:target];

    //NSLog (@"player %d's 25 tokens: %@", number, EMFormat25Components (ours));

    for (a = 0; a < 25; a++)
    {
        can_see_flags[a] = 0;

        EMMapTokenComponents (ours[a], &terrain, &player, &icon, NULL);
        if (player == number)
        {
            if (terrain == t_city)
                can_see_flags[a] = VIS_SUB | VIS_LAND | VIS_SEA | VIS_AIR;
            else
                can_see_flags[a] = unit_can_see_flags[EMConvertIconToUnitType (icon)];
        }
    }

    //NSLog (@"player %d can see: %@", number, EMFormat25VisibilityFlags (can_see_flags));

    // Merge flags
    for (a = 0; a < 9; a++)
    {
        merged_flags[a] = 0;
        for (b = 0; b < 9; b++)
        {
            merged_flags[a] |= can_see_flags[ blarg[a][b] ];
        }

        flags[a] = merged_flags[a];
    }

    //NSLog (@"merged flags: %@", EMFormatNineVisibilityFlags (merged_flags));
}

//======================================================================
// Combating...
//======================================================================

- (MoveResult) unit:(Unit *)attacker attacksPlayer:(Player)cityPlayer cityAtLocation:(EMMapLocation)target
{
    CombatProfile attackerProfile;
    MoveResult moveResult;

    //NSLog (@"attacker: %@, cityPlayer: %d, cityLoc: %d,%d", attacker, cityPlayer, target.row, target.column);

    NSAssert (attackingUnit == nil, @"attackingUnit is not nil.");
    
    attackingUnit = attacker;

    // Build up attacking army's combat profile:

    attackerProfile = [attacker combatProfileAgainstBombardment:NO];
    attackerProfile.adjacent_players = [self playersAdjacentToLocation:attackerProfile.location];

    moveResult = [self unitWithCombatProfile:attackerProfile
                       attacksPlayer:cityPlayer
                       cityAtLocation:target
                       playersAdjacentToDefender:[self playersAdjacentToLocation:target]];

    attackingUnit = nil;

    return moveResult;
}

//----------------------------------------------------------------------

- (MoveResult) unitWithCombatProfile:(CombatProfile)attackerProfile
                       attacksPlayer:(Player)cityPlayer
                      cityAtLocation:(EMMapLocation)target
           playersAdjacentToDefender:(int)adjacentPlayers
{
    CombatProfile defenderProfile;
    MoveResult moveResult;
    
    //NSLog (@"cityPlayer: %d, cityLocation: %d,%d patd: %d", cityPlayer, target.row, target.column, adjacentPlayers);

    // Get defender combat profile
    defenderProfile = [self readyDefendingCityAtLocation:target forPlayer:cityPlayer];
    defenderProfile.adjacent_players = adjacentPlayers;

    moveResult = [self attackerWithCombatProfile:attackerProfile attacksDefenderWithCombatProfile:defenderProfile];

    return moveResult;
}

//----------------------------------------------------------------------

- (MoveResult) unit:(Unit *)attacker
      attacksPlayer:(Player)defender
     unitAtLocation:(EMMapLocation)target
    withBombardment:(BOOL)bombarding
{
    CombatProfile attackerProfile;
    MoveResult moveResult;

    //NSLog (@"attacker: %@, defender: %d, cityLoc: %d,%d", attacker, defender, target.row, target.column);

    NSAssert (attackingUnit == nil, @"attackingUnit is not nil.");
    
    attackingUnit = attacker;

    // Build up attacking army's combat profile:

    attackerProfile = [attacker combatProfileAgainstBombardment:NO];
    attackerProfile.adjacent_players = [self playersAdjacentToLocation:attackerProfile.location];

    moveResult = [self unitWithCombatProfile:attackerProfile
                       attacksPlayer:defender
                       unitAtLocation:target
                       withBombardment:bombarding
                       playersAdjacentToDefender:[self playersAdjacentToLocation:target]];

    attackingUnit = nil;

    return moveResult;
}

//----------------------------------------------------------------------

- (MoveResult) unitWithCombatProfile:(CombatProfile)attackerProfile
                       attacksPlayer:(Player)defender
                      unitAtLocation:(EMMapLocation)target
                     withBombardment:(BOOL)bombarding
           playersAdjacentToDefender:(int)adjacentPlayers
{
    CombatProfile defenderProfile;
    MoveResult moveResult;
    
    //NSLog (@"defender: %d, cityLocation: %d,%d patd: %d", defender, target.row, target.column, adjacentPlayers);

    // Get defender combat profile
    defenderProfile = [self readyDefendingUnitAtLocation:target forPlayer:defender againstBombardment:bombarding];
    defenderProfile.adjacent_players = adjacentPlayers;

    moveResult = [self attackerWithCombatProfile:attackerProfile attacksDefenderWithCombatProfile:defenderProfile];

    return moveResult;
}

//----------------------------------------------------------------------

// Duke it out:
- (MoveResult) attackerWithCombatProfile:(CombatProfile)attackerProfile
        attacksDefenderWithCombatProfile:(CombatProfile)defenderProfile
{
    int attackerAttacks;
    int attackerDefends;
    int defenderAttacks;
    int defenderDefends;
    MoveResult result = mr_blocked;
    BOOL done;
    SNRandom *rng;

    int defenderBits = 1 << (defenderProfile.player - p_neutral);
    int attackerBits = 1 << (attackerProfile.player - p_neutral);
    
    // Create combatant mask
    // [self showExplosions:l+1 playerMask:attackerProfile.adjacent&~mask atLocation:attackerProfile.location];

    // Defender hit:
    // - show explosions for masked observers
    // - [defender hitFor:n];
    // - [attacker showExplosions...];

    // Attacker hit:
    // - show explosions for masked observers
    // - [attacker hitFor:n];
    // - [defender showExplosions...];

    // Originating game manager will update map(s) by removing appropriate icon after it is destroyed...

    NSAssert (defenderProfile.attacking_efficiency != 0, @"Defenders efficiency is 0.");
    NSAssert (attackerProfile.attacking_efficiency != 0, @"Attackers efficiency is 0.");
    NSAssert (attackerProfile.damage_per_hit > 0, @"Attackers damage per hit is 0.");
    NSAssert (defenderProfile.damage_per_hit > 0, @"Defenders damage per hit is 0.");

    rng = [SNRandom instance];

    // First, we need to make sure that both units can see each other.
    // For example, normally a sub and a fighter can't see each other, but if
    // one tries to move into the square occupied by the other, they will fight.

    done = NO;
    while (done == NO)
    {
        attackerAttacks = [rng randomNumberModulo:100];
        attackerDefends = [rng randomNumberModulo:100];
        defenderAttacks = [rng randomNumberModulo:100];
        defenderDefends = [rng randomNumberModulo:100];
#if 0
        NSLog (@"a[d]/e, Attacker: %d[%d]/%d[%d], Defender: %d[%d]/%d[%d]",
               attackerAttacks, attackerProfile.attacking_efficiency, attackerDefends, attackerProfile.defending_efficiency,
               defenderAttacks, defenderProfile.attacking_efficiency, defenderDefends, defenderProfile.defending_efficiency);
#endif

        if (attackerAttacks < attackerProfile.attacking_efficiency && defenderDefends >= defenderProfile.defending_efficiency)
        {
            //NSLog (@"Defender hit.");

            // Hit defender for attackerProfile.damage_per_hit point(s).
            // Show explosions to both combatants. (as well as third parties...)

            [self showExplosions:attackerProfile.damage_per_hit
                  atLocation:defenderProfile.location
                  toPlayers:defenderProfile.adjacent_players & ~defenderBits];

            if (defenderProfile.is_a_city == YES)
            {
                //[self hitDefendingCity:defenderProfile.player withDamage:attackerProfile.damage_per_hit];
                City *capturedCity = [self lostDefendingCityOfPlayer:defenderProfile.player];
                //NSLog (@"captured city: %@", capturedCity);
                //NSLog (@"it's owner: %@", [capturedCity owner]);

                [self player:attackerProfile.player hasCapturedCity:capturedCity];
                //NSLog (@"about to explore");
                //[self explorePlayer:attackerProfile.player around3x3Location:defenderProfile.location updateOtherPlayers:YES];
                //NSLog (@"explored");
            }
            else
            {
                [self hitDefendingUnit:defenderProfile.player withDamage:attackerProfile.damage_per_hit];
            }

            defenderProfile.current_hit_points -= attackerProfile.damage_per_hit;

            if (defenderProfile.current_hit_points <= 0)
            {
                done = YES;
                result = mr_victory;
            }
        }

        // Hmm.  We don't want a city captured, but the army destroyed in the attempt.
        // The army is supposed to be transformed into a garrison...

        if (done == NO && defenderAttacks < defenderProfile.attacking_efficiency
            && attackerDefends >= attackerProfile.defending_efficiency)
        {
            //NSLog (@"Attacker hit.");

            // Hit attacker for defenderProfile.damage_per_hit point(s).
            // Show both combatants.

            [self showExplosions:defenderProfile.damage_per_hit
                  atLocation:attackerProfile.location
                  toPlayers:attackerProfile.adjacent_players &~ attackerBits];

            // The attacker better not be a city!
            [self hitAttacker:attackerProfile.player withDamage:defenderProfile.damage_per_hit];

            attackerProfile.current_hit_points -= defenderProfile.damage_per_hit;

            if (attackerProfile.current_hit_points <= 0)
            {
                [self finishedCombatForPlayer:defenderProfile.player];
                done = YES;
                result = mr_destroyed;
            }
        }
    }

    return result;
}

//----------------------------------------------------------------------

- (CombatProfile) readyDefendingCityAtLocation:(EMMapLocation)target forPlayer:(Player)number
{
    City *city;
    CombatProfile cityProfile;
    
    //NSLog (@"target: %d,%d player: %d", target.row, target.column, number);
    NSAssert (defendingCity == nil, @"Defending city is not nil.");
    NSAssert (defendingUnit == nil, @"Defending unit is not nil.");

    if (number == p_neutral)
    {
        // Fill in standard city combat profile

        cityProfile.location = target;
        cityProfile.player = p_neutral;
        cityProfile.attacking_efficiency = 50;
        cityProfile.defending_efficiency = 50;
        cityProfile.damage_per_hit = 1;
        cityProfile.current_hit_points = 1;
        cityProfile.is_a_city = YES;

        defendingCity = [world neutralCityAtLocation:target];
    }
    else
    {
        NSAssert (players[number] != nil, @"We don't have that player.");

        // Find city
        city = [players[number] cityAtLocation:target];
        NSAssert (city != nil, @"That player doesn't have that city.");
        defendingCity = city;

        cityProfile.location = target;
        cityProfile.player = number;
        cityProfile.attacking_efficiency = [players[number] combatEfficiency];
        cityProfile.defending_efficiency = [players[number] combatEfficiency];
        cityProfile.damage_per_hit = 1;
        cityProfile.current_hit_points = 1;
        cityProfile.is_a_city = YES;
    }

    cityProfile.adjacent_players = [self playersAdjacentToLocation:target];

    return cityProfile;
}

//----------------------------------------------------------------------

- (CombatProfile) readyDefendingUnitAtLocation:(EMMapLocation)target forPlayer:(Player)number againstBombardment:(BOOL)bombarding
{
    Unit *unit;
    CombatProfile unitProfile;
    
    //NSLog (@"target: %d,%d player: %d", target.row, target.column, number);

    NSAssert (defendingCity == nil, @"Defending city is not nil.");
    NSAssert (defendingUnit == nil, @"Defending unit is not nil.");
    NSAssert (players[number] != nil, @"We don't have that player.");

    // Find city
    unit = [players[number] primaryUnitAtLocation:target];
    NSAssert (unit != nil, @"That player doesn't a unit at that location.");
    defendingUnit = unit;

    unitProfile = [unit combatProfileAgainstBombardment:bombarding];

    unitProfile.adjacent_players = [self playersAdjacentToLocation:target];

    return unitProfile;
}

//----------------------------------------------------------------------

- (void) showExplosions:(int)count atLocation:(EMMapLocation)target toPlayers:(int)playerMask
{
    if (playerMask & 0x01)
        [self showExplosions:count forPlayer:p_player1 atLocation:target];

    if (playerMask & 0x02)
        [self showExplosions:count forPlayer:p_player2 atLocation:target];

    if (playerMask & 0x04)
        [self showExplosions:count forPlayer:p_player3 atLocation:target];
}

//----------------------------------------------------------------------

- (void) showExplosions:(int)count forPlayer:(Player)number atLocation:(EMMapLocation)target
{
    NSAssert (players[number] != nil, @"We don't have that player.");

    [players[number] showExplosions:count atLocation:target];
}

//----------------------------------------------------------------------

- (void) hitDefendingUnit:(Player)number withDamage:(int)damage
{
    BOOL wasDestroyed;
    
    NSAssert (players[number] != nil, @"We don't have that player.");
    NSAssert (defendingUnit != nil, @"No defending unit!");

    wasDestroyed = [defendingUnit hitForDamage:damage];
    if (wasDestroyed == YES)
    {
        //NSLog (@"Unit(defender) was destroyed.");
        defendingUnit = nil;
    }
}

//----------------------------------------------------------------------

- (City *) lostDefendingCityOfPlayer:(Player)number
{
    City *city;
    
    NSAssert (number == p_neutral || players[number] != nil, @"We don't have that player.");
    NSAssert (defendingCity != nil, @"No defending city!");
    NSAssert (world != nil, @"No active world.");

    city = [[defendingCity retain] autorelease];
    defendingCity = nil;

    if (number == p_neutral)
    {
        [world lostCity:city];
    }
    else
    {
        [players[number] showExplosions:1 atLocation:[city cityLocation]];
        [players[number] lostCity:city];
    }

    return city;
}

//----------------------------------------------------------------------

- (void) hitAttacker:(Player)number withDamage:(int)damage
{
    BOOL wasDestroyed;
    
    NSAssert (players[number] != nil, @"We don't have that player.");
    NSAssert (attackingUnit != nil, @"No attacking unit!");

    wasDestroyed = [attackingUnit hitForDamage:damage];
    if (wasDestroyed == YES)
    {
        //NSLog (@"Unit(attacker) was destroyed.");
        attackingUnit = nil;
    }
}

//----------------------------------------------------------------------

- (void) player:(Player)number hasCapturedCity:(City *)capturedCity
{
    NSAssert (players[number] != nil, @"We don't have that player.");

    [players[number] capturedCity:capturedCity];

    // Will also need notification of lost unit...
    attackingUnit = nil;
}

//----------------------------------------------------------------------
// Notify defender that the combat is over. (He won).
//----------------------------------------------------------------------

- (void) finishedCombatForPlayer:(Player)number
{
    // Should not need to reset attacker, done elsewhere.

    defendingUnit = nil;
    defendingCity = nil;
}

//----------------------------------------------------------------------

// Returns YES if the attacker has won.
// Client -> Server
- (BOOL) checkForEndOfPlayer:(Player)number
{
    Player winningPlayer, p;
    BOOL done = NO;
    BOOL gameOver = NO;

    //NSAssert (number == p_neutral || players[number] != nil, @"We don't have that player.");

    //[self logStatus];

    if (number != p_neutral)
    {
        done = [self hasPlayerLost:number]; // Server -> Client

        if (done == YES)
        {
            [self storeFinalMapForPlayer:number];
            [self playerHasLost:number activePlayers:[self activePlayers]]; // Server -> Client
            //nyi: Strip icons for other players?  Probably best to avoid fighting phantom units...?
            //[self deactivatePlayer:number];

            // Notify remaining active players?

            //NSLog (@"active player count: %d", activePlayerCount);
            if (activePlayerCount < 2)
            {
                gameOver = YES;
            }
        }
    }
    else
    {
        // Hmm.  Captured last neutral city, need to detect win.
        // Captured last neutral city, but still remaining enemy cities..
    }

    if (done == YES && gameOver == NO)
    {
        // Notify other players so they can strip that players icons from their maps.
        // Do this after notifying player, so that player picks up pre-stripped maps.
        // Don't want to fight phantoms.
        for (p = p_player1; p <= p_player3; p++)
        {
            if (playersActive[p] == YES && p != number)
                [self notifyPlayer:p aPlayerHasResigned:number];
        }
    }

    // Perhaps this should be moved into the server?
    if (gameOver == YES)
    {
        BOOL keepPlaying;

        winningPlayer = p_neutral;
        for (p = p_player1; p <= p_player3; p++)
        {
            if (playersActive[p] == YES)
                winningPlayer = p;
        }

        NSAssert (playersActive[winningPlayer] == YES, @"Expected an active player to win.");
        keepPlaying = [self playerHasWon:winningPlayer activePlayers:[self activePlayers]]; // Server -> Client
        // Also terminates both client and server...
    }

    //[self logStatus];

    return gameOver;
}

//----------------------------------------------------------------------

- (BOOL) hasPlayerLost:(Player)number
{
    NSAssert (players[number] != nil, @"We don't have that player.");

    return [players[number] hasPlayerLost];
}

//----------------------------------------------------------------------

- (void) playerHasLost:(Player)number activePlayers:(int)activePlayers
{
    NSAssert (players[number] != nil, @"We don't have that player.");

    // cache current (final) map for player number

    [players[number] playerHasLost:activePlayers];

    //nyi: ?? Hmm... Not in clients -- but it doesn't hurt.
    [self deactivatePlayer:number];
}

//----------------------------------------------------------------------

- (BOOL) playerHasWon:(Player)number activePlayers:(int)activePlayers
{
    BOOL keepPlaying;

    NSAssert (players[number] != nil, @"We don't have that player.");

    keepPlaying = [players[number] playerHasWon:activePlayers];
    if (keepPlaying == NO)
    {
        [self deactivatePlayer:number];
    }

    return keepPlaying;
}

//----------------------------------------------------------------------

// Server:
// currentMap:(Map *)currentMap // or perhaps we still need a separate method...
- (void) resignPlayerFromGame:(Player)number
{
    NSArray *remainingCities;
    Player p;

    //[self logStatus];

    remainingCities = [self remainingCitiesForPlayer:number];
    //NSLog (@"remaining cities: %@", remainingCities);

    NSAssert (world != nil, @"No active world.");
    [world addNeutralCities:remainingCities];

    [self storeFinalMapForPlayer:number];

    [self playerHasResigned:number activePlayers:[self activePlayers]];

    // Notify other players so they can strip that players icons from their maps.
    // Do this after notifying player, so that player picks up pre-stripped maps.
    for (p = p_player1; p <= p_player3; p++)
    {
        if (playersActive[p] == YES && p != number)
            [self notifyPlayer:p aPlayerHasResigned:number];
    }

    // I need to reduce the activePlayerCount here so that I know if
    // a player has won...

    [self deactivatePlayer:number];


    if (activePlayerCount <= 1)
    {
        Player p, winningPlayer = p_neutral;

        for (p = p_player1; p <= p_player3; p++)
            if (playersActive[p] == YES)
                winningPlayer = p;

        //NSLog (@"winning player: %d", winningPlayer);

        // What about a remote player?
        if (winningPlayer != p_neutral)
        {
            BOOL keepPlaying;

            NSAssert (playersActive[winningPlayer] == YES, @"Expected an active player to win.");

            keepPlaying = [self playerHasWon:winningPlayer activePlayers:[self activePlayers]];

            //NSLog (@"keep playing? %@", keepPlaying ? @"Yes" : @"No");
            if (keepPlaying == YES && number == [self activePlayer])
            {
                // force the next turn...
                [self turnDone:awaitingCookie];
            }
        }
    }
    else if (number == [self activePlayer])
    {
        // force the next turn...
        [self turnDone:awaitingCookie];
    }

    //[self logStatus];
}

//----------------------------------------------------------------------

// Should only be called in server.
- (int) activePlayers
{
    int active = 0;

    if (playersActive[p_player1] == YES)
        active |= ADJ_PLAYER1;

    if (playersActive[p_player2] == YES)
        active |= ADJ_PLAYER2;

    if (playersActive[p_player3] == YES)
        active |= ADJ_PLAYER3;
    
    return active;
}

//----------------------------------------------------------------------

// Sent only to active player.
// NO - need to send to all other players.
//...

- (void) notifyPlayer:(Player)number aPlayerHasResigned:(Player)resignedPlayer
{
    NSAssert (players[number] != nil, @"We don't have that player.");
    [players[number] aPlayerHasResigned:resignedPlayer];
}

//----------------------------------------------------------------------

// Returns player whose turn it is.
- (Player) activePlayer
{
    Player player = p_neutral;

    switch (gameState)
    {
      case gs_player1_turn:
          player = p_player1;
          break;

      case gs_player2_turn:
          player = p_player2;
          break;

      case gs_player3_turn:
          player = p_player3;
          break;

      default:
          player = p_neutral;
    }

    return player;
}

//----------------------------------------------------------------------

- (NSArray *) remainingCitiesForPlayer:(Player)number
{
    NSArray *remainingCities;

    NSAssert (players[number] != nil, @"We don't have that player.");
    remainingCities = [players[number] remainingCities];

    return remainingCities;
}

//----------------------------------------------------------------------

- (void) deactivatePlayer:(Player)number
{
    if (playersActive[number] == YES)
    {
        playersActive[number] = NO;

        SNRelease (players[number]);

        activePlayerCount--;

        //NSLog (@"active player count: %d", activePlayerCount);
        [[NSNotificationCenter defaultCenter] postNotificationName:EMPlayerStateChangedNotification
                                              object:self];

        // DGM does further cleanup after this.  Just make it do it before?
        if (activePlayerCount == 0)
        {
            [self theGameIsOver];
        }
    }
}

//----------------------------------------------------------------------

- (void) playerHasResigned:(Player)number activePlayers:(int)activePlayers
{
    NSAssert (players[number] != nil, @"We don't have that player.");

    [players[number] playerHasResigned:activePlayers];
}

//----------------------------------------------------------------------

// Only done in server.
- (void) storeFinalMapForPlayer:(Player)number
{
    NSAssert (finalMaps[number] == nil, @"Final map already set.");

    finalMaps[number] = [[self fetchMapForPlayer:number] retain];
}

//----------------------------------------------------------------------
// returns final map (if it exists i.e. player already done), or present map otherwise.
//----------------------------------------------------------------------

- (Map *) finalMapForPlayer:(Player)number
{
    Map *map = finalMaps[number];

    //NSLog (@"GM-player: %d", number);

    //NSLog (@"final map[%d]: %@", number, finalMaps[number]);

    if (map == nil)
        map = [self fetchMapForPlayer:number];

    //NSLog (@"map: %@", map);

    return map;
}

//----------------------------------------------------------------------

- (void) logStatus
{
    NSLog (@"foo.");
}

//----------------------------------------------------------------------

- (void) theGameIsOver
{
    //[self logStatus];
}

//----------------------------------------------------------------------
// Game Status support
//----------------------------------------------------------------------

- (NSString *) gameStatus
{
    NSString *gameStateNames[] = {
        @"No Game",
        @"Establishing Game",
        @"Client Active",
        @"Player 1 Turn",
        @"Player 2 Turn",
        @"Player 3 Turn",
        @"Game Over",
    };

    //NSLog (@"game state: %d", gameState);
    //NSLog (@"gs_no_game: %d", gs_no_game);

    return gameStateNames[gameState];
}

//----------------------------------------------------------------------

- (NSDictionary *) playerStatus:(Player)number
{
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];

    [dict setObject:playersActive[number] == YES ? @"Active" : @"Inactive" forKey:@"Status"];
    if (players[number] != nil)
        [dict setObject:[players[number] playerName] forKey:@"Name"];

    return dict;
}

//----------------------------------------------------------------------

- (void) setGameState:(GameState)newGameState
{
    gameState = newGameState;

    [[NSNotificationCenter defaultCenter] postNotificationName:EMGameStateChangedNotification
                                          object:self];
}

//----------------------------------------------------------------------

- (BOOL) validateMenuItem:(NSMenuItem *)menuCell
{
    SEL action = [menuCell action];
    BOOL valid = NO;

    if (action == @selector (showCrystalBall:))
    {
        valid = [self gameInProgress];
    }
    else if (action == @selector (toggleBerserk:))
    {
        valid = YES;
    }
    else if (action == @selector (stopGame:))
    {
        valid = [self gameInProgress];
    }

    return valid;
}

@end

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.