Sign in Join
Wiki copytemporary

Peg Solitaire tutorial/fi

English · Suomi

Tämä opetusohjelma on toinen Lazarus opetusohjelma, jonka tavoitteena on ottaa käyttöön Lazaruksen sovelluskehityksen perusteet. On parasta aloittaa tämän opetusohjelman ensimmäisellä osalla: (Howdy World (Hello World on steroids)/fi). Tämä opetusohjelma selittää vähän sitä, miten työskennellä grafiikan parissa ja miten tehdään modulaarinen ohjelma. Tämän opetusohjelman lopputuotteena on yksinkertainen, mutta toimiva versio Peg Solitaire pelistä (1). Jos kaikki menee hyvin niin lopulta se näyttää tältä:

Projektin aloitus

Kuten edellisessä opetusohjelmassakin niin on parasta aloittaa puhtaalta pöydältä. Tee erillinen hakemisto(tai kansio) kutakin projektia kohti. Nopea kertaus:

Ja lisänä tässä projektissa:

Ensiaskeleet

On aina hyvä idea erottaa käyttöliittymään liittyvä koodi muusta esim. tietorakennetta määritelevästä koodista. Joten ensimmäinen askel on tehdä erillinen käännösyksikkö Solitaire tietorakenteita varten.

Peruselementit PegSolitaire laudalla ovat marmorit, levyn rakenne ja tyhjät paikat. Simuloidaan yksinkertaisella matriisilla, jossa paikat ovat pelin solujen tyyppisiä (tyhjä, pelinappula ja ei pääse). Ja tämä kaikki kiteytetään luokkaan, joka käsittelee kaikkea tietojen manipulointia.

pascal
const
  C_MAX = 7;  // Max board size: 7x7

type
  TCellNums = 1..C_MAX;
  TCellType = (ctNoAccess, ctEmpty, ctPeg);
  TPegCells = array[TCellNums, TCellNums] of TCellType;

  TPegSolitaire = class
  private
    Size: TCellNums;
    PegCells: TPegCells;
    
  public
    constructor Create(const pSize: TCellNums);
  end;

On kohtuullista olettaa, että muu koodi, joka aikoo käyttää tätä luokkaa tarvitsee pääsyn solujen sisältöön (eli PegCells). Tapa käsitellä tätä on joko määrittelemällä joukko funktioita jotka hakee ja vie soluihin tai määritellä ns taulukko-ominaisuus. Valitaan jälkimmäinen lähestymistapa ja lisätään seuraava rivi TPegSolitaire luokan public osioon:

pascal
property Cell[const pRow, pCol: TCellNums]: TCellType;
pascal
  result := PegCells[pRow,pCol];
pascal
  PegCells[pRow,pCol] := AValue;
pascal
var iRow,iCol: integer;
begin
  // Store the size of the board locally
  Size := pSize;

  // Initialize all cells to 'not accessible'
  for iRow := 1 to C_MAX do
    for iCol := 1 to C_MAX do
      Cell[iRow,iCol] := ctNoAccess;

Nyt kun rakenteen perustiedot ovat paikallaan, on aika tarkastella mitä grafiikka tarvitsee. On monia tapoja näyttää solitaire pöytä. Tähän voidaan käyttää TPaintbox- komponenttia. Se antaa täydellisen määräysvallan graafisiin ominaisuuksiin.

Ohjelman ikkunalomake aikoo käyttää datarakennetta joka on määritelty PegDatastructure käännösyksikköön.

pascal
uses
  PegDatastructures,
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs;

Seuraava vaihe on kiinnittää solujen matriisi tähän TPaintbox-komponenttiin jakamalla se riveihin ja sarakkeisiin, jotka sisältävät levyn jokaisen solun. Jotta se olisi skaalautuva niin lasketaan leveys ja korkeus itsenäisesti. Tarvitaan pari muuttujaa pitämään tulosta. Ensin solujen leveys ja korkeus, jotta kaikki solut (7) sopivat täsmälleen lomakkeelle. Kehitetään piirtokoodi interaktiivisesti. Tähän käyttää painiketta, joka tuotiin lomakkeelle.

pascal
var
  CellWidth : integer;
  CellHeight: integer;

Tarvitaan ylimääräisiä muuttujia pitämään välituloksia:

pascal
  iRow, iCol: TCellNums;
  CellArea  : TRect;

CellArea käytetään rajoittamaan suorakulmaista aluetta näytöllä, johon solu piirretään.

Jotta käsittellään kaikki rivit ja sarakkeet niin tehdään se kahdella suoraviivaisella for-silmukalla.

pascal
  
// Calculate the width/height of each cell to accomodate for all cells in the paintbox
  CellWidth := pbPeg.Width div 7;
  CellHeight := pbPeg.Height div 7;

  // Draw boxes for all cells
  for iRow := 1 to 7 do
    for iCol := 1 to 7 do
    begin
      // Calculate the position of the cell in the paintbox
      CellArea.Top    := (iRow-1) * CellHeight;
      CellArea.Left   := (iCol-1) * CellWidth;
      CellArea.Right  := CellArea.Left + CellWidth;
      CellArea.Bottom := CellArea.Top  + CellHeight;
      // And now draw the cell
      pbPeg.Canvas.Rectangle(CellArea);
    end;

Canvas on nimensä mukaisesti on ohjaus, joka auttaa meitä piirtämään asioita, kuten viivoja, suorakaiteita, ympyröitä jne. TPaintbox-komponentti sisältää Canvas:n. Siksi rivillä pbPeg.Canvas.Rectangle (CellArea) ohjelma piirtää suorakulmion paintbox, rajattu alue on määritelty CellArea. Ja koska paintbox sijoitetaan lomakkeelle, voimme nähdä tuloksen siellä.

Tämä todistaa, että laskelmat olivat paikallaan ja että on nyt keino tehdä kaikki tarpeellisen oikealla paikalla. Yksi asia on tehtävä kuitenkin ennen lisäämällä piirustus toiminnallisuutta. Piirustus solujen ei mitenkään tärkein muodossa tai toisessa (tai minkäänlaista että asia). Ainoa asia, meidän piirtämiseen asiat on Canvas ja joitakin mittauksia soluille. Joten aiomme luoda tukea luokan siivota tärkeimmäksi.

implementation osiota).

pascal
type
  TPegSolPainter = class
  private
    PegSol      : TPegSolitaire;
    Canvas      : TCanvas;
  public
    constructor Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);
  end;

Huomaa, että TPegSolitaire muuttuja lisätään myös, koska käyttää tätä luokkaa käytetään noutamaan solujen tila.

pascal
constructor TPegSolPainter.Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);
begin
  PegSol := pPegSol;
  Canvas := pCanvas;
end;

Yritettäessä kääntää koodia niin tulee virheilmoitus koska ei olla lisätty PegDatastrucures käännösyksikköä uses-lauseseen. Ja koska käytetään myös TCanvas-luokkaa niin täytyy lisätä Graphics käännösyksikkö samoin.

pascal
uses
  PegDatastructures,
  Graphics,
  Classes, SysUtils;

Syy tämän luokan rakentamiseen oli poistaa kaikki piirtäminen koodilla lomakkeella. Joten on luotava menetelmä, joka tekee piirtämisen. Ennen lisäystä että menetelmä on jotain, johon on puututtava: laskea leveys solun, jaamme paintbox leveyttä useissa solujen. Teoriassa voisimme käyttää ominaisuutta Canvas.Width tähän. Kuitenkin tämä ominaisuus ei aina anna oikeata leveyttä oikeaan aikaan. Joten piirrettäesä soluja täytyy tarjota piirtää menetelmälle oikeat Canvas arvot leveydelle ja korkeudelle.

Nyt tiedetään tämä, voidaan lisätä piirustusmetodi luokkaan.

pascal
  
TPegSolPainter = class
  private
    PegSol      : TPegSolitaire;
    Canvas      : TCanvas;

  public
    constructor Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);
    procedure Repaint(const pCanvasWidth, pCanvasHeight: integer);
  end;
</sourcet>
* Luo aliohjelman Repaint runko. Paina <code>Ctrl</code>+<code>Shift</code>+<code>C</code>.
* Kopio koodi ''TfrmMain.Button1Click(Sender: TObject)''-aliohjelmasta t??h??n runkoon (Jolloin siit?? tulee t??ll??inen)
<syntaxhighlight lang="pascal">
procedure TPegSolPainter.Repaint(const pCanvasWidth, pCanvasHeight: integer);
var
  CellWidth    : integer;
  CellHeight   : integer;
  iRow, iCol   : TCellNums;
  CellArea     : TRect;
begin
  // Calculate the width of each cell to accomodate for all cells
  CellWidth := pbPeg.Width div 7;
  CellHeight := pbPeg.Height div 7;

  // Draw boxes for all cells
  for iRow := 1 to 7 do
    for iCol := 1 to 7 do
    begin
      // Calculate the position of the cell in the paintbox
      CellArea.Top    := (iRow-1) * CellHeight;
      CellArea.Left   := (iCol-1) * CellWidth;
      CellArea.Right  := CellArea.Left + CellWidth;
      CellArea.Bottom := CellArea.Top  + CellHeight;
      // And now draw the cell
      pbPeg.Canvas.Rectangle(CellArea);
    end;
end;

Koska ei enää tarvita TPaintbox-komponenttia pbPeg. Niin on poistettava viittaukset siihen. Joten kolmessa kohdassa tarvitaan muutoksia:

pascal
  CellWidth := pCanvasWidth div 7;
  CellHeight := pCanvasHeight div 7;
pascal
   Canvas.Rectangle(CellArea);

Nyt on luokka, jolla voidaan tehdä haluttu piirtäminen, on aika käyttää sitä. Tätä piirtoluokkaa käytetään yhdessä PegSolitare luokan kanssa solujen piirtämiseen.

pascal
procedure TfrmMain.Button1Click(Sender: TObject);
var
  pegsol  : TPegSolitaire;  // The game data
  pegpaint: TPegSolPainter; // The paint class for the game
begin
end;

Piirtoluokan käyttö on yksinkertaista: Luodaan uusi esiintymä ja kutsutaan piirto aliohjelmaa:

pascal
  
  // Create a new game object
  pegsol := TPegSolitaire.Create(7);

  // Create a new painter object to paint this game
  pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);

  // And paint the board
  pegpaint.Repaint(pbPeg.Width, pbPeg.Height);

  // Clean up
  pegpaint.Free;
  pegsol.Free

Suorita ja testta ohjelma. Nähdään että se toimii samaan tapaan kuin ennen. Tulos näyttää tältä:

Kyse tapahtumista

Mitä on saatu aikaan tähän mennessä? On tietorakenne, joka sisältää kaikki tiedot PegSolitaire peliä varten. On luokka, johon voi piirtää. Ja on melko yksinkertainen lomake, jossa on testipainike. Joten mitä seuraavaksi? Tapahtumat!

Kuten edellisessä osassa on nähty, solun matriisi joka tehtiin hävisi jos lomakkeen kokoa muutettiin. Näin tapahtuu, koska lomake ei tiedä mitään "pikku pelistä". Heti kun lomakkeen mielestä on aika piirtää itse, se tekee niin ja ohittaa "pikku pelin". Mitä se tekee? Se vain lähettää viestin lapsi kontrolleille, että virkistäminen on välttämätön. Tämä viesti on käytettävissämme tapahtumana: OnPaint-tapahtuma.

Tätä tapahtumaa kutsutaan joka kerta kun TPaintbox tarvitsee uudelleenpiirtämistä. Se on paikka johon tehdään piirtäminen.

pascal
procedure TfrmMain.pbPegPaint(Sender: TObject);
var
  pegsol  : TPegSolitaire;  // The game data
  pegpaint: TPegSolPainter; // The paint class for the game
begin
  // Create a new game object
  pegsol := TPegSolitaire.Create(7);

  // Create a new painter object to paint this game
  pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);

  // And paint the board
  pegpaint.Repaint(pbPeg.Width, pbPeg.Height);

  // Clean up
  pegpaint.Free;
  pegsol.Free
end;

Nyt on selvää, että me luoda pelin objekti OnPaint menetelmässä, piirretään tyhjät solut ja sitten tuhota se. Mutta täytyy tallentaa peliolio kunnes peli on päättynyt. Sama koskee piirtämisoliota. Joten OnPaint tapahtuma ei ole loogisin paikka luoda näitä olioita. Lomakkeen luokan määrittely on parempi paikka tallentaa ne.

pascal
  TfrmMain = class(TForm)
    pbPeg: TPaintBox;
    procedure pbPegPaint(Sender: TObject);
  private
    { private declarations }
    pegsol  : TPegSolitaire;  // The game data
    pegpaint: TPegSolPainter; // The paint class for the game
  public
    { public declarations }
  end;

Nämä muuttujat pitää alustaa heti, kun lomake avataan (tai kun halutaan aloittaa uusi peli). Joten luodaan aliohjelma, joka tekee sen meille ja lisätään se private-osioon lomakkeelle.

pascal
  
  private
    { private declarations }
    pegsol  : TPegSolitaire;  // The game data
    pegpaint: TPegSolPainter; // The paint class for the game

    procedure StartNewGame;
pascal
procedure TfrmMain.StartNewGame;
begin
  // Clean up the previous game
  pegpaint.Free;
  pegsol.Free;

  // Start with a new game
  pegsol := TPegSolitaire.Create(7);
  pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);
end;

Nyt kun alustuksen koodi on luotu, se on suoritettava. Looginen aika tehdä tämä on, kun lomake on luodaan (eli sovellus käynnistetään). Tähän toiseen tapahtumaan pääse kiinni: FormCreate-tapahtumassa. Se voidaan luoda kahdella eri tavalla: Komponenttimuokkaimessa löytää OnFormCreate tapahtuma ja klikata "..." -painiketta. Toinen tapa tuottaa se on tuplaklikata lomaketta.

pascal
procedure TfrmMain.FormCreate(Sender: TObject);
begin
  StartNewGame;
end;

Nyt kun peli- ja piirto-oliot luodaan ohjelman alkaessa niitä ei enää tarvita OnPaint aliohjelmassa.

pascal
procedure TfrmMain.pbPegPaint(Sender: TObject);
begin
  // Paint the board
  pegpaint.Repaint(pbPeg.Width, pbPeg.Height);
end;

Intermezzo

Tähän mennessä ollaan keskittynyt rakentamaan ohjelmaa jolla pelata klassista pasianssi peliä 7x7 ruudukossa. Onko nyt mahdollista luoda pienempi tai suurempi ruudukko esim. muun tyyppiselle pelille? Testataan tätä.

Kuten ruudulla nähdään niin vielä on 7x7 ruudukko! Tämä on seurausta kun käyttää (taika) numeroita, jota olisi voitu välttää (ks http://en.wikipedia.org/wiki/Magic_number_%28programming%29#Unnamed_numerical_constants ).

Taikanumeroiden käyttäminen on suuri katastrofi: se on asia, kun, ei jos, ohjelma epäonnistuu.

Siellä nähdään numero 7 muutamia kertoja. Tämä on se maaginen numero, joka tekee temppuja. Laskettaessa solun leveys ja korkeus, tarvitsemme laudalle koon joka on tallennettu pegsol pelin Size muuttujaan. Ja sama pätee iRow ja iCol silmukoihin. Joten korjataan ne kerralla:

pascal
  
// Calculate the width of each cell to accomodate for all cells
  CellWidth := pCanvasWidth div pegsol.Size;
  CellHeight := pCanvasHeight div pegsol.Size;

  // Draw boxes for all cells
  for iRow := 1 to pegsol.Size do
    for iCol := 1 to pegsol.Size do
    begin
      // Calculate the position of the cell in the paintbox
      CellArea.Top    := (iRow-1) * CellHeight;
      CellArea.Left   := (iCol-1) * CellWidth;
      CellArea.Right  := CellArea.Left + CellWidth;
      CellArea.Bottom := CellArea.Top  + CellHeight;
      // And now draw the cell
      Canvas.Rectangle(CellArea);
    end;

Tässä varoitus: pegsol:ssa ei ole julkisesti saatavilla Size -muuttujaa. Ja näin se pitäisi olla: kaikki luokan muuttujat pitäisi olla yksityisiä. Tapa käyttää näitä yksityisiä arvoja on funktioiden tai ominaisuuksien (rajapinta luokan) kautta. Tähän yksinkertaiseen arvoon sovelletaan ominaisuutta.

Luokka nyt näyttää tältä:

pascal
  
TPegSolitaire = class
  private
    FSize: TCellNums;
    PegCells: TPegCells;
    function GetCell(const pRow, pCol: TCellNums): TCellType;
    procedure SetCell(const pRow, pCol: TCellNums; const pValue: TCellType);

  public
    constructor Create(const pSize: TCellNums);
    property Cell[const pRow, pCol: TCellNums]: TCellType read GetCell write SetCell;
    property Size: TCellNums read FSize;
  end;

On yleinen käytäntö käyttää muuttujien etuliitteenä kirjainta F joihin päästään käsiksi ominaisuuksilla. Julkinen pääsy ominaisuuteen Size on vain sen arvon lukeminen koska sitä ei pidä koskaan muuttaa kun peli käynnistetään. Ainoa paikka, jossa tämä yksityinen muuttuja pitäisi saada sen lopullinen arvon on Create rakentaja.

pascal
constructor TPegSolitaire.Create(const pSize: TCellNums);
var iRow,iCol: integer;
begin
  FSize := pSize;
  for iRow := 1 to Size do
    for iCol := 1 to Size do
      Cell[iRow,iCol] := ctNoAccess;
end;

Olemme nyt valmis testaamaan ohjelman ja katso jos se piirtää nyt mukavan 5x5 matriisi.

Nyt kun pelin perusteet ovat paikoillaan, on aika täsmentää käyttöliittymää.

Ollaan taiteellisia

Hienoa. Nyt on peli luokka, tukiluokka piirtämiseen ja ruudullinen pelilauta lomakkeella. Pasianssipelin solu voi olla kolmessa (3) tilassa: ei saatavilla, tyhjä tai käytössä. Kuhunkin solutyyppiin halutaan erilainen graafinen esitys. Tehdään nyt se.

Täällä täytyy tehdä muutoksia. Se riippuu solun tilasta mitä tarvitaan piirtämiseen (ei käytettävissä oleva solu, tyhjä solu tai pelinappula solu). Tapaus ... toiminta tulee pelastus.

pascal
  // Draw boxes for all cells
  for iRow := 1 to pegsol.Size do
    for iCol := 1 to pegsol.Size do
    begin
      // Calculate the position of the cell in the paintbox
      CellArea.Top    := (iRow-1) * CellHeight;
      CellArea.Left   := (iCol-1) * CellWidth;
      CellArea.Right  := CellArea.Left + CellWidth;
      CellArea.Bottom := CellArea.Top  + CellHeight;

      // And now draw the cell based on the cell's contents
      case pegsol.Cell[iRow,iCol] of

        ctNoAccess: // Draw cells that are not accessible
          begin
            Canvas.Brush.Color := clGray;
            Canvas.Rectangle(CellArea);
          end;

        ctEmpty:    // Draw cells that are currently empty
          begin
            Canvas.Brush.Color := clBlue;
            Canvas.Rectangle(CellArea);
          end;

        ctPeg:      // Draw cells that are occupied
          begin
            Canvas.Brush.Color := clBlue;
            Canvas.Rectangle(CellArea);    // Erase the background first
            Canvas.Brush.Color := clGreen; 
            Canvas.Ellipse(CellArea);      // Draw the pegs as green circles
          end;
      end;
    end;

Ohjelmaa voitaisiin ajaa tässä vaiheessa (tai vain kokeilla sitä), mutta se on vain tylsä? (5x5) harmaa ruudukko. Tämä johtuu siitä ettei vielä ole määritelty mitään peliasetuksia. Korjataan sitä seuraavaksi.

pascal
procedure TfrmMain.StartNewGame;
begin
  // Clean up the previous game
  pegpaint.Free;
  pegsol.Free;

  // Start with a new 7x7 game
  pegsol := TPegSolitaire.Create(7);
  pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);

  // Initialize some cells
  pegsol.Cell[3,4] := ctEmpty;
  pegsol.Cell[4,2] := ctPeg;
  pegsol.Cell[4,3] := ctPeg;
  pegsol.Cell[4,4] := ctEmpty;
  pegsol.Cell[4,5] := ctPeg;
  pegsol.Cell[4,6] := ctPeg;
  pegsol.Cell[5,4] := ctEmpty;
end;

Täytä pelilauta

Kuten tiedetään niin klassinen solitaire pelilaudan pitäisi näyttää tältä:

Jos täytetään kaikki solut yksitellen, niin se johtaisi suureen koodimäärään. Mitä jos voitaisiin alustaa peli vain tekstillä, joka symbolisesti kuvaa pelilaudan? Jotain tällaista:

pascal
  
// Initialize the cells to the classic game
  pegsol.InitializeBoard( '  ooo  ' + LineEnding +
                          '  ooo  ' + LineEnding +
                          'ooooooo' + LineEnding +
                          'ooo.ooo' + LineEnding +
                          'ooooooo' + LineEnding +
                          '  ooo  ' + LineEnding +
                          '  ooo  ' );
  1. o on solu jossa on pelinappula.
  2. . on tyhjä, mutta pelattavaa solu.
  3. Välilyönnit osoittavat solut, jotka eivät ole käytettävissä.

Oletetaan että tämä tulee toimimaan ja luodaan aliohjelma TPegSolitaire luokkaan joka voi hoitaa tämän.

pascal
procedure TfrmMain.StartNewGame;
begin
  // Clean up the previous game
  pegpaint.Free;
  pegsol.Free;

  // Start with a new 7x7 game
  pegsol := TPegSolitaire.Create(7);
  pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);

  // Initialize the cells to the classic game
  pegsol.InitializeBoard( '  ooo  ' + LineEnding +
                          '  ooo  ' + LineEnding +
                          'ooooooo' + LineEnding +
                          'ooo.ooo' + LineEnding +
                          'ooooooo' + LineEnding +
                          '  ooo  ' + LineEnding +
                          '  ooo  ' );
end;
pascal
  
public
    constructor Create(const pSize: TCellNums);
    procedure InitializeBoard(const pBoard: ansistring);

    property Cell[const pRow, pCol: TCellNums]: TCellType read GetCell write SetCell;
    property Size: TCellNums read FSize;

Mitä tämä aliohjelman tarvitse tehdä? Se jakaa textstringin osiksi erillisiin riveihin ja sitten käsitellä nämä rivit, koska käytettiin LineEnding-vakiota erottamaan rivit niin voidaan käyttää TStringList luokkaa erottamaan ne.

pascal
procedure TPegSolitaire.InitializeBoard(const pBoard: ansistring);
var lst      : TStringList;
    iRow,iCol: integer;
    s        : string;
begin
  // Create a list with the board text in it. This will split all lines
  // into individual lines, because of the LineEnding 'splitter'.
  lst := TStringList.Create;
  lst.Text := pBoard;

  // Process all lines one at a time
  for iRow := 0 to lst.Count-1 do
    if iRow < Size then // Make sure there is no overflow in the rows
    begin
      // Process a single line of text
      s := lst[iRow];
      for iCol := 1 to length(s) do
        if iCol <= Size then  // Make sure there is no overflow in the columns
          case s[iCol] of
            ' ': Cell[iRow+1,iCol] := ctNoAccess;
            '.': Cell[iRow+1,iCol] := ctEmpty;
            'o': Cell[iRow+1,iCol] := ctPeg;
          end;
    end;

  // Clean up the list
  lst.Free;
end;
  1. TStringList käytetään puskurina. Tämä toimii, koska LineEnding:a käytettiin rivien erottimena.
  2. Count kertoo rivien määrän, mutta ne on numeroitu 0..Count-1. Ja solut on numeroitu aloittaen ykkösestä(1). Siksi on solua osoittaessa tehtävä iRow + 1.

Edellä esitetty menettely sisältää paljon ylimääräisiä muuttujia ja ei pitäisi olla kovin vaikea ymmärtää. On mahdollista vähentää aliohjelma minimiin kuten näin:

pascal
procedure TPegSolitaire.InitializeBoard(const pBoard: ansistring);
var iRow,iCol: integer;
begin
  with TStringList.Create do
  begin
    Text := pBoard;
    for iRow := 0 to Min(Count-1, Size-1) do
      for iCol := 1 to Min(length(Strings[iRow]),Size) do
        case Strings[iRow][iCol] of
          ' ': Cell[iRow+1,iCol] := ctNoAccess;
          '.': Cell[iRow+1,iCol] := ctEmpty;
          'o': Cell[iRow+1,iCol] := ctPeg;
        end;
    Free;
  end;
end;

Tämä menettely tekee täsmälleen saman. Se käyttää kätevää ominaisuutta, sitä että voidaan käyttää With-lausetta kun luodaan dynaamisesti oliota. Toimiakseen tämä aliohjelma vaatii että lisätään Math käännösyksikkö uses lauseen luetteloon.

Tapahtumien uudelleenkäynti

Se ei kovin vaikuttava vielä, mutta tehty on jo paljon. Tärkein joka puuttuu tällä hetkellä on kyky hypätä solujen poistaa ne pelilaudalta. Ja että siihen käytetään hiirtä. Mutta miten? Tutkitaan mahdollisuuksia.

On kaksi tapahtumaa, jotka ovat mielenkiintoisia: OnMouseDown ja OnMouseUp. Katsotaan mitä tapahtuu OnMouseDown tapahtumassa.

Syntyvään aliohjelman otsikkoon sisältyy useita muuttujia. Kaksi niistä ovat X ja Y. Ne sisältävät TPaintBox:n paikan jossa hiirtä painettiin. Tehdään ne näkyviksi lisäämällä joitakin tietoja TMemo-komponenttiin joka on lisätty lomakkeelle.

pascal
Memo1.Append( format('Mouse down @ %dx%d',[X,Y]) );

Ilmeisesti ei voi käyttää "raakaa" X ja Y-koordinaatteja; ne ovat liian isoja 7x7 pelilaudalle. Ne täytyy sovittaa solun rivin ja sarakkeen koordinaattuin. Sitä varten tarvitaan toiminto, joka sovittaa X / Y-koordinaatin solun koordinaattiin. Tähän tarvitaan paintbox:n leveys ja korkeus, solujen lukumäärä ruudukossa ja yksittäisen solun leveys ja korkeus. On yksi paikka, jossa kaikki tämä tulee yhdessä ... pn Paint luokka. Tämä on siis selvä paikka tehdä tämä laskelma.

pascal
type
  TCellNums = 1..C_MAX;
  TCellType = (ctNoAccess, ctEmpty, ctPeg);
  TPegCells = array[TCellNums, TCellNums] of TCellType;

  TCellPosition = record
    Row: TCellNums;
    Col: TCellNums;
  end;
pascal
  public
    constructor Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);

    procedure Repaint(const pCanvasWidth, pCanvasHeight: integer);
    function CanvasXYtoCell(const pX, pY: integer): TCellPosition;
pascal
function TPegSolPainter.CanvasXYtoCell(const pX, pY: integer): TCellPosition;
begin
  result.Col := (pX div CellWidth)  + 1;
  result.Row := (pY div CellHeight) + 1;
end;

Edellä koodi kartoittaa suuret X / Y-koordinaatit vastaamaan solun rivi ja sarake numeroita. Yksi ongelma on kuitenkin: Täällä CellWidth ja CellHeight ei ole saatavilla. Ne ovat välituloksia Repaint aliohjelman eikä niitä tallenneteta. Tämä pitää korjata.

pascal
  TPegSolPainter = class
  private
    PegSol      : TPegSolitaire;
    Canvas      : TCanvas;
    CellWidth   : integer;
    CellHeight  : integer;
pascal
procedure TPegSolPainter.Repaint(const pCanvasWidth, pCanvasHeight: integer);
var
  iRow, iCol   : TCellNums;
  CellArea     : TRect;
begin

Nyt voidaan muuttaa aliohjelmaa, missä hiiren napin alaspainamisen koordinaattia käsiteltiin. Uuden funktion X / Y-paikka voidaan nyt sovittaa solun koordinaatteihin.

pascal
procedure TfrmMain.pbPegMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var CellRC: TCellPosition;
begin
  CellRC := pegpaint.CanvasXYtoCell(X,Y);
  Memo1.Append( format('Mouse down @ %dx%d',[CellRC.Row, CellRC.Col]) );
end;

Tehdään sama OnMouseUp tapahtumaan:

pascal
procedure TfrmMain.pbPegMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  with pegpaint.CanvasXYtoCell(X,Y) do
    Memo1.Append( format('Mouse up @ %dx%d',[Row, Col]) );
end;

Huomaa, että tämä menetelmä tekee täsmälleen sama kuin OnMouseDown, mutta käyttäen with-rakennetta säästetään vaivaa määrittää paikallinen muuttuja (vähemmän on enemmän ...).

Testataan tätä:

Joka kerta kun painaa hiiren painiketta ja vapauttaa sen, niin solu, jossa hiiren painiketta painettiin tai vapautettiin näkyy muistiossa.

Liimataan asioita yhteen

Tämä ohjelma toimii vähän kuin laskin ohjelma:

  1. Käyttäjä valitsee pelinappulan jota siirretään painamalla hiiren painiketta.
  2. Käyttäjä siirtää pelinappulan toiseen pelinappulan yli tyhjään soluun (hyppy).
  3. Käyttäjä vapauttaa hiiren painikkeen ja siirto tapahtuu.

Tällä ohjelma ei muista mitä solua painettiin. Joten kun käyttäjä painaa hiiren painiketta solun päällä, meidän täytyy säilyttää tämä paikka jonnekin.

pascal
  
private
    { private declarations }
    pegsol  : TPegSolitaire;  // The game data
    pegpaint: TPegSolPainter; // The paint class for the game
    FromCell: TCellPosition;  // The peg that is going to leap
pascal
   FromCell := pegpaint.CanvasXYtoCell(X,Y);

Nyt kun käyttäjä vapauttaa hiiren painikkeen, on suoritettava hyppy (myös tarkistettava, onko se ok ja päivittää pelilautaa). Ollaan optimistisia (muista Top Down huomautus?) Ja olettaa, on aliohjelma, joka tekee sen meille: Leap (<FromCell>, <ToCell>).

pascal
  pegsol.Leap(FromCell, pegpaint.CanvasXYtoCell(X,Y));

FromCell oli jo pelinappula kun hiiren näppäintä painettiin. Kohdesolun lasketaan yksinkertaisesti kutsumalla pegpaint.CanvasXYToCell (...). Voisimme ottaa käyttöön paikallinen muuttuja sitä, mutta se ei ole välttämättä tarpeen.

pascal
  pbPeg.Repaint;

Tämä pakottaa paintbox:n piirtämään itsensä, joten kaikki päivitetyt solut näyttävät oikean sisällön.

Täydellinen OnMouseDown aliohjelma nyt näyttää tältä:

pascal
procedure TfrmMain.pbPegMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  with pegpaint.CanvasXYtoCell(X,Y) do
    Memo1.Append( format('Mouse up @ %dx%d',[Row, Col]) );
  pegsol.Leap(FromCell, pegpaint.CanvasXYtoCell(X,Y));
  pbPeg.Repaint;
end;

Ennenkuin mennään Leap aliohjelmaan niin vähän lisätoimintoja otetaan käyttöön. Muista, että voidaan noutaa solun tila (tyhjä, pelinappula tai ei pääsyä) TPegSolitaire:n Cell ominaisuuden kautta! Esimerkiksi pegsol.Cell [2,2] antaisi rivin 2 ja sarakkeen 2 solun tilan. Mutta tähän väliin, on otettu käyttöön uusi tietorakenne: TCellPosition. Niinpä esimerkiksi, jos halutaan saada tila FromCell:stä tarvitaan seuraava hankala lause:

pascal
   state := pegsol.Cell[ FromCell.Row, FramCell.Col ]

Olisi mukavaa, jos voisi kirjoittaa jotain:

pascal
  state := pegsol.GetCell(FromCell)

Lisätään tämä peli luokkaan.

pascal
  TPegSolitaire = class
  private
    FSize: TCellNums;
    PegCells: TPegCells;
    function GetCell(const pRow, pCol: TCellNums): TCellType;
    procedure SetCell(const pRow, pCol: TCellNums; const pValue: TCellType);
    function GetCell(const pPosition: TCellPosition): TCellType;

Nyt on kaksi samannimistä GetCell funktiota, mutta niiden parametriluettelo on erilainen. Joten Lazarus tietää tarkalleen, milloin kutsua kyseistä funktiota.

pascal
  result := Cell[pPosition.Row, pPosition.Col];

Takaisin "pääjuoneen". On otettu käyttöön aliohjelma Leap (...), mutta sitä ei vielä ole. Se on aliohjelma, joka muuttaa peliä pelilaudalla, joten paikka mihin laittaa se on TPegSolitaire luokka.

pascal
  public
    constructor Create(const pSize: TCellNums);
    procedure InitializeBoard(const pBoard: ansistring);
    procedure Leap(const pFromCell, pToCell: TCellPosition);

Nyt tämä aliohjelma tekee kaiken kovan työn: tarkistaa, että solut ovat voimassa ja onko hyppy on suinkin mahdollista. Tämä ei ole vaikeaa, mutta tarkastusten määrä on varsin laaja jotain, joka näyttää yksinkertaista. Koodi on melko itsestään selvä, ja jos ei, toivottavasti kommentit selittää mitä tapahtuu. Huomaa, että aliohjelma käyttää GetCell (<cellposition>) funktiota joka luotiin aikaisemmin.

pascal
procedure TPegSolitaire.Leap(const pFromCell, pToCell: TCellPosition);
var dx, dy: integer;
    JumpedCell: TCellPosition;
begin
  // Verify that the start cell is occupied and the target cell is empty
  // If not, leave the procedure via the EXIT.
  if (GetCell(pFromCell) <> ctPeg) or
     (GetCell(pToCell) <> ctEmpty) then
    EXIT;

  // Calculate the horizontal and vertical distance between the cells
  dx := abs(pFromCell.Col - pToCell.Col);
  dy := abs(pFromCell.Row - pToCell.Row);

  // A valid move has one direction equal to zero and the other equal to 2
  if    ((dx = 2) and (dy = 0))
     or ((dx = 0) and (dy = 2)) then
  begin
    // Determine the position of the jumped cell; it's in the middle
    JumpedCell.Col := (pFromCell.Col + pToCell.Col) div 2;
    JumpedCell.Row := (pFromCell.Row + pToCell.Row) div 2;

    // Final check: is there a peg in the jumped cell?
    if GetCell(JumpedCell) = ctPeg then
    begin
      // Jump: clear the FromCell, empty the jumped cell and populate the ToCell
      Cell[pFromCell.Row, pFromCell.Col]   := ctEmpty;
      Cell[JumpedCell.Row, JumpedCell.Col] := ctEmpty;
      Cell[pToCell.Row, pToCell.Col]       := ctPeg;
    end;
  end;
end;

Usko tai älä, mutta nyt on toimiva Peg Solitaire lautapasianssi peliohjelma!

Puhdistusta

Lomakkeelle meillä on vielä muistio komponentti, joka näyttää hiiren klikkauksella. Sitä enää todellakaan tarvita.

pascal
procedure TfrmMain.pbPegMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  FromCell := pegpaint.CanvasXYtoCell(X,Y);
end;

procedure TfrmMain.pbPegMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  pegsol.Leap(FromCell, pegpaint.CanvasXYtoCell(X,Y) );
  pbPeg.Repaint;
end;

Mitä seuraavaksi?

Jotta tämä olisi täysin toimiva peli tarvitsemme tapa muuttaa pelilautaa ilman että kääntää sovellus uudelleen. Ja grafiikka voitaisiin parantaa. aletaan käsitellä ensinmäistä kysymystä ensin.

Luodaan enemmän pelilautoja

Olisi mukavaa, jos voidaan lisätä pelilautoja peliin ilman että joutuisi kääntämään uudelleen sovelluksen. Ilmeinen paikka tallentaa pelilaudan kaavat olisi ulkoiset tekstitiedosto. Käyttäjä voi sitten aloittaa uuden pelin avaamalla jonkun tekstitiedoston. Hauska asia on, se on helppo lisätä, koska ollaan jo tehty suurimman osan kovasta työstä. Katsotaanpa ensin tehdä kaksi pelilaudan kaavoista.

pascal
  ooo
  ooo
ooooooo
ooo.ooo
ooooooo
  ooo
  ooo

Lisää toinen tiedosto:

pascal
  ...
  .o.
..ooo..
.ooooo.
ooooooo
  ...
  ...

On monia tapoja luoda ja avata uusia pelilautoja. Tässä opetusohjelmassa käytetään valikko lähestymistapaa (perusteellisempi kuvaus valikosta on jossain toisessa opetusohjelmassa).

Valikko nyt näyttää tältä:

Valikko on myös näkyvissä lomakkeen yläosassa.

pascal
  Close;

Tämä sulkee lomakkeen. Ja koska se on ainoa lomake niin myös sovellus loppuu.

Ok, nyt on aika tarkastella, miten avata pelilauta tiedostoja. On olemassa vakiokomponentteja ja valikko käytettävissä tehdä tämä, mutta tehdään se manuaalisesti.

Hyvä niin. Nyt todella avata tiedoston, kun valikko on valittuna:

pascal
  if OpenDialog1.Execute then
    ShowMessage(OpenDialog1.FileName);

Muista: edetä pienin askelin. Nyt on lisätty valikko lomakkeelle josta voi poistua sovelluksesta tai ladata pelilaudan pohjan levyltä. Vielä ei ole ??koodattu latausosaa, mutta yritetään ensin testata koodia joka on jo tehty.

Nyt viimeistellään latauskoodi. Koodi on melko itsestään selvä ja käyttää uudelleen joitakin aliohjelmia jotka on jo luotu.

pascal
procedure TfrmMain.MenuItem4Click(Sender: TObject);
begin
  // Open the pop up dialog
  if OpenDialog1.Execute then
  begin
    // Start with a new empty board
    StartNewGame;

    // Dynamically create a stringlist to load the board layout
    with TStringList.Create do
    begin
      // Load the board layout from the textfile
      LoadFromFile(OpenDialog1.FileName);

      // Initialize the board with the file's contents
      pegsol.InitializeBoard(Text);

      // Clean up the stringlist
      Free
    end;

    // After loading the new board update the paintbox
    pbPeg.Repaint;
  end;
end;

Ja se kaikki on siinä!

Suorita ohjelma, avaa pelilaudan pohjatiedosto ja pelaa peliä!

Silmänruokaa

Nyt on toimiva Peg Solitaire sovellus. Siellä on vielä paljon asioita parannettavaksi, mutta peli itsessään toimii hienosti. Viimeinen osa tätä opetusohjelmaa on parantaa pelin ulkonäköä. Toki peli toimii, mutta hieman näyttävämpi grafiikka olisi mukava. Ja se on melko helppo lisätä. Kaikki mitä tarvitaan on kolme kuvaa solutyyppejä varten, päivittää TPegSolPainter-luokkaa ja tärkein muoto, kerro maalari käyttää kuvia. Joten lisää kolme kuvaa TPegSolPainter luokkaan:

pascal
  TPegSolPainter = class
  private
    PegSol     : TPegSolitaire;
    Canvas     : TCanvas;
    CellWidth  : integer;
    CellHeight : integer;

    ImgNoAccess: TPicture;
    ImgEmpty   : TPicture;
    ImgPeg     : TPicture;
pascal
  public
    constructor Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);
    procedure Repaint(const pCanvasWidth, pCanvasHeight: integer);
    function CanvasXYtoCell(const pX, pY: integer): TCellPosition;
    procedure LoadImage(const pCellType: TCellType; const pFilename: string);
pascal
procedure TPegSolPainter.LoadImage(const pCellType: TCellType; const pFilename: string);

  procedure UpdateImage(var pImg: TPicture; const pNewImage: TPicture);
  begin
    pImg.Free;
    pImg := pNewImage
  end;

var pic: TPicture;
begin
  // Make sure the file exists
  if FileExists(pFilename) then
  begin
    // First load the picture in a local variable
    pic := TPicture.Create;
    pic.LoadFromFile(pFilename);

    // Now update the required image based on the pCelltype parm
    case pCelltype of
      ctNoAccess: UpdateImage(ImgNoAccess, pic);
      ctEmpty   : UpdateImage(ImgEmpty, pic);
      ctPeg     : UpdateImage(ImgPeg, pic);
    end;
  end;
end;

Koodi on taas aika itsestään selvä. Huomaa, että käytetään lyhyttä paikallista aliohjelmaa päivittämään kuvaa. Jos emme tekisi sitä sitten case-lause ((case pCellType of) olisi monimutkaisempi (eli sisältäisi ylimääräistä koodia). Nyt kukin solutyyppi tarvitsee vain yhden rivin päivittäkseen oikean kuvan.

Nyt kun kuvia on lisätty luokkaan, täytyy käyttää niitä ja se tietenkin tapahtuu Repaint aliohjelmassa. Jokaista celltype tarkistetaan, onko kuva on saatavilla. Ja jos on niin käytetäään tavallisen piirustuksen sijasta kuvaa.

pascal
      
// And now draw the cell based on the cell's contents
      case pegsol.Cell[iRow,iCol] of

        ctNoAccess: // Draw cells that are not accessible
          if not assigned(ImgNoAccess) then
            begin
              Canvas.Brush.Color := clGray;
              Canvas.Rectangle(CellArea);
            end
          else with ImgNoAccess do
            Canvas.CopyRect(CellArea, Bitmap.Canvas, Rect(0,0,Width,Height));

        ctEmpty:    // Draw cells that are currently empty
          if not assigned(ImgEmpty) then
            begin
              Canvas.Brush.Color := clBlue;
              Canvas.Rectangle(CellArea);
            end
          else with ImgEmpty do
            Canvas.CopyRect(CellArea, Bitmap.Canvas, Rect(0,0,Width,Height));

        ctPeg:      // Draw cells that are occupied
          if not assigned(ImgPeg) then
            begin
              Canvas.Brush.Color := clBlue;
              Canvas.Rectangle(CellArea);  // Erase the background first
              Canvas.Brush.Color := clGreen;
              Canvas.Ellipse(CellArea);
            end
          else with ImgPeg do
            Canvas.CopyRect(CellArea, Bitmap.Canvas, Rect(0,0,Width,Height));

      end;

Lisätään kuva latauskoodi piirtoluokkaan.

pascal
  // Start with a new 7x7 game
  pegsol := TPegSolitaire.Create(7);
  pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);

  pegpaint.LoadImage(ctNoAccess, 'tutpeg_cellforbidden.jpg');
  pegpaint.LoadImage(ctEmpty,    'tutpeg_cellempty.jpg');
  pegpaint.LoadImage(ctPeg,      'tutpeg_celloccupied.jpg');

Varmista, että kuvat ladataan ja laitetaan ohjelman bin kansioon, tai vielä parempaa, luot omat kuvat. Huom. kun lataat allaolevia kuvia, klikkaa niitä ensin.

Viimeinen "helmi"

Useimmilla ohjelmilla on oma kuvake, joka näkyvät valikon ja tiedoston selaus ikkunoissa. On helppo lisätä oma kuvake sovellukseen.

Sovelluksella on nyt oma kuvake, joka näkyy valikossa ja tiedoston selaus ikkunassa.


From the Free Pascal wiki, page "Peg Solitaire tutorial/fi", as it stood on 2019-07-13 after a change by Djzepi, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's Licensing page. If you have concerns about this material, message the admin.

Revision 2, last changed by WikiArchive .

Talk about this page

Nobody has opened a bench for this page yet. Sign in to open one.