ELTE Informatikai kar - Prognyelvek portál

Smalltalk

Példaprogram: TicTacToe

A feladat

A példaprogram a népszerű TicTacToe (amőba) játékot valósítja meg. A grafikus felülethez Morphic-ot használtam, tehát a program tekinthető a felület bemutatásaként is.

A játék menete

Indításkor megjelenik a játék ablak:



Két játékos felváltva “lépked”, azaz kattint valamely szabad mezőre. A kattintás helyén megjelenik az adott játékoshoz tartozó szimbólum: egy kör, vagy egy X. Amelyik játékosnak függőleges, vízszintes, vagy átlós irányban kigyűlik 5 szimbólum, megnyeri a játékot, erről egy felugró ablak is tájékoztatja a felhasználókat. Az ablakon található gomb segítségével ekkor újraindíthatjuk a játékot. Megjegyzendő, hogy erre a játék közben, a “Restart” gomb lenyomásával is van lehetőség.

Megvalósítás

A játék fő osztálya a TicTacToe osztály. Ennek fontosabb attribútumai:

Az osztály kódja:

Object subclass: #TicTacToe

instanceVariableNames: 'table fields fieldSize fieldColor gapBetweenFields gapOnTheEdge numOfRows numOfColumns symbols symbolsNeeded currentPlayerNum restartWindow restartButtonExtent state'

classVariableNames: ''

poolDictionaries: ''

category: 'Game'


Inicializálás

A TicTacToe osztálynak egy attribútuma a table, ami egy morph típusú objektum. Kezdetben ez mindössze egy szürke négyzet, erre kerül rá numOfRows*numOfColumns darab GameField típusú objektum (ezekről lentebb), illetve az újraindításra és a kilépésre használható gombok (RestartButton, ExitButton). Ezek inicializálása történik az initialize metódusban, amely használja a fillRow: rowInd metódust. A fillRow egy segéd eljárás, amely arra jó, hogy feltölt egy sort játékmezőkkel, megfelelően elhelyezi őket és hozzáadja submorph-ként a játéktáblába. Az initSymbols eljárás pedig beállítja az egyes játékosok lépésekor milyen képfájl töltődjön be a játékmezőre.

Ezen eljárások kódja:

initialize

| restartButton exitButton |



super initialize.

state:='match'.

fieldSize:=(25@25).

restartButtonExtent:=(50@25).

gapBetweenFields:=5.

gapOnTheEdge:=10.

numOfRows:=15.

numOfColumns:=15.

table:=Morph new color:Color veryVeryLightGray.

table extent: ((((fieldSize x)*numOfColumns)+((numOfColumns-1)*gapBetweenFields)+(gapOnTheEdge*2))@((fieldSize y)*numOfRows+((numOfRows-1)*gapBetweenFields)+(gapOnTheEdge*3)+(restartButtonExtent y))).

fieldColor:=Color lightBlue.

fields:=Array new: numOfRows.

1 to: numOfRows do: [ :i | self fillRow: i.].

1 to: numOfRows do: [ :i | (fields at: i) do: [:field | table addMorph: field. field extent: fieldSize]].

self initSymbols.

symbolsNeeded:=5.

currentPlayerNum:=1.

restartButton:=RestartButton new initializeWithLabel: 'Restart' parent: self.

restartButton extent: restartButtonExtent.

restartButton position: ((table left)+gapOnTheEdge*2+((restartButton extent x)/2)@(table bottom)-gapOnTheEdge-(restartButton extent y)).

table addMorph: restartButton.

exitButton:=ExitButton new initializeWithLabel: 'X' parent: self.

exitButton extent: (25@25).

exitButton position: ((table right)@(table bottom)-gapOnTheEdge-(restartButton extent y)).

table addMorph: exitButton.

table openInWorld.


fillRow: rowInd



| array |

array:=Array new: numOfColumns.

1 to: numOfColumns do: [ :j | array at: j put: (GameField parent: self color: fieldColor fieldPos: (j@rowInd)).].

1 to: numOfColumns do: [ :j | ((array at: j) position: table position+((j-1)*(fieldSize x+gapBetweenFields)+gapOnTheEdge@((rowInd-1)*(fieldSize y+gapBetweenFields)+gapOnTheEdge)))].

fields at: rowInd put: array


initSymbols



symbols:=Array new: 2.

symbols at: 1 put: 'x.jpg'.

symbols at: 2 put: 'o.jpg'


Az újraindítás akár játék közben, akár a játék végén történik, a restart eljárás feladata.

restart



fields do: [:row | row do: [:field | (field controllingPlayer=0) ifFalse: [field reset]]].

(state='finished') ifTrue: [restartWindow delete].

CurrentPlayerNum:=1.

state:='match'

Adattagok elérése

Egysoros eljárások biztosítják a szükséges adattagok külső elérését, illetve esetleges megváltoztatását. Kicsit kilóg a sorból az exit metódus, amely bezárja a játékot. Azért tettem ide mégis, mert ez is a játék osztályának külső manipulálására szolgál.

currentPlayerNum

^currentPlayerNum


currentPlayerSymbol

^(symbols at: currentPlayerNum)


state

^state

state: newState

state:=newState

exit

table delete

Ellenőrzés

Külön kategóriát definiáltam az ellenőrzéssel foglalkozó műveleteknek – természetesen itt annak ellenőrzéséről van szó, hogy a játék véget ért-e. A symbolDrawn: position metódust a position pozíción levő mező (Point típusú objektum, a táblában elfoglalt sor és oszlop koordinátát tartalmazza) hívja meg akkor, ha valamely játékos jelet tett rá. Ekkor történik meg az ellenőrzés, a checkIfFinished: position eljárás által.

A checkIfFinished a következőképp működik: egy checkDirection: d position: pos nevű eljárást hívogat meg minden lehetséges “irányra”: az egyes irányokat “irányvektorokkal” ábrázoltam: olyan Point típusú objektumok, amelyeknek x és y koordinátája csak 0, 1 vagy -1 lehet. A 0 azt jelenti, hogy az a bizonyos koordináta nem változik, az 1 azt, hogy nő, a -1 pedig, hogy csökken. Így lesz pl. az (1@-1) pont a jobb oldalra felfelé mutató átlós irány. A checkDirection, ha kap egy irányt, annak automatikusan az ellenkezőjét is megvizsgálja, hogy az adott irányhoz tartozó összes mezőt megvizsgálja. Ezért elegendő a checkDirection-t mindössze 4 irányra meghívni 8 helyett.

Bár a checkDirection a privát függvények közé tartozik, a kódját ide másolom, mert szorosan kötődik a másik két metódushoz.

symbolDrawn: position



(self checkIfFinished: position) ifTrue: [state:='finished'. self showWinner].

self changePlayer


checkIfFinished: pos



^((self checkDirection: (0@1) position: pos) | (self checkDirection: (1@1) position: pos) | (self checkDirection: (1@0) position: pos) | (self checkDirection: (1@-1) position: pos))


checkDirection: d position: pos



|numOfSymbols i j|

numOfSymbols:=1.

i:=d x.

j:=d y.

[((numOfSymbols<symbolsNeeded) & ((self outOfBounds: ((pos x)+i) bounds: numOfColumns) not) & ((self outOfBounds: ((pos y)+j) bounds: numOfRows) not)) and: [(((fields at: ((pos y)+j)) at: ((pos x)+i)) controllingPlayer)=currentPlayerNum]] whileTrue: [i:=i+(d x). j:=j+(d y). numOfSymbols:=numOfSymbols+1].

i:=(-1)*(d x).

j:=(-1)*(d y).

[((numOfSymbols<symbolsNeeded) & ((self outOfBounds: ((pos x)+i) bounds: numOfColumns) not) & ((self outOfBounds: ((pos y)+j) bounds: numOfRows) not)) and: [(((fields at: ((pos y)+j)) at: ((pos x)+i)) controllingPlayer)=currentPlayerNum]] whileTrue: [i:=i-(d x). j:=j-(d y). numOfSymbols:=numOfSymbols+1].

^(numOfSymbols>=symbolsNeeded)

Privát függvények

Ebbe a kategóriába tettem azokat az eljárásokat, amelyek csak a TicTacToe osztály belső használatára lettek írva. A metódusok a checkDirection-ön kívül magától értetődőek, ez utóbbiról azonban már fentebb szó volt.


changePlayer



(currentPlayerNum=2)

ifTrue: [currentPlayerNum:=1.]

ifFalse: [currentPlayerNum:=currentPlayerNum+1.]


min: int1 and: int2



(int1 < int2)

ifTrue: [^int1]

ifFalse: [^int2]


outOfBounds: ind bounds: anInt



^((ind <= 0) | (ind > anInt))


showWinner

|b s|



restartWindow:=SystemWindow labelled: 'Game finished'.

restartWindow setWindowColor: Color lightBlue.

b:=RestartButton new initializeWithLabel: 'Restart' parent: self.

b color: restartWindow color.

b extent: restartButtonExtent.

restartWindow addMorph: b.

s:=StringMorph new.

s contents: 'Player ', (currentPlayerNum radix: 10), ' has won!'.

restartWindow addMorph: s.

restartWindow openInWorld.

restartWindow minimumExtent: (100@100).

restartWindow extent: (200@100).

b position: (((restartWindow center x)-30) @ ((restartWindow center y)+10)).

s position: (((restartWindow center x)-57) @ ((restartWindow center y)-15)).

A GameField osztály:

Ez a Morph osztályból származtatott osztály reprezentálja a játékmezőket.

Attribútumai:

Az osztály kódja:

Morph subclass: #GameField

instanceVariableNames: 'game position controllingPlayer sign'

classVariableNames: ''

poolDictionaries: ''

category: 'Game'

Inicializálás

Inicializáláskor mindössze a létrehozó játékot, a színt és a pozíciót kapja meg a mező. A többi attribútumot (pl. méret) kívülről állítja be a játéktábla.

initialize: parent color: selfColor fieldPos: pos



super initialize.

game:=parent.

self color: selfColor.

position:=pos.

controllingPlayer:=0

Megjelenítés

Ebbe a kategóriába a drawOn metódus felüldefiniálása került: mint ahogy az a nyelvleírás Morphic-ról szóló részében említve van, ez az eljárás határozza meg, hogy hogy nézzen ki az a grafikus elem, ami az objektumunkat reprezentálja. Itt viszonylag egyszerű a dolgunk, mindössze egy egyszínű négyzetet kell létrehoznunk – a méret és a szín ekkor már adott.

drawOn: aCanvas



|field|

field:=self bounds.

aCanvas fillRectangle: field color: self color.

Adattagok elérése

Ezen osztály esetében is külső elérésként értelmeztem a restart metódust, ugyanúgy, ahogy a TicTacToe osztálynál.

controllingPlayer

^controllingPlayer



reset

self removeMorph: sign.

controllingPlayer:=0

Eseménykezelés

Az osztály egyféle eseményt kezel, mégpedig az egérgomb lenyomást – ennek eseménykezelője a mouseDown: anEvent. Ebben annyi történik, hogy megvizsgáljuk, éppen folyamatban van-e egy játék (tehát a TicTacToe osztály state attribútuma match értékű-e), illetve hogy még senki nem tett-e erre a mezőre már korábban. Ha minden rendben van, meghívjuk a drawSymbol metódust, amely beállítja, hogy melyik játékosé lett a mező, illetve kirajzolja a játékoshoz tartozó szimbólumot.

handlesMouseDown: anEvent

^true.



mouseDown: anEvent

(game state='match' & controllingPlayer=0)

ifTrue: [self drawSymbol.]


drawSymbol



controllingPlayer:=(game currentPlayerNum).

sign:=((ImageReadWriter formFromFileNamed: (game currentPlayerSymbol)) asMorph).

game symbolDrawn: position.

sign extent:(5@5).

sign position: ((((self position) x)+1)@(((self position) y)+1)).

self addMorph: sign.

self changed


RestartButton, ExitButton

Ezt a két osztályt egyben tárgyalom, mert nagyon hasonlóak. Mindkettő a SimpleButtonMorph nevű, beépített Morphic gomb osztályból van származtatva. A RestartButton feladata a játék újraindítása, az ExitButton-é pedig értelemszerűen az abból való kilépés.

Attribútumaik:

A két osztály kódja:

SimpleButtonMorph subclass: #RestartButton

instanceVariableNames: 'game'

classVariableNames: ''

poolDictionaries: ''

category: 'Game'


SimpleButtonMorph subclass: #ExitButton

instanceVariableNames: 'game'

classVariableNames: ''

poolDictionaries: ''

category: 'Game'

Inicializálás

Mindkét gombot létre lehet hozni az initializeWithLabel: labelString parent: parent metódussal. Ez eredetileg a SimpleButtonMorph osztály egy metódusa és az a lényege, hogy inicializáláskor meg lehet adni egy String-et, amely a gomb felirata lesz. Ezt én annyival toldottam meg, hogy második paraméterként a metódus megkapja a “szülő” TicTacToe típusú objektumot is.

initializeWithLabel: labelString parent: parent



super initializeWithLabel: labelString.

game:=parent

Eseménykezelés

Ez az egyetlen pont, ahol a két osztály különbözik egymástól. Mindkettő természetesen az egérgomb lenyomás eseményre reagál.

Mindkét osztálynál közös:

handlesMouseDown



^true


RestartButton:

mouseDown: anEvent



game restart


ExitButton:

handlesMouseDown



^true