unit UGame;

{$mode delphi}{$H+}

interface

uses
  Classes, SysUtils, ExtCtrls, Graphics, XMLConf, XMLRead, XMLWrite, Forms,
  DOM, Math, LazFileUtils, UXMLUtils, Dialogs, LCLType, LCLIntf, fgl, UBuilding,
  UGeometry, UPlayer, UMap, UMapType, UUnit, UGameSystem;

const
  DefaultPlayerStartUnits = 5;
  MinPlayerCount = 1;
  MaxPlayerCount = 8;

type
  TGame = class;

  { TCanvasEx }

  TCanvasEx = class(TCanvas)
    class procedure TextOutEx(Canvas: TCanvas; X,Y: Integer; const Text: string; MovePen: Boolean = True);
    class procedure PolygonEx(Canvas: TCanvas; const Points: array of Classes.TPoint; Winding: Boolean);
    class procedure PolyLineEx(Canvas: TCanvas; const Points: array of Classes.TPoint);
    class procedure EllipseEx(Canvas: TCanvas; const ARect: TRect);
  end;

  TWinEvent = procedure(Player: TPlayer) of object;
  TGrowAmount = (gaByOne, gaBySquareRoot);
  TGrowCells = (gcNone, gcPlayerCities, gcPlayerAll);
  TWinObjective = (woNone, woDefeatAllOponents, woDefeatAllOponentsCities,
    woSpecialCaptureCell, woStayAliveForDefinedTurns, woCaptureEntireMap);

  { TGame }

  TGame = class
  private
    FMapType: TMapType;
    FOnChange: TNotifyEvent;
    FOnMoveUpdated: TMoveUpdatedEvent;
    FOnNewTurn: TNotifyEvent;
    FOnPlayerChange: TNotifyEvent;
    FOnStart: TNotifyEvent;
    FOnWin: TWinEvent;
    FRunning: Boolean;
    LoadedImageFileName: string;
    ProbabilityMatrix: array of array of Single;
    procedure RecordTurnStats;
    procedure SetMapType(AValue: TMapType);
    procedure SetRunning(AValue: Boolean);
    procedure BuildTerrain;
    procedure PlaceCities;
    procedure InitPlayers;
    procedure SelectPlayerStartCell(Player: TPlayer);
    procedure CalculatePlayersDistance;
    procedure PropagatePlayerDistance(List: TCells);
    procedure InitDefaultPlayers;
    procedure WinObjectiveMapPrepare;
  public
    GameSystem: TGameSystem;
    FileName: string;
    DevelMode: Boolean;
    Players: TPlayers;
    Units: TUnits;
    Buildings: TBuildings;
    Map: TMap;
    MapImageFileName: string;
    VoidEnabled: Boolean;
    VoidPercentage: Integer;
    SymetricMap: Boolean;
    CyclicMap: Boolean;
    GrowCells: TGrowCells;
    GrowAmount: TGrowAmount;
    CityEnabled: Boolean;
    CityPercentage: Integer;
    CurrentPlayer: TPlayer;
    TurnCounter: Integer;
    WinObjective: TWinObjective;
    SpecialCaptureCellCount: Integer;
    StayAliveForDefinedTurns: Integer;
    MaxNeutralUnits: Integer;
    FogOfWar: Boolean;
    BridgeEnabled: Boolean;
    MaxPower: Integer;
    StoredRandSeed: Cardinal;
    GeneratePlayers: Boolean;
    procedure PostConfig;
    procedure Assign(Source: TGame);
    function AttackProbability(AttackCount, DefendCount: Integer): Double;
    procedure LoadConfig(Config: TXmlConfig; Path: string);
    procedure SaveConfig(Config: TXmlConfig; Path: string);
    procedure LoadFromFile(FileName: string);
    procedure SaveToFile(FileName: string);
    procedure ComputePlayerStats;
    procedure NextPlayer;
    procedure CheckWinObjective;
    constructor Create;
    destructor Destroy; override;
    procedure Clear;
    procedure New;
    procedure EndGame(Winner: TPlayer = nil);
    property Running: Boolean read FRunning write SetRunning;
    property MapType: TMapType read FMapType write SetMapType;
  published
    property OnMoveUpdated: TMoveUpdatedEvent read FOnMoveUpdated write FOnMoveUpdated;
    property OnWin: TWinEvent read FOnWin write FOnWin;
    property OnNewTurn: TNotifyEvent read FOnNewTurn write FOnNewTurn;
    property OnPlayerChange: TNotifyEvent read FOnPlayerChange write FOnPlayerChange;
    property OnStart: TNotifyEvent read FOnStart write FOnStart;
    property OnChange: TNotifyEvent read FOnChange write FOnChange;
  end;

var
  PlayerModeText: array[TPlayerMode] of string;

const
  ComputerAggroProbability: array[TComputerAgressivity] of Single = (0.9, 0.7, 0.5);

procedure InitStrings;

resourcestring
  SPlayer = 'Player';
  SSpectator = 'Spectator';


implementation

resourcestring
  SMinimumPlayers = 'You need at least one player';
  SHuman = 'Human';
  SComputer = 'Computer';
  SWrongFileFormat = 'Wrong file format';
  SNewGameFile = 'New game.xtg';
  SUnsupportedMapType = 'Unsupported map type';

procedure InitStrings;
begin
  PlayerModeText[pmHuman] := SHuman;
  PlayerModeText[pmComputer] := SComputer;
end;

function HalfColor(Color: TColor): TColor;
begin
  Result :=
    ((((Color shr 0) and $ff) shr 1) shl 0) or
    ((((Color shr 8) and $ff) shr 1) shl 8) or
    ((((Color shr 16) and $ff) shr 1) shl 16) or
    ((((Color shr 24) and $ff) shr 0) shl 24);
end;

{ TCanvasEx }

class procedure TCanvasEx.TextOutEx(Canvas: TCanvas; X, Y: Integer; const Text: string;
  MovePen: Boolean);
var
  Flags : Cardinal;
begin
  with Canvas do begin
    Changing;
    RequiredState([csHandleValid, csFontValid, csBrushValid]);
    Flags := 0;
    if TextStyle.Opaque then
      Flags := ETO_Opaque;
    ExtUTF8Out(Handle, X, Y, Flags, nil, PChar(Text), Length(Text), nil);
    if MovePen then MoveTo(X + TextWidth(Text), Y);
    Changed;
  end;
end;

class procedure TCanvasEx.PolygonEx(Canvas: TCanvas; const Points: array of Classes.TPoint; Winding: Boolean);
begin
  //Changing;
  //RequiredState([csHandleValid, csBrushValid, csPenValid]);
  //Canvas.Brush.Style := bsClear;
  LCLIntf.Polygon(Canvas.Handle, @Points[0], Length(Points), Winding);
//  SetLength(Points, Length(Points) + 1);
//  Points[Length(Points) - 1] = Points[0];
//  LCLIntf.Polyline(Canvas.Handle, @Points[0], Length(Points));
  //Changed;
end;

class procedure TCanvasEx.PolyLineEx(Canvas: TCanvas;
  const Points: array of Classes.TPoint);
begin
  LCLIntf.Polyline(Canvas.Handle, @Points[0], Length(Points));
end;

class procedure TCanvasEx.EllipseEx(Canvas: TCanvas; const ARect: TRect);
begin
  LCLIntf.Ellipse(Canvas.Handle, ARect.P1.X, ARect.P1.Y, ARect.P2.X, ARect.P2.Y);
end;

{ TGame }

function TGame.AttackProbability(AttackCount, DefendCount: Integer): Double;
var
  OA, OD: Integer;
  Len: Integer;
  I: Integer;
begin
  if AttackCount < 0 then raise Exception.Create('Attack power needs to be possitive' + IntToStr(AttackCount));
  if AttackCount = 0 then begin
    Result := 0;
    Exit;
  end;
  if DefendCount < 0 then raise Exception.Create('Defend power needs to be possitive but is ' + IntToStr(DefendCount));
  if DefendCount = 0 then begin
    Result := 1;
    Exit;
  end;

  // Enlarge probability cache table on demand
  if Length(ProbabilityMatrix) < AttackCount then begin
    SetLength(ProbabilityMatrix, AttackCount);
  end;
  if Length(ProbabilityMatrix[AttackCount - 1]) < DefendCount then begin
    Len := Length(ProbabilityMatrix[AttackCount - 1]);
    SetLength(ProbabilityMatrix[AttackCount - 1], DefendCount);
    for I := Len to Length(ProbabilityMatrix[AttackCount - 1]) - 1 do
      ProbabilityMatrix[AttackCount - 1][I] := -1;
  end;

  if ProbabilityMatrix[AttackCount - 1, DefendCount - 1] <> -1 then begin
    // Use cached value
    Result := ProbabilityMatrix[AttackCount - 1, DefendCount - 1];
    Exit;
  end else Result := 1;

  OA := Min(AttackCount, 3);
  OD := Min(DefendCount, 2);

  if (OA = 1) and (OD = 1) then
    Result := 0.4167 * AttackProbability(AttackCount, DefendCount - 1) +
      0.5833 * AttackProbability(AttackCount - 1, DefendCount)
  else if (OA = 2) and (OD = 1) then
    Result := 0.5787 * AttackProbability(AttackCount, DefendCount - 1) +
      0.4213 * AttackProbability(AttackCount - 1, DefendCount)
  else if (OA = 3) and (OD = 1) then
    Result := 0.6597 * AttackProbability(AttackCount, DefendCount - 1) +
      0.3403 * AttackProbability(AttackCount - 1, DefendCount)
  else if (OA = 1) and (OD = 2) then
    Result := 0.2546 * AttackProbability(AttackCount, DefendCount - 1) +
      0.7454 * AttackProbability(AttackCount - 1, DefendCount)
  else if (OA = 2) and (OD = 2) then
    Result := 0.2276 * AttackProbability(AttackCount, DefendCount - 2) +
      0.4483 * AttackProbability(AttackCount - 2, DefendCount) +
      0.3241 * AttackProbability(AttackCount - 1, DefendCount - 1)
  else if (OA = 3) and (OD = 2) then
    Result := 0.3717 * AttackProbability(AttackCount, DefendCount - 2) +
      0.2926 * AttackProbability(AttackCount - 2, DefendCount) +
      0.3358 * AttackProbability(AttackCount - 1, DefendCount - 1);
  ProbabilityMatrix[AttackCount - 1, DefendCount - 1] := Result;
end;

procedure TGame.SetMapType(AValue: TMapType);
var
  OldMap: TMap;
begin
  if FMapType = AValue then Exit;
  OldMap := Map;
  case AValue of
    mtNone: Map := TMap.Create;
    mtHexagonVertical: Map := THexMapVertical.Create;
    mtSquare: Map := TSquareMap.Create;
    mtTriangle: Map := TTriangleMap.Create;
    mtRandom: Map := TVoronoiMap.Create;
    mtIsometric: Map := TIsometricMap.Create;
    mtHexagonHorizontal: Map := THexMapHorizontal.Create;
    else raise Exception.Create(SUnsupportedMapType);
  end;
  Map.Game := Self;
  Map.Assign(OldMap);
  FreeAndNil(OldMap);
  FMapType := AValue;
end;

procedure TGame.SetRunning(AValue: Boolean);
begin
  if FRunning = AValue then Exit;
  if AValue then begin
    if Players.Count < 1 then raise Exception.Create(SMinimumPlayers);
    FRunning := AValue;
  end else begin
    FRunning := AValue;
    if Assigned(FOnStart) then FOnStart(Self);
  end;
end;

procedure TGame.BuildTerrain;
var
  Cell: TCell;
  NewPower: Integer;
begin
  // Randomize map terrain
  for Cell in Map.Cells do
  with Cell do begin
    if (VoidEnabled and (Random < VoidPercentage / 100)) or
    (Map.IsOutsideShape(PosPx)) then Terrain := ttVoid
      else Terrain := ttNormal;
    NewPower := Random(MaxNeutralUnits + 1);
    if (NewPower > 0) and not Assigned(OneUnit) then begin
      OneUnit := Units.AddNew(TUnitKind(GameSystem.UnitKinds[0]), NewPower);
    end;
    Player := nil;
  end;
end;

procedure TGame.PlaceCities;
var
  Cell: TCell;
begin
  for Cell in Map.Cells do
  with Cell do begin
    if (Terrain = ttNormal) and CityEnabled and (Random < CityPercentage / 100) then begin
      Building := TBuilding(Buildings.AddItem('City'));
      Building.Kind := TBuildingKind(GameSystem.BuildingKinds.FindBySpecialType(stCity));
    end;
  end;
end;

procedure TGame.InitPlayers;
var
  I: Integer;
  Player: TPlayer;
begin
  if not GeneratePlayers then Exit;

  for I := 0 to Players.Count - 1 do begin
    TPlayer(Players[I]).Reset;
    TPlayer(Players[I]).StartCell := nil;
  end;
  for I := 0 to Players.Count - 1 do
  with TPlayer(Players[I]) do begin
    Player := TPlayer(Players[I]);
    PlayerMap.Update;
    if (Map.Size.X > 0) and (Map.Size.Y > 0) then begin
      SelectPlayerStartCell(Player);
      if Assigned(Player.StartCell) then begin
        if SymetricMap and (I = 1) then
          StartCell := Map.Cells[Map.Cells.Count - 1 - Map.Cells.IndexOf(TPlayer(Players[0]).StartCell)];

        if CityEnabled then begin
          StartCell.Building := TBuilding(Buildings.AddItem('City'));
          StartCell.Building.Kind := TBuildingKind(GameSystem.BuildingKinds.FindBySpecialType(stCity));
        end;
        StartCell.Player := Player;
        if not Assigned(StartCell.OneUnit) then
          StartCell.OneUnit := Self.Units.AddNew(TUnitKind(GameSystem.UnitKinds[0]), Player.StartUnits);
        StartCell.OneUnit.Power := Player.StartUnits;
        StartCell.OneUnit.Kind := TUnitKind(GameSystem.UnitKinds[0]);
        StartCell.OneUnit.Player := Player;
      end;
    end;
    InitUnitMoves;
    PlayerMap.CheckVisibility;
  end;
end;

procedure TGame.PostConfig;
begin
  if (Map.Shape = msImage) and FileExists(MapImageFileName) and
  (LoadedImageFileName <> MapImageFileName) then begin
    LoadedImageFileName := MapImageFileName;
    Map.Image.Picture.LoadFromFile(MapImageFileName);
  end;
end;

procedure TGame.SelectPlayerStartCell(Player: TPlayer);
var
  LongestDistance: Integer;
  Cell: TCell;
  List: TCells;
  I: Integer;
begin
  with Player do begin
    Map.Cells.ClearMark;
    Map.Cells.ClearWeight;
    CalculatePlayersDistance;

    // Calculate longest distance
    LongestDistance := 0;
    for Cell in Map.Cells do
      if (Cell.Terrain <> ttVoid) and (Cell.Weight > LongestDistance) then
        LongestDistance := Cell.Weight;

    List := TCells.Create;
    try
      List.FreeObjects := False;
      Map.Cells.GetCellsWithWeight(List, Round(LongestDistance * 0.6), Round(LongestDistance * 0.8));

      // Remove cells already allocated to different player
      for I := List.Count - 1 downto 0 do
        if Assigned(List[I].Player) then
          List.Delete(I);

      if List.Count > 0 then
        StartCell := List[Random(List.Count)];
    finally
      FreeAndNil(List);
    end;
  end;
end;

procedure TGame.CalculatePlayersDistance;
var
  Player: TPlayer;
  List: TCells;
  I: Integer;
begin
  for I := 0 to Players.Count - 1 do begin
    Player := TPlayer(Players[I]);
    if Assigned(Player.StartCell) then begin
      Player.StartCell.Weight := 1;
      Player.StartCell.Mark := True;
      List := TCells.Create;
      List.FreeObjects := False;
      List.Add(Player.StartCell);
      PropagatePlayerDistance(List);
      FreeAndNil(List);
    end;
  end;
end;

procedure TGame.PropagatePlayerDistance(List: TCells);
var
  NeighborCell: TCell;
  NeighborList: TCells;
  Cell: TCell;
begin
  NeighborList := TCells.Create;
  NeighborList.FreeObjects := False;

  for Cell in List do begin
    for NeighborCell in Cell.Neighbors do begin
      if (NeighborCell.Terrain <> ttVoid) and
        ((not NeighborCell.Mark) or (NeighborCell.Weight > Cell.Weight + 1)) then begin
        NeighborCell.Weight := Cell.Weight + 1;
        NeighborCell.Mark := True;
        NeighborList.Add(NeighborCell);
      end;
    end;
  end;
  if NeighborList.Count > 0 then
    PropagatePlayerDistance(NeighborList);
  FreeAndNil(NeighborList);
end;

procedure TGame.InitDefaultPlayers;
begin
  Players.Clear;
  Players.New(SPlayer + ' 1', clBlue, pmHuman);
  Players.New(SPlayer + ' 2', clRed, pmComputer);
end;

procedure TGame.WinObjectiveMapPrepare;
var
  Cell: TCell;
  Cells: TCells;
  I: Integer;
begin
  if WinObjective = woSpecialCaptureCell then begin
    Cells := TCells.Create(False);
    for I := 0 to Map.Cells.Count - 1 do
    if (Map.Cells[I].Terrain <> ttVoid) and (Map.Cells[I].Extra <> etObjectiveTarget) then
      Cells.Add(Map.Cells[I]);

    for I := 0 to SpecialCaptureCellCount - 1 do begin
      if Cells.Count = 0 then Break;
      Cell := Cells[Random(Cells.Count)];
      Cell.Extra := etObjectiveTarget;
      Cells.Remove(Cell);
    end;
    Cells.Free;
  end;
end;

procedure TGame.Assign(Source: TGame);
begin
  StoredRandSeed := Source.StoredRandSeed;
  DevelMode := Source.DevelMode;
  Players.Assign(Source.Players);
  MapType := Source.MapType;
  Map.Assign(Source.Map);
  MapImageFileName := Source.MapImageFileName;
  VoidEnabled := Source.VoidEnabled;
  VoidPercentage := Source.VoidPercentage;
  SymetricMap := Source.SymetricMap;
  CyclicMap := Source.CyclicMap;
  GrowCells := Source.GrowCells;
  GrowAmount := Source.GrowAmount;
  CityEnabled := Source.CityEnabled;
  CityPercentage := Source.CityPercentage;
  TurnCounter := Source.TurnCounter;
  WinObjective := Source.WinObjective;
  SpecialCaptureCellCount := Source.SpecialCaptureCellCount;
  StayAliveForDefinedTurns := Source.StayAliveForDefinedTurns;
  MaxNeutralUnits := Source.MaxNeutralUnits;
  FileName := Source.FileName;
  FogOfWar := Source.FogOfWar;
  BridgeEnabled := Source.BridgeEnabled;
  MaxPower := Source.MaxPower;
  GameSystem.Assign(Source.GameSystem);
end;

procedure TGame.SaveConfig(Config: TXmlConfig; Path: string);
begin
  with Config do begin
    SetValue(DOMString(Path + '/RandSeed'), Integer(StoredRandSeed));
    SetValue(DOMString(Path + '/GridType'), Integer(MapType));
    SetValue(DOMString(Path + '/MapImage'), DOMString(MapImageFileName));
    SetValue(DOMString(Path + '/SymetricMap'), SymetricMap);
    SetValue(DOMString(Path + '/CyclicMap'), CyclicMap);
    SetValue(DOMString(Path + '/FogOfWar'), FogOfWar);
    SetValue(DOMString(Path + '/VoidEnabled'), VoidEnabled);
    SetValue(DOMString(Path + '/VoidPercentage'), VoidPercentage);
    SetValue(DOMString(Path + '/MapSizeX'), Map.Size.X);
    SetValue(DOMString(Path + '/MapSizeY'), Map.Size.Y);
    SetValue(DOMString(Path + '/MapShape'), Integer(Map.Shape));
    SetValue(DOMString(Path + '/CityEnabled'), CityEnabled);
    SetValue(DOMString(Path + '/CityPercentage'), CityPercentage);
    SetValue(DOMString(Path + '/BridgeEnabled'), BridgeEnabled);
    SetValue(DOMString(Path + '/GrowAmount'), Integer(GrowAmount));
    SetValue(DOMString(Path + '/GrowCells'), Integer(GrowCells));
    SetValue(DOMString(Path + '/WinObjective'), Integer(WinObjective));
    SetValue(DOMString(Path + '/StayAliveForDefinedTurns'), StayAliveForDefinedTurns);
    SetValue(DOMString(Path + '/SpecialCaptureCellCount'), SpecialCaptureCellCount);
    SetValue(DOMString(Path + '/MaxNeutralUnits'), MaxNeutralUnits);
    SetValue(DOMString(Path + '/MaxPower'), MaxPower);
    Players.SaveConfig(Config, Path + '/Players');
    SetValue(DOMString(Path + '/GameSystemName'), DOMString(GameSystem.GetName));
  end;
end;

procedure TGame.LoadConfig(Config: TXmlConfig; Path: string);
var
  Value: Integer;
begin
  with Config do begin
    StoredRandSeed := GetValue(DOMString(Path + '/RandSeed'), 0);
    MapType := TMapType(GetValue(DOMString(Path + '/GridType'), Integer(mtHexagonVertical)));
    Map.Size := TPoint.Create(GetValue(DOMString(Path + '/MapSizeX'), 10),
      GetValue(DOMString(Path + '/MapSizeY'), 10));
    MapImageFileName := string(GetValue(DOMString(Path + '/MapImage'), DOMString(MapImageFileName)));
    SymetricMap := GetValue(DOMString(Path + '/SymetricMap'), False);
    CyclicMap := GetValue(DOMString(Path + '/CyclicMap'), False);
    FogOfWar := GetValue(DOMString(Path + '/FogOfWar'), False);
    VoidEnabled := GetValue(DOMString(Path + '/VoidEnabled'), True);
    VoidPercentage := GetValue(DOMString(Path + '/VoidPercentage'), 20);
    Value := GetValue(DOMString(Path + '/MapShape'), 0);
    if (Value >= Integer(Low(TMapShape))) and (Value <= Integer(High(TMapShape))) then
      Map.Shape := TMapShape(Value) else Map.Shape := Low(TMapShape);
    CityEnabled := GetValue(DOMString(Path + '/CityEnabled'), False);
    CityPercentage := GetValue(DOMString(Path + '/CityPercentage'), 10);
    BridgeEnabled := GetValue(DOMString(Path + '/BridgeEnabled'), True);
    Value := GetValue(DOMString(Path + '/GrowAmount'), Integer(gaBySquareRoot));
    if (Value >= Integer(Low(TGrowAmount))) and (Value <= Integer(High(TGrowAmount))) then
      GrowAmount := TGrowAmount(Value) else GrowAmount := Low(TGrowAmount);
    Value := GetValue(DOMString(Path + '/GrowCells'), Integer(gcPlayerAll));
    if (Value >= Integer(Low(TGrowCells))) and (Value <= Integer(High(TGrowCells))) then
      GrowCells := TGrowCells(Value) else GrowCells := Low(TGrowCells);
    Value := GetValue(DOMString(Path + '/WinObjective'), Integer(woDefeatAllOponents));
    if (Value >= Integer(Low(TWinObjective))) and (Value <= Integer(High(TWinObjective))) then
      WinObjective := TWinObjective(Value) else WinObjective := Low(TWinObjective);
    StayAliveForDefinedTurns := GetValue(DOMString(Path + '/StayAliveForDefinedTurns'), 20);
    SpecialCaptureCellCount := GetValue(DOMString(Path + '/SpecialCaptureCellCount'), 1);
    MaxNeutralUnits := GetValue(DOMString(Path + '/MaxNeutralUnits'), 5);
    MaxPower := GetValue(DOMString(Path + '/MaxPower'), 99);
    Players.LoadConfig(Config, Path + '/Players');
  end;
end;

procedure TGame.LoadFromFile(FileName: string);
var
  NewNode: TDOMNode;
  Doc: TXMLDocument;
  RootNode: TDOMNode;
  I: Integer;
begin
  ReadXMLFile(Doc, FileName);
  Self.FileName := FileName;
  Clear;
  with Doc do try
    if Doc.DocumentElement.NodeName <> 'XtacticsGame' then
      raise Exception.Create(SWrongFileFormat);
    RootNode := Doc.DocumentElement;
    with RootNode do begin
      StoredRandSeed := ReadInteger(RootNode, 'RandSeed', 0);
      MapType := TMapType(ReadInteger(RootNode, 'MapType', Integer(mtNone)));
      SymetricMap := ReadBoolean(RootNode, 'SymetricMap', False);
      CyclicMap := ReadBoolean(RootNode, 'CyclicMap', False);
      FogOfWar := ReadBoolean(RootNode, 'FogOfWar', False);
      VoidEnabled := ReadBoolean(RootNode, 'VoidEnabled', False);
      VoidPercentage := ReadInteger(RootNode, 'VoidPercentage', 0);
      MaxNeutralUnits := ReadInteger(RootNode, 'MaxNeutralUnits', 3);
      MaxPower := ReadInteger(RootNode, 'MaxPower', DefaultMaxPower);
      GrowCells := TGrowCells(ReadInteger(RootNode, 'GrowCells', Integer(gcNone)));
      GrowAmount := TGrowAmount(ReadInteger(RootNode, 'GrowAmount', Integer(gaByOne)));
      CityEnabled := ReadBoolean(RootNode, 'CityEnabled', False);
      CityPercentage := ReadInteger(RootNode, 'CityPercentage', 0);
      BridgeEnabled := ReadBoolean(RootNode, 'BridgeEnabled', False);
      TurnCounter := ReadInteger(RootNode, 'TurnCounter', 0);
      WinObjective := TWinObjective(ReadInteger(RootNode, 'WinObjective', Integer(woDefeatAllOponents)));
      StayAliveForDefinedTurns := ReadInteger(RootNode, 'StayAliveForDefinedTurns', 10);

      NewNode := FindNode('GameSystem');
      if Assigned(NewNode) then
        GameSystem.LoadFromNode(NewNode);

      NewNode := FindNode('Map');
      if Assigned(NewNode) then
        Map.LoadFromNode(NewNode);

      NewNode := FindNode('Players');
      if Assigned(NewNode) then
        Players.LoadFromNode(NewNode);
      if Players.Count > 0 then CurrentPlayer := TPlayer(Players[0])
        else CurrentPlayer := nil;

      NewNode := FindNode('Units');
      if Assigned(NewNode) then
        Units.LoadFromNode(NewNode);

      Map.Cells.FixRefId;
      Units.FixRefId;

      for I := 0 to Players.Count - 1 do begin
        TPlayer(Players[I]).PlayerMap.Update;
        TPlayer(Players[I]).PlayerMap.CheckVisibility;
      end;
      ComputePlayerStats;
      Running := ReadBoolean(RootNode, 'Running', True);
    end;
  finally
    FreeAndNil(Doc);
  end;
end;

procedure TGame.SaveToFile(FileName: string);
var
  NewNode: TDOMNode;
  Doc: TXMLDocument;
  RootNode: TDOMNode;
begin
  Self.FileName := FileName;
  Doc := TXMLDocument.Create;
  with Doc do try
    RootNode := CreateElement('XtacticsGame');
    AppendChild(RootNode);
    with RootNode do begin
      WriteInteger(RootNode, 'RandSeed', Integer(StoredRandSeed));
      WriteInteger(RootNode, 'MapType', Integer(MapType));
      WriteBoolean(RootNode, 'SymetricMap', SymetricMap);
      WriteBoolean(RootNode, 'CyclicMap', CyclicMap);
      WriteBoolean(RootNode, 'FogOfWar', FogOfWar);
      WriteBoolean(RootNode, 'VoidEnabled', VoidEnabled);
      WriteInteger(RootNode, 'VoidPercentage', VoidPercentage);
      WriteInteger(RootNode, 'MaxNeutralUnits', MaxNeutralUnits);
      WriteInteger(RootNode, 'MaxPower', MaxPower);
      WriteInteger(RootNode, 'GrowCells', Integer(GrowCells));
      WriteInteger(RootNode, 'GrowAmount', Integer(GrowAmount));
      WriteBoolean(RootNode, 'CityEnabled', CityEnabled);
      WriteInteger(RootNode, 'CityPercentage', CityPercentage);
      WriteBoolean(RootNode, 'BridgeEnabled', BridgeEnabled);
      WriteInteger(RootNode, 'TurnCounter', TurnCounter);
      WriteInteger(RootNode, 'WinObjective', Integer(WinObjective));
      WriteInteger(RootNode, 'StayAliveForDefinedTurns', StayAliveForDefinedTurns);
      WriteBoolean(RootNode, 'Running', Running);

      NewNode := OwnerDocument.CreateElement('GameSystem');
      AppendChild(NewNode);
      GameSystem.SaveToNode(NewNode);

      NewNode := OwnerDocument.CreateElement('Map');
      AppendChild(NewNode);
      Map.SaveToNode(NewNode);

      NewNode := OwnerDocument.CreateElement('Players');
      AppendChild(NewNode);
      Players.SaveToNode(NewNode);

      NewNode := OwnerDocument.CreateElement('Units');
      AppendChild(NewNode);
      Units.SaveToNode(NewNode);
    end;
    if ExtractFileDir(FileName) <> '' then
      ForceDirectories(ExtractFileDir(FileName));
    WriteXMLFile(Doc, FileName);
  finally
    FreeAndNil(Doc);
  end;
end;

procedure TGame.ComputePlayerStats;
var
  I: Integer;
  J: Integer;
begin
  for I := 0 to Players.Count - 1 do
  with TPlayer(Players[I]) do begin
    TotalUnits := 0;
    TotalCells := 0;
    TotalCities := 0;
    TotalWinObjectiveCells := 0;
    TotalDiscovered := 0;
    for J := 0 to PlayerMap.Cells.Count - 1 do
    with PlayerMap.Cells[J] do begin
      if Explored then Inc(TotalDiscovered);
    end;
  end;

  Map.ComputePlayerStats;
end;

procedure TGame.RecordTurnStats;
var
  I: Integer;
  NewStat: TGameTurnStat;
begin
  for I := 0 to Players.Count - 1 do
  with TPlayer(Players[I]) do begin
    NewStat := TGameTurnStat.Create;
    NewStat.DiscoveredCells := TotalDiscovered;
    NewStat.OccupiedCells := TotalCells;
    NewStat.Units := TotalUnits;
    NewStat.Cities := TotalCities;
    NewStat.WinObjectiveCells := TotalWinObjectiveCells;
    TurnStats.Add(NewStat);
  end;
end;

procedure TGame.NextPlayer;
var
  AlivePlayers: TPlayers;
  NewPlayerIndex: Integer;
begin
  {$IFDEF DEBUG}
  Map.CheckCells;
  {$ENDIF}

  // Finalize current player
  CurrentPlayer.MoveAll;
  CurrentPlayer.Grow;
  CurrentPlayer.UpdateEmptyCellsNeutral;
  CurrentPlayer.RemoveEmptyUnits;
  CurrentPlayer.UpdateRepeatMoves;
  ComputePlayerStats;

  // Select new player from alive players
  AlivePlayers := TPlayers.Create(False);
  try
    Players.GetAlivePlayers(AlivePlayers);
    NewPlayerIndex := AlivePlayers.IndexOf(CurrentPlayer) + 1;
    if NewPlayerIndex >= AlivePlayers.Count then begin
      // Start of turn
      Inc(TurnCounter);
      RecordTurnStats;
      if Assigned(FOnNewTurn) then
        FOnNewTurn(Self);
      NewPlayerIndex := NewPlayerIndex mod AlivePlayers.Count;
    end;
    CurrentPlayer := TPlayer(AlivePlayers[NewPlayerIndex]);
  finally
    AlivePlayers.Free;
  end;

  if Assigned(FOnPlayerChange) then
    FOnPlayerChange(Self);
  CheckWinObjective;
  CurrentPlayer.PlayerMap.CheckVisibility;
  CurrentPlayer.ReduceMovesPower;
  CurrentPlayer.RemoveInvalidMoves;
  CurrentPlayer.InitUnitMoves;
  if Assigned(FOnChange) then
    FOnChange(Self);
end;

procedure TGame.CheckWinObjective;
var
  AlivePlayers: TPlayerArray;
  Winner: TPlayer;
  Cells: TCells;
  Player: TPlayer;
  R: Boolean;
  I: Integer;
begin
  Winner := nil;
  if WinObjective = woDefeatAllOponents then begin
    AlivePlayers := Players.GetAlivePlayers;
    if (Length(AlivePlayers) <= 1) then begin
      if Length(AlivePlayers) > 0 then Winner := AlivePlayers[0];
      EndGame(Winner);
    end;
  end else
  if WinObjective = woDefeatAllOponentsCities then begin
    AlivePlayers := Players.GetAlivePlayersWithCities;
    if (Length(AlivePlayers) <= 1) then begin
      if Length(AlivePlayers) > 0 then Winner := AlivePlayers[0];
      EndGame(Winner);
    end;
  end else
  if WinObjective = woSpecialCaptureCell then begin
    Cells := TCells.Create(False);
    Map.Cells.GetCellsWithExtra(Cells, etObjectiveTarget);
    R := True;
    for I := 0 to Cells.Count - 1 do begin
      if I = 0 then Player := TPlayer(Cells[I].Player);
      if not Assigned(Cells[I].Player) then begin
        R := False;
        Break;
      end;
      if (Cells[I].Player <> Player) then begin
        R := False;
        Break;
      end;
    end;
    if R then EndGame(Player);
    Cells.Free;
  end else
  if WinObjective = woStayAliveForDefinedTurns then begin
    if TurnCounter > StayAliveForDefinedTurns then
      EndGame(nil);
  end else
  if WinObjective = woCaptureEntireMap then begin
    Player := nil;
    for I := 0 to Map.Cells.Count - 1 do
    if TCell(Map.Cells[I]).Terrain <> ttVoid then begin
      if (TCell(Map.Cells[I]).Player <> nil) then begin
        if Player = nil then begin
          // First player found
          Player := TPlayer(TCell(Map.Cells[I]).Player);
        end else
        if Player <> TCell(Map.Cells[I]).Player then begin
          // Multiple players still alive
          Player := nil;
          Break;
        end;
      end else begin
        // Neutral cell
        Player := nil;
        Break;
      end;
    end;
    if Player <> nil then
      EndGame(Player);
  end;
end;

constructor TGame.Create;
begin
  GameSystem := TGameSystem.Create;
  Units := TUnits.Create;
  Units.Game := Self;
  Buildings := TBuildings.Create;
  Buildings.Game := Self;
  Map := TMap.Create;
  Map.Game := Self;
  Players := TPlayers.Create;
  Players.Game := Self;

  GeneratePlayers := True;
  MapImageFileName := 'Images/Maps/WorldMap.png';
  Randomize;
  StoredRandSeed := RandSeed;
  InitDefaultPlayers;

  VoidEnabled := True;
  VoidPercentage := 20;
  MaxPower := DefaultMaxPower;
  MaxNeutralUnits := Min(4, MaxPower);

  Map.Size := TPoint.Create(3, 3);
end;

destructor TGame.Destroy;
begin
  FreeAndNil(Players);
  FreeAndNil(Map);
  FreeAndNil(Buildings);
  FreeAndNil(Units);
  FreeAndNil(GameSystem);
  inherited Destroy;
end;

procedure TGame.Clear;
var
  I: Integer;
begin
  for I := 0 to Players.Count - 1 do TPlayer(Players[I]).Clear;
  Map.Clear;
end;

procedure TGame.New;
begin
  Clear;
  RandSeed := StoredRandSeed;
  FileName := SNewGameFile;
  TurnCounter := 1;

  Map.Cyclic := CyclicMap;
  Map.Generate;
  Map.MaxPower := MaxPower;
  BuildTerrain;
  PlaceCities;
  WinObjectiveMapPrepare;

  // Build bridges
  if BridgeEnabled then Map.CreateLinks;

  if SymetricMap then begin
    Map.MakeSymetric;
    if BridgeEnabled then Map.CreateLinks;
  end;

  InitPlayers;
  if Players.Count > 0 then CurrentPlayer := TPlayer(Players[0])
    else CurrentPlayer := nil;

  ComputePlayerStats;
end;

procedure TGame.EndGame(Winner: TPlayer = nil);
begin
  Running := False;
  if Assigned(OnWin) then OnWin(Winner);
end;

end.
