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:
table: Egy Morph típusú objektum. A megjelenített játék ablakot reprezentálja.
fields: Kétdimenziós tömb, amely GameField típusú (ld. lentebb) objektumokat tartalmaz. A játékmezőket reprezentálja.
gapBetweenFields, gapOnTheEdge: A mezők közti távolság, illetve a mezők mellett, a játéktábla szélén kihagyott rész hossza.
numOfRows, numOfColumns: Azt reprezentálják, hogy hány sorbanm illetve hány oszlopban helyezkedjenek el a mezők.
currentPlayerNum: A soron következő játékos sorszáma.
restartWindow: A játék végén megjelenő ablak.
state: A játék állapota. Kétféle lehet: match és finished. A jelentősége az, hogy finished állapotban nem lehet jeleket rakni a játékmezőkre.
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:
game: Az a TicTacToe típusú objektum, amely a mezőt létrehozta.
position: Point típusú objektum: a táblán elfoglalt pozíció.
controllingPlayer: A mezőt elfoglaló játékos száma.
sign: A mezőre rákerült jelzés. ImageMorph típusú objektum. Azért kell eltárolnunk, hogy később le tudjuk venni.
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:
game: Az a TicTacToe típusú objektum, amelyhez a gomb tartozik.
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 |