unit board;

interface

uses Winprocs, Wintypes, Objects, OWindows, ChPieces, Chessdll,
     chconst;

type
  PChessBoard = ^TChessBoard;
  TChessBoard = object(TWindow)
    Game: HChess;
    Squares: array [1..8,1..8] of PChessPiece;
    Pieces: TCollection;
    BoardBitmap: HBitmap;
    WhiteColor: TColorRef;
    BlackColor: TColorRef;
    WhiteBrush: HBrush;
    BlackBrush: HBrush;
    SquareWidth: Word;
    Dragger: PChessPiece;    { if <> nil, we're dragging it }
    BoardDC, DragDC: HDC;
    BoardOldBM, DragBM : HBitmap;
    ValidMoves,
    OpponentMoves: array [0..(28*16+1)] of TMove;
    { Setup and shutdown }
    constructor Init(AParent: PWindowsObject; GH: HChess);
    destructor Done; virtual;
    function  GetClassName: PChar; virtual;
    procedure GetWindowClass(var WC: TWndClass); virtual;
    procedure ResetBoard(GH: HChess);

    { Board display }
    function  IdealWidth: Word;
    procedure InitBoardBitmap;
    procedure DrawBoard;
    procedure Paint(DC: HDC; var PS: TPaintStruct); virtual;
    procedure WMEraseBkgnd(var Msg: TMessage);
      virtual wm_First + wm_EraseBkgnd;
    procedure WMSize(var Msg: TMessage);
      virtual wm_First + wm_Size;

    { Conversions }
    function  PieceFromPoint(Pt: TPoint): PChessPiece;
    procedure SquareFromPoint(Pt: TPoint; var Sq: TLocation);
    procedure SquareToRect(Sq: TLocation; var R: TRect);

    { Piece management }
    procedure InsertPiece(Sq: TLocation; P: PChessPiece);
    function  RemovePiece(Sq: TLocation): PChessPiece;
    procedure ExecuteMove(const Move: TMove);
    procedure RetractMove(const Move: TMove);
    procedure ResetValidMoves;
    procedure FreshenPiece(P : PChessPiece);

    { Piece dragging routines }
    procedure WMSetCursor(var Msg: TMessage);
      virtual wm_First + wm_SetCursor;
    procedure WMLButtonDown(var Msg: TMessage);
      virtual wm_First + wm_LButtonDown;
    procedure WMMouseMove(var Msg: TMessage);
      virtual wm_First + wm_MouseMove;
    procedure WMLButtonUp(var Msg: TMessage);
      virtual wm_First + wm_LButtonUp;
  end;

  function OtherPlayer(Color: TColor): TColor;

implementation

uses AppUtils, ChessDlg;

function OtherPlayer(Color: TColor): TColor;
begin
  if Color = cWhite then
    OtherPlayer := cBlack
  else
    OtherPlayer := cWhite;
end;

constructor TChessBoard.Init(AParent: PWindowsObject; GH: HChess);
begin
  inherited Init(AParent, nil);
  with Attr do
  begin
    X := 0;
    Y := 0;
    W := 200;
    H := 200;
    Style := ws_Child {or ws_Border};
        { NOT ws_Visible - the parent window will resize us  }
        { to the ideal width and then show us.               }
  end;
  BoardBitmap := 0;
  DragDC := 0;
  BoardDC := CreateMemoryDC;
  Dragger := nil;
  WhiteColor := XApp^.GetAppProfileRGB(
                       'Board','WhiteColor',RGB(255,255,255));
  WhiteBrush := CreateSolidBrush(WhiteColor);
  BlackColor := XApp^.GetAppProfileRGB(
                       'Board','BlackColor',RGB(255,0,0));
  BlackBrush := CreateSolidBrush(BlackColor);
  Pieces.Init(32, 4);  { Growth allows for edited boards with > 32 pieces }
  ResetBoard(GH);
end;

destructor TChessBoard.Done;
var
  Temp: array [0..15] of Char;
begin
  inherited Done;
  Pieces.Done;
  if BoardDC <> 0 then
  begin
    SelectObject(BoardDC, BoardOldBM);
    DeleteDC(BoardDC);
  end;
  if DragDC <> 0 then
  begin
    DeleteObject(SelectObject(DragDC, DragBM));
    DeleteDC(DragDC);
  end;
  if BoardBitmap <> 0 then
    DeleteObject(BoardBitmap);
  DeleteObject(WhiteBrush);
  DeleteObject(BlackBrush);
  XApp^.WriteAppProfileRGB('Board','WhiteColor',WhiteColor);
  XApp^.WriteAppProfileRGB('Board','BlackColor',BlackColor);
end;

function  TChessBoard.GetClassName: PChar;
begin
  GetClassName := 'TPWOWLChessBoard';
end;

procedure TChessBoard.GetWindowClass(var WC: TWndClass);
begin
  inherited GetWindowClass(WC);
  WC.Style := cs_ByteAlignWindow;
  WC.hCursor := 0;
end;

procedure TChessBoard.ResetBoard(GH: HChess);
  procedure DoResize(P : PChessPiece); far;
  var
    R: TRect;
    S: TLocation;
  begin
    P^.GetSquare(S);
    SquareToRect(S, R);
    P^.SetRect(R);
  end;
var
  TempBoard: TBoard;
  Square: TLocation;
begin
  Game := GH;
  Pieces.FreeAll;
  FillChar(Squares, SizeOf(Squares), 0);

  GetBoard(Game, TempBoard);

  SquareWidth := Attr.W div 8;
  for Square.X := 1 to 8 do
    for Square.Y := 1 to 8 do
      if (TempBoard[Square.X, Square.Y].Piece <> pEmpty) then
      begin
        Squares[Square.X,Square.Y] := New(PChessPiece,
             Init(@Self, TempBoard[Square.X, Square.Y], Square));
        Pieces.Insert(Squares[Square.X,Square.Y]);
      end;
  ResetValidMoves;
  if HWindow <> 0 then
  begin
    Pieces.ForEach(@DoResize);
    DrawBoard;
    InvalidateRect(HWindow, nil, False);
  end;
end;

function TChessBoard.IdealWidth: Word;
var
  Best: Word;
  procedure CheckBitmapSize(P: PChessPiece); far;
  begin
    if Best < P^.BitSize.X then Best := P^.BitSize.X;
    if Best < P^.BitSize.Y then Best := P^.BitSize.Y;
  end;
begin
  Best := 0;
  Pieces.ForEach(@CheckBitmapSize);
  IdealWidth := (Best + 4) * 8;
end;

procedure TChessBoard.InitBoardBitmap;
var
  DC: HDC;
begin
  if BoardBitmap <> 0 then
  begin
    SelectObject(BoardDC, BoardOldBM);
    DeleteObject(BoardBitmap);
  end;

  DC := GetDC(HWindow);
  BoardBitmap := CreateCompatibleBitmap(DC, Attr.W, Attr.H);
  ReleaseDC(HWindow, DC);
  BoardOldBM := SelectObject(BoardDC, BoardBitmap);
  SquareWidth := Attr.W div 8;
end;

procedure TChessBoard.DrawBoard;
var
  OldBrush, SquareBrush : HBrush;
  X, Y: Integer;

  procedure DoPaint(P: PChessPiece); far;
  begin
    P^.Paint(BoardDC);
  end;

begin
  OldBrush := SelectObject(BoardDC, CreateSolidBrush(WhiteColor));
  PatBlt(BoardDC, 0, 0, Attr.W, Attr.H, PatCopy);

  DeleteObject(SelectObject(BoardDC, CreateSolidBrush(BlackColor)));
  for Y := 0 to 7 do
    for X := 0 to 7 do
      if Odd(X + Y) then
        PatBlt(BoardDC, X * SquareWidth, Y * SquareWidth,
                      SquareWidth, SquareWidth, PatCopy);

  DeleteObject(SelectObject(BoardDC, OldBrush));
  Pieces.ForEach(@DoPaint);
end;

{ Because of the way the board paints from a memory bitmap, we don't
  need the window to erase the background before we paint.  }
procedure TChessBoard.WMEraseBkgnd(var Msg: TMessage);
begin
  Msg.Result := 1;
end;

procedure TChessBoard.Paint(DC: HDC; var PS: TPaintStruct);
  procedure CheckPieces(P: PChessPiece); far;
  var
    Sq: TLocation;
    OldBrush: HBrush;
  begin
    if P^.NeedRedraw then
    begin
      P^.GetSquare(Sq);
      if Odd(Sq.X + Sq.Y) then
        OldBrush := SelectObject(BoardDC, WhiteBrush)
      else
        OldBrush := SelectObject(BoardDC, BlackBrush);
      with P^.Rect do
        PatBlt(BoardDC, Left, Top, Right - Left, Bottom - Top, PatCopy);
      SelectObject(BoardDC, OldBrush);
      P^.Paint(BoardDC);
    end;
  end;
begin
  Pieces.ForEach(@CheckPieces);
  with PS.rcPaint do
    BitBlt(DC, Left, Top, Right - Left, Bottom - Top,
           BoardDC, Left, Top, SrcCopy);
  if Dragger <> nil then
    Dragger^.Paint(DC);
end;

procedure TChessBoard.WMSize(var Msg: TMessage);

  procedure DoResize(P : PChessPiece); far;
  var
    R: TRect;
    S: TLocation;
  begin
    P^.GetSquare(S);
    SquareToRect(S, R);
    P^.SetRect(R);
  end;

begin
  inherited WMSize(Msg);
  SquareWidth := Attr.W div 8;
  InitBoardBitmap;
  Pieces.ForEach(@DoResize);
  DrawBoard;
end;

function TChessBoard.PieceFromPoint(Pt: TPoint): PChessPiece;
  function DoHitTest(P: PChessPiece): Boolean; far;
  begin
    DoHitTest := P^.HitTest(Pt);
  end;
begin
  PieceFromPoint := PChessPiece(Pieces.FirstThat(@DoHitTest));
end;

procedure TChessBoard.SquareFromPoint(Pt: TPoint; var Sq: TLocation);
begin
  Sq.X := (Pt.X div SquareWidth) + 1;
  Sq.Y := (Attr.H - Pt.Y) div SquareWidth + 1;
end;

procedure TChessBoard.SquareToRect(Sq: TLocation; var R: TRect);
begin
  R.Left   := (Sq.X - 1) * SquareWidth;
  R.Right  := R.Left + SquareWidth;
  R.Top    := Attr.H - (Sq.Y * SquareWidth);
  R.Bottom := R.Top + SquareWidth;
end;

procedure TChessBoard.ExecuteMove(const Move: TMove);

  function  CreatePromote(P: TPiece; Dest: TLocation): PChessPiece;
  var                    { This function creates the piece specified by }
    X: TSquare;          { P using color info from the piece already on }
  begin                  { on the board at Dest.  This is for           }
    X.Piece := P;        { Pawn Promotion moves only.                   }
    X.Color := Squares[Dest.X, Dest.Y]^.Color;
    InsertPiece(Dest, New(PChessPiece, Init(@Self, X, Dest)));
  end;

begin
  if Move.Change.Piece = pEmpty then Exit;
  with Move, Move.Change do
  begin
    InsertPiece(Dest, RemovePiece(Source)); { Also deletes what's at dest }
    case Move.Kind of
      kEnPassant  : Dispose(RemovePiece(EPCapture), Done);
      kCastling   : InsertPiece(RookDest, RemovePiece(RookSource));
      kPawnPromote: CreatePromote(Piece, Dest);
    end;
  end;
end;

procedure TChessBoard.RetractMove(const Move: TMove);
  procedure CreatePiece(P: TPiece; Color: Boolean; Dest: TLocation);
  var
    X: TSquare;
  begin
    X.Piece := P;
    X.Color := TColor(Color);
    InsertPiece(Dest, New(PChessPiece, Init(@Self, X, Dest)));
  end;
var
  Color: Boolean;   { Color of opponent }
begin
  if Move.Change.Piece = pEmpty then Exit;
  with Move, Move.Change do
  begin
    Color := not Boolean(Squares[Dest.X, Dest.Y]^.Color);
    InsertPiece(Source, RemovePiece(Dest)); {Back out of destination }
    case Move.Kind of
      kNormal     : if Capture then CreatePiece(Contents, Color, Dest);
      kEnPassant  : CreatePiece(Contents, Color, EPCapture);
      kCastling   : InsertPiece(RookSource, RemovePiece(RookDest));
      kPawnPromote:
        begin
          if Capture then CreatePiece(Contents, not Color, Dest);
          CreatePiece(pPawn, Color, Source);
        end;
    end;
  end;
end;


procedure TChessBoard.ResetValidMoves;
var
  Chg: TChange;
  PlayerColor: TColor;
  EmptyMove: TMove;

  procedure DoValids(P : PChessPiece); far;
  begin
    if P^.Color = PlayerColor then
      P^.ResetValidMoves(ValidMoves)  { piece gets its moves from list }
    else
    begin
      P^.ResetValidMoves(EmptyMove);  { clear opponent's move lists }
      if ChessSettings.ShowAttacks then
        P^.CheckJeopardy(ValidMoves);
    end;
  end;

  procedure DoJeopardies(P : PChessPiece); far;
  begin
    if P^.Color = PlayerColor then
      P^.CheckJeopardy(OpponentMoves);
  end;

begin
  Chg.Piece := pEmpty;
  Word(Chg.Source) := 0;
  Word(Chg.Dest) := 0;
  FillChar(EmptyMove, SizeOf(EmptyMove), 0);
  PlayerColor := GetPlayer(Game);
  if ChessSettings.ShowJeopardies then
  begin
    { Switch players to see which opponent pieces attack ours }
    SetPlayer(Game, OtherPlayer(PlayerColor));
    GetValidMoves(Game, Chg, OpponentMoves);
    SetPlayer(Game, PlayerColor);
    Pieces.ForEach(@DoJeopardies);
  end;
  { Now see what moves our pieces can make }
  GetValidMoves(Game, Chg, ValidMoves);
  Pieces.ForEach(@DoValids);
end;

procedure TChessBoard.FreshenPiece(P:PChessPiece);
var
  TempMoves: array [0..28] of TMove;
  PlayerColor: TColor;
  Chg: TChange;
  EmptyMove: TMove;
begin
  Chg.Piece := pEmpty;
  Word(Chg.Source) := 0;
  Chg.Dest := P^.Square;
  FillChar(EmptyMove, SizeOf(EmptyMove), 0);
  PlayerColor := GetPlayer(Game);
  if ChessSettings.ShowJeopardies then
  begin
    { Switch players to see which opponent pieces attack ours }
    SetPlayer(Game, OtherPlayer(PlayerColor));
    GetValidMoves(Game, Chg, TempMoves);
    SetPlayer(Game, PlayerColor);
    P^.CheckJeopardy(TempMoves);
  end;
end;

procedure TChessBoard.WMSetCursor(var Msg: TMessage);
var
  P: TPoint;
  X: PChessPiece;
begin
  DefWndProc(Msg);
  if Msg.Result = 0 then
  begin
    GetCursorPos(P);
    ScreenToClient(HWindow, P);
    X := PieceFromPoint(P);
    if (X <> nil) and X^.CanDrag then
      SetCursor(X^.GetCursor)
    else
      SetCursor(LoadCursor(0, PChar(idc_Arrow)));
  end;
end;

procedure TChessBoard.WMLButtonDown(var Msg: TMessage);
var
   R: TRect;
  Sq: TLocation;
  DC: HDC;
begin
  if Dragger = nil then
  begin
    Dragger := PieceFromPoint(TPoint(Msg.LParam));
    if Dragger <> nil then
      if Dragger^.CanDrag then
      begin
        Dragger^.GetSquare(Sq);
        RemovePiece(Sq);
        SetCapture(HWindow);
        DC := GetDC(HWindow);
        DragDC := CreateCompatibleDC(DC);
        DragBM := SelectObject(DragDC,
                    CreateCompatibleBitmap(DC, Attr.W, Attr.H));
        BitBlt(DragDC, 0, 0, Attr.W, Attr.H, BoardDC, 0, 0, SrcCopy);
        R := Dragger^.Rect;
        Dragger^.DragBegin(DragDC, TPoint(Msg.LParam));
        UnionRect(R, R, Dragger^.Rect);
        with R do
          BitBlt(DC, Left, Top, Right - Left, Bottom - Top,
                 DragDC, Left, Top, SrcCopy);
        ReleaseDC(HWindow, DC);
      end
      else
      begin
        Dragger := nil;
        MessageBeep(0);
      end;
  end;
  DefWndProc(Msg);
end;

procedure TChessBoard.WMMouseMove(var Msg: TMessage);
var
   R: TRect;
  Sq: TLocation;
  DC: HDC; 
begin
  if Dragger <> nil then
  begin
    GetClientRect(HWindow, R);
    if PtInRect(R, TPoint(Msg.LParam)) then
    begin
      SquareFromPoint(TPoint(Msg.LParam), Sq);
      with Dragger^.Rect do
        BitBlt(DragDC, Left, Top, Right - Left, Bottom - Top,
               BoardDC, Left, Top, SrcCopy);
      R := Dragger^.Rect;
      Dragger^.DragContinue(DragDC, TPoint(Msg.LParam), Sq);
      UnionRect(R, R, Dragger^.Rect);
      DC := GetDC(HWindow);
      with R do
        BitBlt(DC, Left, Top, Right - Left, Bottom - Top,
               DragDC, Left, Top, SrcCopy);
      ReleaseDC(HWindow, DC);
    end
    else
    begin
      Dragger^.DragHide;
      InvalidateRect(HWindow, @Dragger^.Rect, False);
      SetCursor(LoadCursor(GetModuleHandle('User'), PChar(idc_No)));
    end;
  end;
  DefWndProc(Msg);
end;

procedure TChessBoard.WMLButtonUp(var Msg: TMessage);
var
  NewSq, OldSq: TLocation;
  R: TRect;
  Chg: TChange;
  ValidMove: Boolean;
  PlayerColor : TColor;
begin
  if Dragger <> nil then
  begin
    GetClientRect(HWindow, R);
    with Dragger^.Rect do
      BitBlt(DragDC, Left, Top, Right - Left, Bottom - Top,
             BoardDC, Left, Top, SrcCopy);
    if PtInRect(R, TPoint(Msg.LParam)) then
      SquareFromPoint(TPoint(Msg.LParam), NewSq)
    else
    begin
      NewSq.X := 0;     { 0 = off board or invalid }
      NewSq.Y := 0;
    end;
    R := Dragger^.Rect;
    Dragger^.GetSquare(OldSq);
    ValidMove := Dragger^.DragEnd(DragDC, TPoint(Msg.LParam), NewSq, Chg);
    InvalidateRect(HWindow, @R, False);
    InsertPiece(OldSq, Dragger);  { Go back to original square }
    Dragger := nil;
    ReleaseCapture;
    DeleteObject(SelectObject(DragDC, DragBM));
    DeleteDC(DragDC);
    DragDC := 0;
    if (Chg.Piece = pPawn) and (VerifyMove(Game, Chg) = ceAmbiguousMove) then
      Chg.Piece := pQueen;
            { am_SubmitMove will return a boolean accept/reject response }
    if ValidMove and
       LongBool(SendMessage(Parent^.HWindow, am_SubmitMove, 0, Longint(@Chg))) then
    begin
        { After Submitmove, player color has switched.  We need to temporarily
          switch it back to our color for the following operations. }
      PlayerColor := GetPlayer(Game);
      SetPlayer(Game, OtherPlayer(PlayerColor));
      if ChessSettings.ShowAttacks then
        { Reset all pieces' valid moves, so that opponents that were attacked by
          the moved piece's former position can be cleared as well as note what
          opponent pieces are now attacked in the new position. }
        ResetValidMoves
      else
      if ChessSettings.ShowJeopardies then
        { For jeopardies, we just need to see who attacks the new square.
          FreshenPiece is must faster and simpler than ResetValidMoves. }
        FreshenPiece(Squares[NewSq.X,NewSq.Y]);
      SetPlayer(Game, PlayerColor);
    end;
    UpdateWindow(HWindow);
  end;
  DefWndProc(Msg);
end;

procedure TChessBoard.InsertPiece(Sq: TLocation; P: PChessPiece);
var
  R: TRect;
begin
  if Squares[Sq.X,Sq.Y] = P then Exit;
  if (Squares[Sq.X,Sq.Y] <> nil) then
    Dispose(RemovePiece(Sq), Done);
  Pieces.Insert(P);
  P^.SetSquare(Sq);
  Squares[Sq.X, Sq.Y] := P;
  SquareToRect(Sq, R);
  P^.SetRect(R);
  P^.Paint(BoardDC);
  InvalidateRect(HWindow, @R, False);
end;

function TChessBoard.RemovePiece(Sq: TLocation): PChessPiece;
var
  OldBrush: HBrush;
  R: TRect;
begin
  RemovePiece := nil;
  if Squares[Sq.X,Sq.Y] <> nil then
  begin
    RemovePiece := Squares[Sq.X,Sq.Y];
    Pieces.Delete(Squares[Sq.X,Sq.Y]);
    Squares[Sq.X,Sq.Y] := nil;

    if Odd(Sq.X + Sq.Y) then
      OldBrush := SelectObject(BoardDC, WhiteBrush)
    else
      OldBrush := SelectObject(BoardDC, BlackBrush);
    SquareToRect(Sq, R);
    with R do
      PatBlt(BoardDC, Left, Top, Right - Left, Bottom - Top, PatCopy);
    SelectObject(BoardDC, OldBrush);
    InvalidateRect(HWindow, @R, False);
  end;
end;

end.