Skip to content

15 Puzzle

O objetivo aqui foi criar uma aplicação para desktop e tentar recriá-la para web, vendo o que seria possível reaproveitar. Poderia ter reaproveitado mais coisas mas, é só um exemplo.

Escolhi um quebra cabeças que fosse simples de implementar. Optei pelo 15 Puzzle (ou jogo do 15).

A versão para desktop ficou assim:

15

e a versão para web ficou assim:

15

Acho que ficou semelhante o suficiente para um teste.

TL;DR;

Fonte do projeto no github.

HTML

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!doctype html>
<html lang="en">
<head>
  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
  <title>Project1</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
  <script src="quinze_web.js"></script>
</head>
<body>
  <div class="w3-container w3-padding-16">
  <div class="w3-row">
    <div class="w3-col s6 w3-center"><canvas id="my_canvas"></canvas></div>
      <div class="w3-col s6 w3-center">
        <header class="w3-container w3-black w3-center">
          <h1 class="w3-xxxlarge">15 Puzzle</h1>
        </header>
        <div class="w3-padding-16">
          <button id="bt_new" class="w3-block w3-button w3-white w3-border w3-border-black w3-hover-green">New Game</button>
        </div>
        <p>Move tiles in grid to order them from 1 to 15. Use arrows to move</p>
      </div>
    </div>
  </div>
  <script>
    window.addEventListener("load", rtl.run);
  </script>
</body>
</html>

Não tem muito mistério. Basicamente um canvas na linha 13 para o desenho do tabuleiro e um botão na linha 19 para novo jogo.

Desktop vs Navegador

Basicamente é necessário separar o desenho da janela, do tabuleiro e alguns detalhes referente ao movimento conforme as teclas pressionadas. A lógica principal do jogo foi reutilizada. Por falta de um nome melhor, coloquei no arquivo fifteenengine.pas. Para trabalhar com as diferenças entre desktop e navegador na hora da compilação, basta um {$IFDEF/IFNNDEF BROWSER}.

Programa comum

fifteenengine.pas
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
unit fifteenengine;

{$mode ObjFPC}{$H+}

interface

uses
  Classes, SysUtils, Math
  {$IFNDEF BROWSER}
    ,LCLType
  {$ENDIF}
  ;

Const
  CSIZE = 15;
  BSIZE = 100;
  GAP = 4;
  {$IFDEF BROWSER}
    VK_UP    = 100;
    VK_DOWN  = 101;
    VK_RIGHT = 102;
    VK_LEFT  = 103;
    COLORBLANK = '#808080';
    COLOROK    = '#b3ffb3';
    COLORWRONG = '#ff9999';
  {$ELSE}
    COLORBLANK = $00808080;
    COLOROK    = $00b3ffb3;
    COLORWRONG = $009999ff;
  {$ENDIF}

type Block = Record
    n,
    x,
    y : integer;
  end;

var
  Board : array [0..CSIZE] of Block;
  Blank : integer;

Procedure BoardInit;
procedure Shuffle;
Function  DoMove(key:word):Boolean;

implementation

procedure SetBlank;
var
  i: Integer;
begin
  for i:=0 to CSIZE do
    if Board[i].n = 0 then begin
      Blank:=i;
      Break;
    end;
end;

Procedure BoardInit;
var
  px,py,
  pb: Integer;
begin
  {$IFNDEF BROWSER}
  Randomize;
  {$ENDIF}
  for pb:=0 to CSIZE do begin
    DivMod(pb,4,py,px);
    with Board[pb] do begin
      n:=pb+1;
      x:=px;
      y:=py;
    end;
  end;
  Board[CSIZE].n:=0;
  Blank:=CSIZE;
end;

Function IsSolvable:Boolean;
var
  i,j : integer;
  p,c : integer;
begin
  c:=0;
  p:=4-Board[Blank].y;
  for i:=0 to CSIZE - 1 do
    for j:=i to CSIZE -1 do
      if (Board[i].n > 0) and (Board[j].n > 0) then
        if Board[i].n > Board[j].n then
          inc(c);
  Result:=odd(c) xor odd(p);
end;

procedure Shuffle;
var
  i,j,n: Integer;
begin
  for i := CSIZE downto 0 do begin
    j := Random(i);
    if not (i = j) then begin
      n := Board[i].n;
      Board[i].n := Board[j].n;
      Board[j].n := n;
    end;
  end;
  SetBlank;
  if not IsSolvable then
    Shuffle;
end;

Function  CanMove(key:word):Boolean;
begin
  Result:=False;
  case key of
    VK_UP    : if Board[Blank].y = 3 then exit;
    VK_DOWN  : if Board[Blank].y = 0 then exit;
    VK_RIGHT : if Board[Blank].x = 0 then exit;
    VK_LEFT  : if Board[Blank].x = 3 then exit;
  end;
  Result:=True;
end;

Function DoMove(Key:word):Boolean;
var
  d:integer;
begin
  Result:=CanMove(key);
  if not Result then
    exit;
  case Key of
    VK_UP    : d :=  4;
    VK_DOWN  : d := -4;
    VK_RIGHT : d := -1;
    VK_LEFT  : d :=  1;
  end;
  Board[Blank].n := Board[Blank+d].n;
  Blank:=Blank+d;
  Board[Blank].n:=0;
end;

end.

Nas linhas de 19 a 25, definem as teclas com os mesmos nomes (valores quaisquer pois não fui procurar) da definidas em LCLTypeque não pode ser incluída e as cores desejadas para peças no local correto, errado e branco que serve para a movimentação.

De 32 a 36 um record que indica o número e as coordenadas. Poderia ser um TPoint mas fica assim.

A linha 39 é o tabuleiro. Algumas versões utilizam matrizes bidimensionais. Resolvi usar uma lista pois achei mais práticas algumas operações. E as próprias coordenadas definidas no record da linha 32 facilitam as coisas. A variável Blank da linha 40 serve para indicar onde está o quadro vazio para servir na movimentação.

De 42 a 44 os procedimentos utilizados para o jogo.

Procedure SetBlank

Apenas ajusta a posição do espaço em branco.

BoardInit

Inicializa o tabuleiro com as peças de 1 a 15 em sequência e tendo o quadro 16 em branco para a movimentação. As coordenadas das peças são ajustadas de 0..3 para x e 0..3 para y.

IsSolvable

Quando as peças são embaralhadas, é necessário saber se o resultado tem solução. Você pode ver maiores detalhes sobre a rotina para verificar se existe uma solução para o arranjo atual aqui.

Shuffle

Bem, uma rotina para embaralhar as peças no tabuleiro. Se a posição resultante não tiver solução, embaralha novamente até conseguir uma posição válida.

CanMove

Verifica se o movimento escolhido pelo jogador é válido, isto é, se existe uma peça que possa ser movida na direção escolhida. Se o local em branco estiver na última coluna do tabuleiro, não é possível move uma peça da direita para esquerda (pressionamento de seta para esquerda).

DoMove

Executa um movimento válido.

Era isso. Nem ficou muito grande com as 141 linhas de código (contando as linhas em branco).

Diferenças

As maiores diferenças foram as rotinas para desenhar o tabuleiro e para a manipulação da tecla pressionada. Vou mostrar apenas as rotinas utilizadas para a interação com a página do navegador.

OnFormKeyPress

48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
function TMyApplication.OnFormKeyPress(k: TJSKeyBoardEvent): Boolean;
Var
  key : word;
begin
  Result:=True;
  case K.Code of
    TJSKeyNames.ArrowRight : key:=VK_RIGHT;
    TJSKeyNames.ArrowUp    : key:=VK_UP;
    TJSKeyNames.ArrowLeft  : key:=VK_LEFT;
    TJSKeyNames.ArrowDown  : key:=VK_DOWN;
  end;
  k.preventDefault;
  if DoMove(key) then
    DrawBoard;
end;
As linhas de 53 a 58 servem apenas para compatibilizar o código da tecla pressionada. A versão para desktop contém apenas as linhas 60 e 61.

DrawBoard

Responsável pelo desenho do estado atual do tabuleiro na tela. Os comandos necessários para desenhar em um canvas de uma página e para desenhar em uma janela são totalmente diferentes. Até poderia ter feito uma rotina comum. Fica para a próxima.

34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
procedure TMyApplication.DrawBoard;
var
  i, x, y: Integer;
begin
  for i:=0 to CSIZE do begin
    FCtx.beginPath;
    x:=Board[i].x;
    x:=x*BSIZE+x*GAP+GAP;
    y:=Board[i].y;
    y:=y*BSIZE+y*GAP+GAP;
    FCtx.strokeStyle:='Black';
    if Board[i].n = 0 then begin
      FCtx.fillStyle:=COLORBLANK;
      FCtx.rect(x,y,BSIZE,BSIZE);
      FCtx.fillRect(x,y,BSIZE,BSIZE);
      FCtx.stroke;
      Continue;
    end else begin
      if Board[i].n=i+1 then
        FCtx.fillStyle:=COLOROK
      else
        FCtx.fillStyle:=COLORWRONG;
      FCtx.rect(x,y,BSIZE,BSIZE);
      FCtx.fillRect(x,y,BSIZE,BSIZE);
      FCtx.fillStyle:='Black';
      FCtx.fillText(Board[i].n.ToString, x + BSIZE div 2, y+BSIZE div 2);
      FCtx.stroke;
    end;
  end; 

A linha 45 inicia o desenho da pastilha. Se o quadro for vazio, desenha e vai para a próxima coordenada. Se o número está na posição correta, desenha em verde e se estiver na posição errada desenha em vermelho.

Integração com a página

26
27
28
uses
  BrowserApp, JS, Classes, SysUtils, Web,
  fifteenengine;

No uses apenas foi incluído o fifteenengine para a lógica do jogo.

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
procedure TMyApplication.BindElements;
begin
  btNew:=TJSHTMLButtonElement(getHTMLElement('bt_new'));
  btNew.OnClick := @NewGame;
  FCanvas:=TJSHTMLCanvasElement(document.getElementById('my_canvas'));
  FCanvas.width:=window.innerWidth;
  FCanvas.height:=window.innerHeight;
  FCanvas.width:=BSIZE*4+GAP*5;
  FCanvas.height:=FCanvas.width;
  FCtx:=TJSCanvasRenderingContext2D(FCanvas.getContext('2d'));
  FCtx.font:='bold 40px Arial';
  FCtx.textAlign:= 'center';
  document.onkeydown:=@OnFormKeyPress;
  BoardInit;
  DrawBoard;
end;

As linhas 28 e 20 associam o botão com a função que deverá ser executada por ocasião do pressionamento.

As linhas 30 a 37 inicializam o canvas e o contexto para o desenho.

A linha 38 o pressionamento de uma tecla com o procedimento OnFormKeyPress.

E era isso. Para programas mais complexos, as coisas não serão tão simples. Mas fica a dúvida da necessidade de uma versão desktop e navegador do mesmo programa.

Existe espaço para melhorias como:

  • Mostrar uma mensagem quando o jogador terminar (todas as peças nos respectivos lugar).
  • Mostrar o número de jogadas efetuadas.
  • Ajustar o tamnho do canvas e o desenho do tabuleiro quando a janela for redimensionada.
  • Animação do movimento.
  • Etc..