{ Puzzle! is a puzzle game using BGRABitmap

  originally written in 2012 by lainz http://sourceforge.net/users/lainz

  Thanks to circular and eny from the Lazarus forum for their contributions.

  This library is free software; you can redistribute it and/or modify it
  under the terms of the GNU Library 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 Library General Public License
  for more details.

  You should have received a copy of the GNU Library General Public License
  along with this library; if not, write to the Free Software Foundation,
  Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
}
unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs,
  BGRAVirtualScreen, BGRABitmap, BGRABitmapTypes, ugame, LCLType,
  ExtCtrls{$IFDEF WINDOWS},
  mmsystem{$ENDIF};

const
  // controls
  L1 = VK_LEFT;
  R1 = VK_RIGHT;
  U1 = VK_UP;
  D1 = VK_DOWN;
  L2 = VK_A;
  R2 = VK_D;
  U2 = VK_W;
  D2 = VK_S;

type
  TArrayOfString = array of string;

  TGameKey = (gkLeft, gkUp, gkRight, gkDown);

  PGameMap = ^TGameMap;

  TGameMap = record
    map, solve: string;
    mapW: integer;
    mapH: integer;
    blockW: integer;
    blockH: integer;
    background: TBGRABitmap;
  end;

var
  Game: array [1..12] of TGameMap;
  GameOver: TGameMap = (map: 'Game-Over'; solve: 'GameOver-'; mapW: 3;
  mapH: 3; blockW: 85; blockH: 85; background: nil);

procedure DrawMap(var map: TGameMap; bitmap: TBGRABitmap; texture: TBGRABitmap);
function ClickMap(var map: TGameMap; pos: TPoint): boolean;
function KeyPressMap(var map: TGameMap; var Key: word): boolean;
function IsMapSolved(var map: TGameMap): boolean;

function CalcularPosibles(str: string; showDebug: boolean = False): TarrayOfString;

type

  { TForm1 }

  TForm1 = class(TForm)
    BGRAVirtualScreen1: TBGRAVirtualScreen;
    Timer1: TTimer;
    procedure BGRAVirtualScreen1Click(Sender: TObject);
    procedure BGRAVirtualScreen1Redraw(Sender: TObject; Bitmap: TBGRABitmap);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormKeyDown(Sender: TObject; var Key: word; Shift: TShiftState);
    procedure FormResize(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    { private declarations }
  public
    { public declarations }
    Size: TRect;
    map: PGameMap;
    current: integer;
    texture: TBGRABitmap;
    procedure OpenMap(num: integer);
    procedure CloseMap;
    procedure ScaleBlocks;
    procedure GenerateTexture;
    procedure PlayBlockSound;
  end;

var
  Form1: TForm1;

implementation

uses BGRAGradients, ugraph;

{ DRAWMAP }

procedure DrawMap(var map: TGameMap; bitmap: TBGRABitmap; texture: TBGRABitmap);
var
  r: TRect;
  n, n1, n2: integer;
  phong: TPhongShading;
  tile3D, empty3D: TBGRABitmap;
  h: integer;
begin
  if (bitmap = nil) or (texture = nil) then
    exit;
  phong := TPhongShading.Create;

  if map.background = nil then
  begin
    map.background := TBGRABitmap.Create(map.BlockW * map.mapW, map.BlockH * map.mapH);
    empty3D := TBGRABitmap.Create(texture.Width, texture.Height, BGRABlack);
    for n1 := 1 to (map.background.Width + texture.Width - 1) div texture.Width do
      for n2 := 1 to (map.background.Height + texture.Height - 1) div texture.Height do
        phong.Draw(map.background, empty3D, 0, (n1 - 1) * texture.Width,
          (n2 - 1) * texture.Height, texture);
    empty3D.Free;
  end;
  h := (map.blockW + map.blockH) div 16;
  tile3D := CreateRectanglePreciseMap(map.BlockW, map.BlockH, h, []);

  bitmap.PutImage(0, 0, map.background, dmDrawWithTransparency);
  n1 := 0;
  n2 := 0;
  for n := 1 to length(map.map) do
  begin
    r.Left := map.blockW * n1;
    r.Top := map.blockH * n2;
    r.Right := r.Left + map.blockW;
    r.Bottom := r.Top + map.blockH;

    // begin to draw here //

    if map.map[n] <> '0' then
    begin
      phong.Draw(bitmap, tile3D, h, r.left, r.top, texture);

      if map.blockH > map.blockW then
        bitmap.FontHeight := round(map.blockW * 0.75)
      else
        bitmap.FontHeight := round(map.blockH * 0.75);

      bitmap.TextRect(Rect(r.Left + 10, r.Top + 10, r.Right, r.Bottom),
        map.map[n], taCenter, tlCenter, BGRA(0, 0, 0, 175));
      bitmap.TextRect(r, map.map[n], taCenter, tlCenter, BGRABlack);
    end;
   {
    if map.map[n] = '0' then
    begin
      bitmap.FontHeight := map.blockH div 4;
      if (n1 <> 0) then
        bitmap.TextRect(r, '►', taLeftJustify, tlCenter, BGRA(192, 192, 192));

      if (n1 <> map.mapW - 1) then
        bitmap.TextRect(r, '◄', taRightJustify, tlCenter, BGRA(192, 192, 192));

      if (n2 <> 0) then
        bitmap.TextRect(r, '▼', taCenter, tlTop, BGRA(192, 192, 192));

      if (n2 <> map.mapH - 1) then
        bitmap.TextRect(r, '▲', taCenter, tlBottom, BGRA(192, 192, 192));
    end;
    }

    // end to draw here //

    if n1 = map.mapW - 1 then
    begin
      n1 := -1;
      Inc(n2);
    end;

    Inc(n1);
  end;

  tile3D.Free;
  phong.Free;
end;

function ClickMap(var map: TGameMap; pos: TPoint): boolean;
var
  n, n1, n2: integer;
  r: TRect;
begin
  Result := False;
  n1 := 0;
  n2 := 0;

  for n := 1 to length(map.map) do
  begin
    r.Left := map.blockW * n1;
    r.Top := map.blockH * n2;
    r.Right := r.Left + map.blockW;
    r.Bottom := r.Top + map.blockH;

    { SCALING }
    r.Left += Form1.BGRAVirtualScreen1.Left;
    r.Top += Form1.BGRAVirtualScreen1.Top;
    r.Right += Form1.BGRAVirtualScreen1.Left;
    r.Bottom += Form1.BGRAVirtualScreen1.Top;

    if (pos.x >= r.Left) and (pos.x <= r.Right) and (pos.y >= r.Top) and
      (pos.y <= r.Bottom) then
    begin
      // vacio
      if map.map[n] = '0' then
        exit;

      // el vacio esta a la izquierda
      if (n1 <> 0) then
        if map.map[n - 1] = '0' then
        begin
          map.map[n - 1] := map.map[n];
          map.map[n] := '0';
        end;

      // el vacio esta a la derecha
      if (n1 <> map.mapW - 1) then
        if map.map[n + 1] = '0' then
        begin
          map.map[n + 1] := map.map[n];
          map.map[n] := '0';
        end;

      // el vacio esta arriba
      if map.map[n - map.mapW] = '0' then
      begin
        map.map[n - map.mapW] := map.map[n];
        map.map[n] := '0';
      end;

      // el vacio esta abajo
      if map.map[n + map.mapW] = '0' then
      begin
        map.map[n + map.mapW] := map.map[n];
        map.map[n] := '0';
      end;

      Result := True;
      exit;
    end;

    if n1 = map.mapW - 1 then
    begin
      n1 := -1;
      Inc(n2);
    end;

    Inc(n1);
  end;
end;

function KeyPressMap(var map: TGameMap; var Key: word): boolean;

  function GetGameKey(var Key: word; invertLR, invertUD: boolean): TGameKey;
  begin
    if (Key = L1) or (Key = L2) then
      Result := gkLeft;
    if (Key = R1) or (Key = R2) then
      Result := gkRight;
    if (Key = U1) or (Key = U2) then
      Result := gkUp;
    if (Key = D1) or (Key = D2) then
      Result := gkDown;

    if invertLR then
      case Result of
        gkLeft: Result := gkRight;
        gkRight: Result := gkLeft;
      end;

    if invertUD then
      case Result of
        gkUp: Result := gkDown;
        gkDown: Result := gkUp;
      end;
  end;

var
  n, n1, n2: integer;
begin
  n1 := 0;
  n2 := 0;
  Result := False;

  for n := 1 to length(map.map) do
  begin
    if map.map[n] = '0' then
    begin

      case GetGameKey(Key, True, True) of
        gkLeft:
          // el de la izquierda
          if (n1 <> 0) then
          begin
            map.map[n] := map.map[n - 1];
            map.map[n - 1] := '0';
            Result := True;
            Key := 0;
          end;

        gkRight:
          // el de la derecha
          if (n1 <> map.mapW - 1) then
          begin
            map.map[n] := map.map[n + 1];
            map.map[n + 1] := '0';
            Result := True;
            Key := 0;
          end;

        gkUp:
          // el de arriba
          if (n2 <> 0) then
          begin
            map.map[n] := map.map[n - map.mapW];
            map.map[n - map.mapW] := '0';
            Result := True;
            Key := 0;
          end;

        gkDown:
          // el de abajo
          if (n2 <> map.mapH - 1) then
          begin
            map.map[n] := map.map[n + map.mapW];
            map.map[n + map.mapW] := '0';
            Result := True;
            Key := 0;
          end;

      end;

      if Result then
        exit;
    end;

    if n1 = map.mapW - 1 then
    begin
      n1 := -1;
      Inc(n2);
    end;

    Inc(n1);
  end;
end;

function IsMapSolved(var map: TGameMap): boolean;
begin
  Result := map.map = map.solve;
end;

function CalcularPosibles(str: string; showDebug: boolean = False): TarrayOfString;

  function Factorial(number: integer): integer;
  var
    i: integer;
  begin
    Result := number;
    for i := number - 1 downto 1 do
    begin
      Result := Result * i;
    end;
  end;

  function MoverIzq(str: string; pos: integer): string;
  var
    s1, s2: char;
  begin
    Result := str;
    s1 := Result[pos];
    s2 := Result[pos - 1];
    Result[pos] := s2;
    Result[pos - 1] := s1;
  end;

  function MoverIni(str: string; pos: integer): string;
  var
    s1, s2: char;
  begin
    Result := str;
    s1 := Result[pos];
    s2 := Result[1];
    Result[pos] := s2;
    Result[1] := s1;
  end;

var
  nLargo, // numero de char en la string
  nFactorial, // numero de combinaciones
  nFactorialDivLargo, // primer char veces repetido
  nFactorialm1DivLargom1, // segundo char veces repetido
  nPosibles: integer; // numero de combinaciones jugables
  n: integer;

  rstr: string;
begin
  // Los comentarios de los numeros son para str:=1230;

  nLargo := length(str); // 4

  if nLargo <= 1 then
    exit;

  nFactorial := Factorial(nLargo); // 24
  //nFactorialDivLargo := nFactorial div nLargo; // 6
  //nFactorialm1DivLargom1 := Factorial(nLargo - 1) div (nLargo - 1); // 2

  nPosibles := nFactorial div 2; // 12

  SetLength(Result, nPosibles);

  //0 to 11
  for n := 0 to nPosibles - 1 do
  begin
    // create a function here ;)

    // start with '1'
    if n = 0 then
      Result[n] := str // 1230 (no clic)
    else
    if n = 1 then // 1203 (clic 3)
      Result[n] := MoverIzq(Result[n - 1], nLargo); // 1203
    if n = 2 then // 1032 (clic 2)
    begin
      Result[n] := MoverIzq(Result[n - 1], nLargo - 1); // 1023
      Result[n] := MoverIzq(Result[n], nLargo); // 1032
    end;

    // start with '2' (the 2 positioned char)
    if n = 3 then // 2310 (clic 3 2 1 3)
    begin
      Result[n] := MoverIni(Result[0], 2); // 2130
      Result[n] := MoverIzq(Result[n], nLargo - 1); // 2310
    end;
    if n = 4 then // 2301
    begin
      Result[n] := MoverIzq(Result[n - 1], nLargo); // 2301
    end;
    if n = 5 then // 2013
    begin
      Result[n] := MoverIzq(Result[n - 1], nLargo - 1); // 2031
      Result[n] := MoverIzq(Result[n], nLargo); // 2013
    end;

    // start with '3' (the 3 positioned char)
    if n = 6 then // 3120
    begin
      Result[n] := MoverIni(Result[0], 3);
      Result[n] := MoverIzq(Result[n], nLargo - 1); // 3120
    end;
    if n = 7 then // 3102
    begin
      Result[n] := MoverIzq(Result[n - 1], nLargo); // 3102
    end;
    if n = 8 then // 3021
    begin
      Result[n] := MoverIzq(Result[n - 1], nLargo - 1);
      Result[n] := MoverIzq(Result[n], nLargo); // 3021
    end;

    // start with '0' (the 4 positioned char)
    if n = 9 then // 0321
    begin
      Result[n] := MoverIni(Result[0], 4);
      Result[n] := MoverIzq(Result[n], nLargo - 1); // 0321
    end;
    if n = 10 then // 0213
    begin
      Result[n] := MoverIzq(Result[n - 1], nLargo - 1);
      Result[n] := MoverIzq(Result[n], nLargo); // 0213
    end;
    if n = 11 then // 0132
    begin
      Result[n] := MoverIzq(Result[n - 1], nLargo - 1);
      Result[n] := MoverIzq(Result[n], nLargo); // 0232
    end;
  end;

  // Debug
  if showDebug then
  begin
    rstr := '';
    for n := 0 to nPosibles - 1 do
    begin
      rstr := concat(rstr, LineEnding, Result[n]);
    end;

    ShowMessage(rstr);

    ShowMessage(
      concat('numero de char en la string: ', IntToStr(nLargo), LineEnding,
      'numero de combinaciones: ', IntToStr(nFactorial), LineEnding,
      //'primer char veces repetido: ', IntToStr(nFactorialDivLargo),
      //LineEnding, 'segundo char veces repetido: ', IntToStr(nFactorialm1DivLargom1), LineEnding,
      'numero de combinaciones jugables: ', IntToStr(nPosibles))
      );
  end;
end;

{$R *.lfm}

{ TForm1 }

procedure TForm1.BGRAVirtualScreen1Redraw(Sender: TObject; Bitmap: TBGRABitmap);
begin
  DrawMap(map^, bitmap, texture);
end;

{ MOUSE }

procedure TForm1.BGRAVirtualScreen1Click(Sender: TObject);
var
  pos: TPoint;
begin
  pos := ScreenToClient(Mouse.CursorPos);
  if ClickMap(map^, pos) then
  begin
    PlayBlockSound;
    BGRAVirtualScreen1.DiscardBitmap;
  end;
end;

{ CREATE }

procedure TForm1.FormCreate(Sender: TObject);
var
  a: TArrayOfString;
  i: integer;
begin
  a := CalcularPosibles('1230'{, True});

  // 0 is 1230 the solve
  // 1 to 11
  for i := 1 to length(a) - 1 do
  begin
    Game[i].map := a[i];
    Game[i].solve := a[0];
    Game[i].mapW := length(a[0]) div 2;
    Game[i].mapH := length(a[0]) div 2;
    Game[i].blockW := 128;
    Game[i].blockH := 128;
  end;

  Game[length(Game)] := GameOver;

  { SCALING }
  // FullScreen with current screen resolution
  //Size := Rect(0, 0, Screen.Width, Screen.Height);



  Size := Rect(0, 0, 640, 480);

  SetBounds(Size.Left, Size.Top, Size.Right, Size.Bottom);

  ScaleAspectRatio(BGRAVirtualScreen1, Size.Right, Size.Bottom, True, True, True);

  ToggleFullScreen(Self, Size);

  ScaleBlocks;

  { OPEN }
  randomize; // to randomize the skin

  OpenMap(1);
  current := 1;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  CloseMap;
end;

{ KEYBOARD }

procedure TForm1.FormKeyDown(Sender: TObject; var Key: word; Shift: TShiftState);
begin
  if (Key = VK_ESCAPE) then
    Application.Terminate;

  if (Key = VK_F11) then
    ToggleFullScreen(Self, Size);

  if KeyPressMap(map^, Key) then
  begin
    PlayBlockSound;
    BGRAVirtualScreen1.DiscardBitmap;
  end;
end;

procedure TForm1.FormResize(Sender: TObject);
begin
  CenterControl(BGRAVirtualScreen1);
end;

{ TIMER }

procedure TForm1.Timer1Timer(Sender: TObject);

{$IFDEF WINDOWS}
  procedure JoyDown(const pKey: word);
  var
    Key: word;
  begin
    Key := pKey;
    FormKeyDown(nil, Key, []);
  end;

{$ENDIF}

var
  nextGame: boolean;
  {$IFDEF WINDOWS}
  myJoy: TJoyInfoEx;
  myJoyCaps: TJoyCaps;
  ErrorResultC, ErrorResultP: MMRESULT;
  {$ENDIF}
begin
  if map <> nil then
  begin
    Caption := 'Level ' + IntToStr(current);

    nextGame := IsMapSolved(map^);
    if (current = length(Game)) and nextGame then
    begin
      Timer1.Enabled := False;
      //ShowMessage('Game Win!');
      exit;
    end;

    if nextGame then
    begin
      Timer1.Enabled := False;
      OpenMap(current + 1);
      //ShowMessage('Next: Level ' + IntToStr(current));
      BGRAVirtualScreen1.DiscardBitmap;
      Timer1.Enabled := True;
    end;

    {$IFDEF WINDOWS}
    ErrorResultC := joyGetDevCaps(joystickid1, @myJoyCaps, sizeof(myJoyCaps));
    ErrorResultP := joyGetPosEx(joystickid1, @MyJoy);
    if (ErrorResultC = JOYERR_NOERROR) and (ErrorResultP = JOYERR_NOERROR) then
    begin
      if (myJoy.dwPOV = JOY_POVFORWARD) or (myJoy.wYpos = myJoyCaps.wYmin) then
        JoyDown(U1)
      else if (myJoy.dwPOV = JOY_POVBACKWARD) or (myJoy.wYpos = myJoyCaps.wYmax) then
        JoyDown(D1)
      else if (myJoy.dwPOV = JOY_POVLEFT) or (myJoy.wXpos = myJoyCaps.wXmin) then
        JoyDown(L1)
      else if (myJoy.dwPOV = JOY_POVRIGHT) or (myJoy.wXpos = myJoyCaps.wXmax) then
        JoyDown(R1);
    end;
   {$ENDIF}

  end;
end;

procedure TForm1.OpenMap(num: integer);
begin
  CloseMap;
  if (num < low(Game)) or (num > high(Game)) then
    halt;
  current := num;
  map := @Game[current];
  GenerateTexture;
end;

procedure TForm1.CloseMap;
begin
  if map <> nil then
    FreeAndNil(map^.background);
  FreeAndNil(texture);
  map := nil;
end;

procedure TForm1.ScaleBlocks;
var
  i: integer;
begin
  for i := low(game) to high(game) do
  begin
    Game[i].blockW := BGRAVirtualScreen1.Width div Game[i].mapW;
    Game[i].blockH := BGRAVirtualScreen1.Height div Game[i].mapH;
  end;
end;

procedure TForm1.GenerateTexture;
begin
  if texture <> nil then
    raise Exception.Create('Texture not freed');
  case random(9) of
    0: texture := CreatePlastikTexture(BGRAVirtualScreen1.Width,
        BGRAVirtualScreen1.Height);
    1: texture := CreateCamouflageTexture(BGRAVirtualScreen1.Width,
        BGRAVirtualScreen1.Height);
    2: texture := CreateSnowPrintTexture(BGRAVirtualScreen1.Width,
        BGRAVirtualScreen1.Height);
    3: texture := CreateRoundStoneTexture(BGRAVirtualScreen1.Width,
        BGRAVirtualScreen1.Height);
    4: texture := CreateStoneTexture(BGRAVirtualScreen1.Width,
        BGRAVirtualScreen1.Height);
    5: texture := CreateWaterTexture(BGRAVirtualScreen1.Width,
        BGRAVirtualScreen1.Height);
    6: texture := CreateMarbleTexture(BGRAVirtualScreen1.Width,
        BGRAVirtualScreen1.Height);
    7: texture := CreateWoodTexture(BGRAVirtualScreen1.Width, BGRAVirtualScreen1.Height);
    8: texture := CreateVerticalWoodTexture(BGRAVirtualScreen1.Width,
        BGRAVirtualScreen1.Height);
  end;
end;

procedure TForm1.PlayBlockSound;
begin
  {$IFDEF WINDOWS}
  PlaySound(PChar('move.wav'), 0, SND_ASYNC);
  {$ENDIF}
end;

end.
