Tema propune implementarea câtorva elemente din mecanica jocului Bloxorz. Puteți juca Bloxorz aici.
Tema este împărțită în 2 etape:
Deadline-ul depinde de semigrupa în care sunteți repartizați. Restanțierii care refac tema și nu refac laboratorul beneficiază de ultimul deadline, și anume în zilele de 24.05, respectiv 31.05.
Rezolvările tuturor etapelor pot fi trimise până în ziua laboratorului 12 (deadline hard pentru toate etapele). Orice exercițiu trimis după un deadline soft se punctează la jumătate. Cu alte cuvinte, nota finală pe etapă se calculează conform formulei: n = (n1 + n2) / 2 (n1 = nota obținută înainte de deadline; n2 = nota obținută după deadline). Când toate submisiile preced deadline-ul, nota pe ultima submisie constituie nota finală (întrucât n1 = n2).
În fiecare etapă, veți valorifica ce ați învățat în săptămâna anterioară și veți avea la dispoziție un schelet de cod, cu toate că rezolvarea se bazează în mare măsură pe etapele anterioare. Enunțul caută să ofere o imagine de ansamblu atât la nivel conceptual, cât și în privința aspectelor care se doresc implementate, în timp ce detaliile se găsesc direct în schelet.
Fiecare nivel (stage) din Bloxorz conține o suprafață de joc formată din pătrățele (tiles) care pot fi normale sau au diverse proprietăți. Jucătorul controloează un bloc care trebuie dus prin mișcări sus / jos / dreapta / stânga până la trecerea printr-o pătrățică lipsă din suprafața de joc (o numim scop sau gaură). Puteți urmări rezolvarea nivelurilor de Bloxorz aici.
Aveți în directorul ref
un fișier cu codurile pentru fiecare nivel (1-6) și cu imagini ale stării intițiale pentru fiecare nivel.
În această temă, trebuie să acoperiți următoarele puncte:
În etapa 1 ne interesează reprezentarea stării și implementarea mecanicii principale de mișcare a blocului (fără partea de switches).
În ambele etape veți lucra numai în fișierul blox.pl
.
intern_*
și tt*
, nu aveți nevoie de ele.
Faceți o implementare cât mai generală, nu hardcodați lucruri. De principiu, fiecare predicat ar trebui să aibă 1-2-3 reguli. De exemplu, nu faceți reguli separate pentru cele 4 direcții, ci folosiți neighbors
din util.pl
.
În prima parte trebuie să dezvoltați o reprezentare a stării jocului. Această reprezentare poate fi construită cum doriți. Ea trebuie să conțină informații despre:
Reprezentarea va fi construită și accesată numai prin intermediul predicatelor pe care le scrieți voi. Testerul va considera reprezentarea unei stări ca fiind opacă și nu va parsa reprezentarea în niciun fel. Puteți alege ce reprezentare doriți, folosind liste, tupluri, sau alți compuși, acestea putând fi și imbricate.
În testarea unui nivel, vor exista două faze:
empty_state
și set_*
pentru a construi reprezentarea unei stări S
(care poate să corespundă unui nivel din joc sau nu);S
rezultată, a predicatelor get_*
move
care să transforme o stare S
într-o altă stare, rezultată în urma realizării unei acțiuni.
Important: nu citiți direct reprezentarea nivelurilor din levels.pl
. Stările corespunzătoare fiecărui nivel vor fi construite pătrățică cu pătrățică de către checker prin apeluri ale predicatelor set_*
.
Aveți de implementat următoarele predicate în Prolog, documentate și în sursă:
empty_state(S)
– leagă S
la o reprezentare care nu conține încă nicio informație.set_*(S, Pos, SN)
– leagă SN
la o reprezentare care conține aceleași informații ca și S, la care se adaugă faptul că la poziția Pos
se află:set_tile
– o pătrățică normală;set_fragile
– o pătrățică pe care blocul nu poate sta în picioare;set_target
– pătrățica lipsă unde trebuie ca blocul să ajungă în picioare ca să se termine nivelul;set_block_initial
– o pătrățică normală, și în plus pe aceasta se află blocul inițial, poziționat vertical;set_blank
– [opțional] va fi apelat pentru toate coordonatele din interiorul dereptunghiului minim care conține toate pătrățelele (bounding box), unde nu se află pătrățele. În funcție de reprezentarea pe care o folosiți, acest predicat vă poate fi util. Dacă nu vă este util, este suficient să fie adevărat întotdeauna.get_b_pos(S, B)
– în funcție de poziția blocului:B
la poziția blocului.B
la o listă de poziții, conținând pozițiile fiecărei jumătăți a blocului.get_cell(S, Pos, Type)
– leagă Type
la tipul pătrățelei de la poziția Pos
(vezi documentația predicatului) sau întoarce false
dacă nu există o pătrățică la poziția respectivă.get_bounds(S, Xmin, Xmax, Ymin, Ymax)
– leagă ultimele 4 argumente la minimul/maximul coordonatelor X/Y pentru pătrățelele definite în starea S
.is_final(S)
– este adevărat dacă blocul este în poziție verticală pe poziția găurii (target).move(S, Move, SNext)
– calculează SNext
, care rezultă în urma acțiunii Move
în starea S
. Se disting 3 situații:neighbor
și neighbor2
din util.pl
). I.e. dacă ocupă o poziție P și mișcarea este r
, va ocupa poziția la dreapta de poziția P și următoarea poziție la dreapta de poziția P.Move
față de cele două poziții inițiale.Move
este d
, și blocul ocupă două poziții P1 și P2, unde P2 este pe direcția d
față de P1, blocul va ocupa, în picioare, poziția pe direcția d
față de P2.
Partea de switches rămâne pentru etapa 2; switch-urile vor fi adăugate cu predicatul set_switch
.
Coordonatele pozițiilor sunt perechi de forma (X, Y)
, cu X=0
pe cea mai din stânga coloană pe care există pătrățele și cu Y=0
pe cea mai de sus linie pe care există pătrățele. Predicatele set_*
vor fi apelate în ordine pentru toate pătrățelele, de la stânga la dreapta, și de sus în jos. În imaginea de mai sus, am notat cu buline albe pozițiile pentru care se va apela, la construcția reprezentării set_blank
.
De exemplu, pentru construcția reprezentării nivelului 3, testerul va apela următoarele predicate:
empty_state(S0), set_blank(S0, (0, 0), S1), set_blank(S1, (1, 0), S2), [...], set_blank(..., (5, 0), ...), set_tile(..., (6, 0), ...), set_tile(..., (7, 0), ...), ..., set_tile(..., (12, 0), ...), set_blank(..., (13, 0), ...), set_blank(..., (14, 0), ...), set_tile(..., (0, 1), ...), set_tile(..., (1, 1), ...), ..., set_tile(..., (0, 3), ...), set_block_initial(..., (1, 3), ...), ... set_target(..., (13, 3), ...), ..., set_tile(..., (14, 5), Level3).
După construcția unei reprezentări, putem utiliza predicate get_*
pe această stare. De exemplu, pe starea de mai sus, putem apela get_cell(Level3, (8, 2), X)
care ne așteptăm să lege X
la tile
.
După implementarea tuturor predicatelor set_*
și get_*
veți putea utiliza predicatul print_state/1
din util.pl
pentru a afișa o stare. De exemplu, pentru Stage 03, starea va fi afișată ca
+++++++ ++++ +++ ++ +++++++++ ++++ +B++ ++$+ ++++ ++++ +++
Nu uitați că aveți predicate utile pentru calculul pozițiilor în fișierul util.pl
.
În această etapă trebuie să includeți funcționalitatea switch-urilor în mecanica jocului și să rezolvați nivelurile de Bloxorz date.
Un switch în Bloxorz permite activarea și/sau dezactivarea unor pătrățele din tabla de joc (numite și pod – bridge).
Aveți de implementat:
set_switch(S, Pos, Switch, Func, Positions, SOut)
care leagă SOut
la o stare cu aceleași informații ca și S
, având în plus un switch în poziția Pos
. Switch
este tipul (oswitch
sau xswitch
); Func
este funcționalitatea (uponly
, dnonly
sau switch
); Positions
sunt pozițiile celulelor podului care este activat / dezactivat de switch. Odată implementat acest predicat, se așteaptă următorul comportament:get_cell
trebuie să lege ultimul argument la oswitch
sau xswitch
pentru poziția switch-urilor, pentru ca ele să fie afișate corectget_cell
trebuie să lege ultimul argument la tile
pentru toate pozițiile podului, iar dacă podul este dezactivat get_cell
trebuie să eșueze (să întoarcă false
) pentru toate pozițiile poduluimove
va fi ajustat în așa fel încât să modifice starea tablei de joc în mod corespunzător cu funcționalitatea switch-urilor.Pentru rezolvarea unui nivel, aveți de implementat predicatul:
solve(S, Moves)
care leagă Moves
la o listă de mutări necesare pentru a rezolva starea S
.solve
, ar trebui să fie suficient să lăsați pe Prolog să facă backtracking, alegând din mutările disponibile și avansând starea corespunzător.msort/2
din Prolog.
Încărcați în Prolog fișierul blox.pl
. Pentru testare folosiți predicatele check
sau vmcheck
. Pentru teste individuale folosiți vmtest(<nume_test>).
, e.g. vmtest(s0)
. Vedeți și observația de la începutul fișierului testing.pl
.
La trimiterea temei, este suficient să puneți în arhivă fișierul blox.pl
.
Testele se află în fișierul testing.pl
. Tipurile testelor sunt descrise succint la începutul fișierului. Pentru a verifica un test, puteți face o interogarea în consolă care conține scopurile plasate (de obicei) între ghilimele în primul argumental testului.
De exemplu, pentru a replica testul s0|b
puteți interoga la consolă:
empty_state(S), ttSetCell(S, 0, 0, +, _, _)
iar pentru testul s0|c
:
empty_state(S), ttSetCell(S, 0, 0, 'B', _, S1), get_cell(S1, (0, 0), X)
Pentru rezultate mai detaliate ale testării, puteți decomenta linia %detailed_mode_disabled :- !, fail.
din fișierul testing.pl
. Atenție! În acest caz, este posibil ca punctarea să fie diferită decât rezultatul de pe vmchecker, unde linia este comentată.
Pentru a face debug pe secvențe de mutări, puteți folosi predicatul ttSeqDbg
din testing.pl
, care afișează starea după fiecare mutare din secvență.