Introdução
Seguindo a linha de jogos em AdvPL, hoje o post traz para vocês a contribuição do desenvolvedor e nobre colega Flávio Luiz Vicco, que fez um porte do jogo “Resta 1” para o AdvPL, usando orientação a objeto em AdvPL.
O Jogo
Resta 1 é um quebra-cabeças, cujo objetivo é retirar todas as peças do tabuleiro, até que reste somente uma. No início do jogo, há 32 peças no tabuleiro, deixando vazia a posição central. Um movimento consiste em pegar uma peça e fazê-la “saltar” sobre outra peça, sempre na horizontal ou na vertical, terminando em um espaço vazio. A peça que foi “saltada” é retirada do tabuleiro. O jogo termina quando não mais é possível fazer nenhum outro movimento. Nesta ocasião, o jogador ganha se restar apenas uma peça no tabuleiro.

Com apenas 250 linhas a classe com o “core” do jogo foi implementada, e usando menos de 50 linhas, foi implementada a função executora do Jogo. Internamente, o jogo usa a classe tPaintPanel, e a classe executora monta o jogo usando uma caixa de diálogo e um menu superior. Os objetos correspondentes às peças do quebra-cabeça são criados nas respectivas posições, disparando um bloco de código a cada clique do mouse em cada posição. Ao clicar em uma posição ocupada por uma pedra, a respectiva posição é selecionada. Ao clicar em uma nova pedra, esta passa ser a pedra selecionada. Uma vez havendo a seleção de uma pedra, caso você clique em uma posição livre localizada a 2 pedras de distância da pedra selecionada, e entre elas existe uma pedra, a pedra selecionada é movida para a nova posição selecionada, e a pedra entre elas é removida.
Compilando e Executando
Para compilar e executar o Resta Um, basta baixar os fontes e imagens do GitHub, no endereço “https://github.com/siga0984/Resta1-OO“, criar um projeto no IDE/TDS, acrescentar os dois fontes PRW no projeto, acrescentar todas as imagens no projeto como recursos do Projeto, compilar tudo, e chamar a função “U_RESTA1” através do SmartClient.
O Código
Um jogo de tabuleiro, sem adversário ou contagem de tempo, deve ter uma representação em memória do estado do tabuleiro, uma interface que represente graficamente esta representação, e permita ao jogador imputar um movimento, e ser capaz de criticar um movimento inválido, e aplicar um movimento válido na representação do tabuleiro em memória.
Para desempenhar estas tarefas, o jogo foi implementado como uma classe em AdvPL, chamada “TResta1”, onde o construtor “New()” recebe como parâmetro o “container” de interface do Jogo — no nosso caso o objeto da janela de diálogo — e inicia a execução do jogo através do método Activete() da classe “TResta1”.
Na classe TRESTA1, a propriedade “aShape” contem todas as peças do quebra-cabeças. Como o tabuleiro é basicamente a união de um array 3×7 e outro 7×3, compartilhando a área central ( 3×3 ), o array é criado baseado em um loop de 7×7 elementos, onde as posições onde não entraria nenhuma pedra nao são ocupadas não criam nenhum novo elemento. São 16 posicoes ignoradas, 4 de cada quadrante. Sabendo que uma matriz 7×7 tem 49 elementos, e 16 serão ignorados, serão criadas 33 posições no tabuleiro, onde para cada uma será atribuído um status ( vazio / ocupado / selecionado ) e uma imagem correspondente ao estado.
Partindo de um loop 7×7, numerando cada elemento sequencialmente, eu teria as seguintes posições numeradas:
01 02 03 04 05 06 07
08 09 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
Somente serão criados elementos para as posições onde podem haver pedras no quebra-cabeça. Logo, as 4 pedras de cada extremidade deste quadrado não serão usadas — repare que as posições a serem usadas estão em negrito. Cada pedra é criada como um “Shape” dentro do tPaintPanel, e cada uma delas recebe um identificador único, sendo criadas 33 pedras ( ou shapes ), no loop abaixo — vide método Activate() da classe tResta1.
For nX := 1 To 7
For nY := 1 To 7
nZ ++
If !StrZero(nZ,2) $ "01|02|06|07|08|09|13|14|36|37|41|42|43|44|48|49"
::Create( ::oTPanel, nX, nY, "P"+StrZero(nZ,2), IIf(nZ==25,0,1) )
EndIf
Next nY
Next nX
O evento interessante tratado no jogo é o Click() … A classe tPaintPanel() tem uma propriedade que permite informar um bloco de código, que será disparado quando o usuário da aplicação clicar com o mouse dentro da área do tPaintPanel. Ao ver o fonte, reparamos que o evento em si não recebe nenhum parâmetro, mas isso não impede do método ‘descobrir’ se o evento de clique do mouse foi feito em cima de um determinado Shape. Ao ser criado, cada shape possui um “ID”, informado pela aplicação como um valor numérico. Quando um evento de clique do mouse é realizado sobre um shape, a propriedade ShapeAtu da instância do tPaintPanel é atualizada. Logo, o método somente precisa procurar no array de Shapes qual o shape que foi clicado. Se não foi clicado em nenhum shape, o evento é ignorado.
//-- Identifica obj. shape clicado.
nDestino := aScan(::aShape,{ |x|(x[1] == ::oTPanel:ShapeAtu)})
Ainda dentro do método Click(), existe todo o tratamento para avaliar como o clique deve ser tratado. Para realizar um “pulo”, você deve selecionar uma pedra, criando sobre ela, e depois clicando em um espaço em branco, indicando que você quer movê-la para aquela posição. Seu movimento somente será realizado se, a posição escolhida tiver a distancia de 2 casas, na horizontal ou vertical — diagonal não pode — , e entre a posição selecionada e a posição escolhida, exista uma pedra — que será removida.
E, da forma que o jogo foi idealizado, existe um programa “base”, ou “executor”, que é responsável por prover a interface externa do jogo — A janela de diálogo onde o jogo será montado e o menu de opções — Iniciar Novo Jogo, Ajuda, Sair, barra de status inferior, etc. Reparem que apenas uma instância do jogo é criada e armazenada na variável oResta1, e a partir deste ponto todas as ações externas que podem ser realizadas com o Jogo ( iniciar, reiniciar, mostrar ajuda) são feitas a partir dos métodos publicados.
#INCLUDE "PROTHEUS.CH"
/* -----------------------------------------------------
Fonte RESTA1.PRW
Programa Resta 1 - executor
Autor Flavio Luiz Vicco
Data 11/2016
----------------------------------------------------- */
User Function Resta1()
Local oDlg
Local oResta1
DEFINE DIALOG oDlg TITLE "Resta1" From 180,180 TO 550,700 PIXEL COLOR CLR_BLACK,CLR_WHITE
oDlg:lEscClose := .F.
//-- Cria Resta1
oResta1:= TResta1():New(oDlg)
//-- Cria Menu superior
CreateMenuBar(oDlg,oResta1)
//-- Cria Barra de Status inferior
CreateMsgBar(oDlg)
// Na ativação do dialogo, ativa o jogo
ACTIVATE DIALOG oDlg CENTERED ON INIT oResta1:Activate()
Return Nil
//-- Cria Menu superior
Static Function CreateMenuBar(oDlg,oResta1)
oTMenuBar:= TMenuBar():New(oDlg)
oTMenuBar:SetCss("QMenuBar{background-color:#eeeddd;}")
oTMenuBar:Align := CONTROL_ALIGN_TOP
oTMenuBar:nClrPane := RGB(238,237,221)
oTMenuBar:bRClicked := {||}
oFile:= TMenu():New(0,0,0,0,.T.,,oDlg)
oHelp:= TMenu():New(0,0,0,0,.T.,,oDlg)
oTMenuBar:AddItem("&Arquivo",oFile,.T.)
oTMenuBar:AddItem("Aj&uda" ,oHelp,.T.)
oFile:Add(TMenuItem():New(oDlg,"&Novo Jogo",,,,{|| oResta1:NewGame()},,"",,,,,,,.T.))
oFile:Add(TMenuItem():New(oDlg,"Sai&r",,,,{|| If(MsgYesNo("Deseja realmente sair do jogo?"),oDlg:End(),)},,"FINAL",,,,,,,.T.))
oHelp:Add(TMenuItem():New(oDlg,"&Sobre... F1",,,,{|| oResta1:Help() },,"RPMPERG",,,,,,,.T.))
Return
//-- Cria Barra de Status inferior
Static Function CreateMsgBar(oDlg)
oTMsgBar := TMsgBar():New(oDlg, "Resta1",.F.,.F.,.F.,.F., RGB(116,116,116),,,.F.)
oTMsgItem1 := TMsgItem():New( oTMsgBar,"2014", 100,,,,.T., {||} )
oTMsgItem2 := TMsgItem():New( oTMsgBar,"V.1.00", 100,,,,.T., {||} )
Return
//----------------------------------------------------------------------------
E, segue abaixo o fonte da classe do jogo .
#INCLUDE "PROTHEUS.CH"
/* -----------------------------------------------------
Fonte TRESTA1.PRW
Programa Resta 1 - objetos
Autor Flavio Luiz Vicco
Data 11/2016
----------------------------------------------------- */
Class TResta1
Data nId AS INTEGER
Data nOrigem AS INTEGER // Numero da posicao selecionada
Data aShape AS ARRAY INIT {}
DATA oTPanel AS OBJECT // tPaintPanel
Method New(oDlg) CONSTRUCTOR
Method Activate()
Method NewGame()
Method Create()
Method Click( x, y, oTPanel )
Method Change( oTPanel, nItem, nStatus )
Method SetId()
Method ExportImage()
Method Help()
EndClass
Method New(oDlg) Class TResta1
::nId := 0
::nOrigem := 0
::aShape := {}
::oTPanel:= TPaintPanel():new(0,0,0,0,oDlg,.f.)
::oTPanel:Align := CONTROL_ALIGN_ALLCLIENT
::oTPanel:bLClicked := {|x,y| ::Click()}
::ExportImage()
Return Self
Method Activate() Class TResta1
Local nX := 0
Local nY := 0
Local nZ := 0
Local cImg := "backg.png"
Local cId := ""
//-- Tamanho da tabuleiro
cTabLarg := cValToChar(400)
cTabAlt := cValToChar(450)
//-- Ajusta tela conforme tabuleiro
::oTPanel:oWnd:nHeight := Val(cTabAlt)
::oTPanel:oWnd:nWidth := Val(cTabLarg)
//-- Altura largura do tabuleiro
cAltura := '0'
cLargura := '0'
//-- Cria Container
::oTPanel:addShape( "id="+::SetId()+";type=1;left=0;top=0;width="+cValToChar(Val(cTabLarg))+;
";height="+cValToChar(Val(cTabAlt))+";"+;
"gradient=1,0,0,0,0,0.0,#FFFFFF;pen-width=0;pen-color=#FFFFFF"+;
";can-move=0;can-mark=0;is-container=1;")
//-- Cria shape com imagem do tabuleiro
cId := ::SetId()
::oTPanel:addShape( "id="+cId+";type=8;left="+cLargura+";top="+cAltura+";width="+cTabLarg+;
";height="+cTabAlt+";image-file="+::GetTempPath()+cImg+";tooltip=Resta1"+;
";can-move=0;can-deform=0;can-mark=0;is-container=1")
For nX := 1 To 7
For nY := 1 To 7
nZ ++
If !StrZero(nZ,2) $ "01|02|06|07|08|09|13|14|36|37|41|42|43|44|48|49"
::Create( ::oTPanel, nX, nY, "P"+StrZero(nZ,2), IIf(nZ==25,0,1) )
EndIf
Next nY
Next nX
Return
Method NewGame() Class TResta1
Local nX := 0
Local nY := 0
Local nZ := 0
For nZ := 1 To Len(::aShape)
nX := ::aShape[nZ,3]
nY := ::aShape[nZ,4]
If ::aShape[nZ,5] <> IIf(nX==4.And.nY==4,0,1)
::Change( ::oTPanel, nZ, IIf(nX==4.And.nY==4,0,1 ) )
EndIf
Next nZ
Return
Method Create( oPanel, nImgX, nImgY, cCodigo, nStatus, nShape, cImgId ) Class TResta1
Local cWidth := "30"
Local cHeight := "30"
Local cImg := ""
Local cToolTip := AllTrim(cCodigo)+" X= "+AllTrim(Str(nImgX))+" Y= "+AllTrim(Str(nImgY))
Default nShape := 0
Default cImgId := ::SetId()
//-- Define imagem para cada status
// 0 = Nao há pedra - vazio
// 1 - Espaço ocupado com uma pedra
// 2 - Pedra atualmente selecionada
Do Case
Case nStatus == 0
cImg := "empty.png"
Case nStatus == 1
cImg := "full.png"
Case nStatus == 2
cImg := "select.png"
EndCase
//-- criacao do obj
If nShape == 0
aAdd(::aShape,Array(5))
nShape := Len(::aShape)
EndIf
//-- config. do obj
::aShape[nShape,1] := Val(cImgId) //CODIGO DO SHAPE
::aShape[nShape,2] := cCodigo //CODIGO
::aShape[nShape,3] := nImgX //POSICAO X
::aShape[nShape,4] := nImgY //POSICAO Y
::aShape[nShape,5] := nStatus //STATUS
oPanel:addShape("id="+cImgId+";type=8;left="+Str(nImgY*45)+;
";top="+Str(nImgX*45)+";width="+cWidth+";height="+cHeight+;
";image-file="+::GetTempPath()+cImg+";tooltip="+cToolTip+;
";can-move=0;can-deform=1;can-mark=0;is-container=0")
Return
Method Click() Class TResta1
Local nDestino := 0
Local nSalto := 0
Local nIdImg := 0
Local nX := 0
Local nY := 0
Local nIdClk := 0
Local nStatus := 0
Local lOk := .F.
//-- Identifica obj. shape clicado.
nDestino := aScan(::aShape,{ |x|(x[1] == ::oTPanel:ShapeAtu)})
If nDestino > 0
nStatus := ::aShape[nDestino,5]
Do Case
Case nStatus == 0
If ::nOrigem > 0
nX0 := ::aShape[::nOrigem ,3]
nY0 := ::aShape[::nOrigem ,4]
nX1 := ::aShape[ nDestino,3]
nY1 := ::aShape[ nDestino,4]
//-- Verifica se movimento horizontal valido...
If (nX0 == nX1 .And. Abs(nDif := nY0 - nY1) == 2)
If nDif == 2
nDif := -1
Else
nDif := 1
EndIf
lOk := (nSalto:=aScan(::aShape,{|x| x[3]==nX0 .And. x[4]==nY0+nDif .And. x[5]==1})) > 0
EndIf
//-- Verifica se movimento vertical valido...
If (nY0 == nY1 .And. Abs(nDif := nX0 - nX1) == 2)
If nDif == 2
nDif := -1
Else
nDif := 1
EndIf
lOk := (nSalto:=aScan(::aShape,{|x| x[3]==nX0+nDif .And. x[4]==nY0 .And. x[5]==1})) > 0
EndIf
If lOk
nStatus := 1
//-- Retira da posicao saltada
::Change( ::oTPanel, nSalto, 0 )
//-- Retira da posicao anterior
::Change( ::oTPanel, ::nOrigem, 0 )
::nOrigem := 0
EndIf
EndIf
Case nStatus == 1
If ::nOrigem > 0
//-- Retira da posicao anterior
::Change( ::oTPanel, ::nOrigem, 1 )
EndIf
nStatus := 2
::nOrigem:= nDestino
lOk := .T.
Case nStatus == 2
nStatus := 1
::nOrigem:= 0
lOk := .T.
EndCase
//-- Troca figura da posicao atual
If lOk
::Change( ::oTPanel, nDestino, nStatus )
EndIf
EndIf
Return
//-- Realiza uma mudança de status de um elemento ( pedra ) do jogo
Method Change( oTPanel, nItem, nStatus ) Class TResta1
Local nIdImg := 0
Local cCodigo := ""
Local nX := 0
Local nY := 0
nIdImg := ::aShape[nItem,1]
cCodigo := ::aShape[nItem,2]
nX := ::aShape[nItem,3]
nY := ::aShape[nItem,4]
//-- Excluir shape com status anterior
::oTPanel:DeleteItem(nIdImg)
//-- Recriar shape com status atual
::Create( ::oTPanel, nX, nY, cCodigo, nStatus, nItem, Str(nIdImg) )
Return
//-- CRia identificador sequencial para objetos
Method SetId() Class TResta1
Return cValToChar(++::nId)
//-- Exporta as imagens do RPO para o temporario %TEMP%
Method ExportImage() Class TResta1
Local aImage := { "backg.png" , "empty.png" , "full.png" , "select.png" }
Local nImage, cImageTo
For nImage := 1 To Len(aImage)
cImageTo := ::GetTempPath()+aImage[nImage]
If !Resource2File(aImage[nImage],cImageTo)
MsgStop("Image not found: " + aImage[nImage])
QUIT
EndIf
Next nImage
Return
Method Help() Class TResta1
MsgInfo( "Resta1 em ADVPL.","Bem Vindo!")
Return
Conclusão
Agradeço novamente a colaboração do Fávio Vicco, neste momento em que eu estou enfrentando um breve “bloqueio criativo” ..risos.. e desejo a todos TERABYTES DE SUCESSO !!!
Referências
https://github.com/siga0984/Resta1-OO
http://tdn.totvs.com/display/tec/TPaintPanel
https://pt.wikipedia.org/wiki/Resta_um