Marsjáró

Használható segédanyagok: Haskell könyvtárainak dokumentációja, (lokális!) Hoogle, a tárgy honlapja és a BE-AD rendszerbe feltöltött beadandók. Ha bármilyen kérdés, észrevétel felmerül, azt a felügyelőknek kell jelezni, nem a diáktársaknak!

A feladatok egymásra épülnek ezért érdemes ezeket a megadásuk sorrendjében megoldani, de legalább megérteni az aktuális feladatot megelőző feladatokat! A függvények definíciójában lehet, sőt javasolt is alkalmazni a korábban definiált függvényeket (függetlenül attól, hogy sikerült-e azokat megadni).

Tekintve, hogy a tesztesetek, bár odafigyelés mellett íródnak, nem fedik le minden esetben a függvény teljes működését, határozottan javasolt még külön próbálgatni a megoldásokat beadás előtt, vagy megkérdezni a felügyelőket!

Részpontszámokat csak az elégséges szint elérése után lehet kapni!

A feladat összefoglaló leírása

A feladatban egy autonóm marsjáró útvonalkereső algoritmusát fogjuk elkészíteni.

A Mars feltérképezett területét egy négyzetrácsnak (Map) képzeljük el, sorokra és oszlopokra felbontva. Egy-egy koordinátát (Coordinate) sor-oszlop párral ábrázolunk, ahol a sorszámozás nullától indul. A (0,0) koordináta a bal felső sarkot jelöli, az oszlopszámok jobbra nőnek, a sorszámok lefelé.

A Mars felszínét szektorokra (Sector) osztjuk. A felszín egy szektorát egy rendezett pár alkotja, mely megadja a szektor pozícióját és hogy járható-e (Passable) vagy sem (True, ha járható, egyébként False).

A feltérképezett terület oldalai azonos hosszúságúak. A feltérképezett területet az oldalhosszával és a szektorok listájával adjuk meg.

type Coordinate = (Int, Int)
type Passable = Bool
type Sector = (Coordinate, Passable)
type Size = Int
type Map = (Size, [Sector])

Üres térkép (1 pont)

Hozzunk létre egy n × n méretű üres térképet, melyen nincs egyetlen egy akadály sem, minden szektor járható!

Ha a méret nem pozitív, adjunk hibaüzenetet az error függvénnyel (lásd példák)! A paramétert a show függvénnyel lehet szöveggé alakítani.

blankMap :: Size -> Map

Test>
(2, [((0, 0), True), ((0, 1), True), ((1, 0), True), ((1, 1), True)]) :: Map
Test>
(3, [((0, 0), True), ((0, 1), True), ((0, 2), True), ((1, 0), True), ((1, 1), True), ((1, 2), True), ((2, 0), True), ((2, 1), True), ((2, 2), True)]) :: Map
Test>
(5, [((0, 0), True), ((0, 1), True), ((0, 2), True), ((0, 3), True), ((0, 4), True), ((1, 0), True), ((1, 1), True), ((1, 2), True), ((1, 3), True), ((1, 4), True), ((2, 0), True), ((2, 1), True), ((2, 2), True), ((2, 3), True), ((2, 4), True), ((3, 0), True), ((3, 1), True), ((3, 2), True), ((3, 3), True), ((3, 4), True), ((4, 0), True), ((4, 1), True), ((4, 2), True), ((4, 3), True), ((4, 4), True)]) :: Map
Test>
⊥₁ :: Map
⊥₁: blankMap: invalid size 0
CallStack (from HasCallStack):
  error, called at ./Pathfinder.lhs:134:21 in main:Pathfinder
Test>
⊥₁ :: Map
⊥₁: blankMap: invalid size -2
CallStack (from HasCallStack):
  error, called at ./Pathfinder.lhs:134:21 in main:Pathfinder
Test>
(0,0)(8,0)(8,8)

Kulcshoz tartozó érték cseréje (2 pont)

Cseréljünk ki egy megadott kulcshoz tartozó értéket egy kulcs-érték párokból álló listában! Mindig a kulcs első előfordulásához rendelt értéket cseréljük, ha a kulcs többször is előfordulna a listában.

replaceElem :: Eq a => [(a, b)] -> a -> b -> [(a,b)]

Test>
[('a', 3), ('b', 10), ('c', 4), ('a', 6)] :: [(Char, Integer)]
Test>
[('a', 3), ('b', 2), ('c', 0), ('a', 6)] :: [(Char, Integer)]
Test>
[('a', 2), ('b', 2), ('c', 4), ('a', 6)] :: [(Char, Integer)]
Test>
[('a', 3), ('b', 2), ('c', 4), ('a', 6)] :: [(Char, Integer)]

Térkép frissítése (3 pont)

Adjuk meg azt a műveletet, mellyel az egyes szektorokat járhatatlannak minősíthetjük a radar által begyűjtött információk alapján!

Segítség: a térkép frissítéséhez használhatjuk az előbbi replaceElem függvényt.

putObstacles :: Map -> [Coordinate] -> Map

Test>
(2, [((0, 0), True), ((0, 1), False), ((1, 0), True), ((1, 1), False)]) :: Map
Test>
(4, [((0, 0), True), ((0, 1), True), ((0, 2), False), ((0, 3), True), ((1, 0), True), ((1, 1), True), ((1, 2), True), ((1, 3), True), ((2, 0), False), ((2, 1), True), ((2, 2), True), ((2, 3), True), ((3, 0), True), ((3, 1), True), ((3, 2), True), ((3, 3), True)]) :: Map
Test>
(0,0)(3,0)(3,3)

Vezessük be az alábbi három teszt térképet!

map1 :: Map
map1 = blankMap 4
Test>
(0,0)(3,0)(3,3)
map2 :: Map
map2 = putObstacles (blankMap 5) [(1,3),(1,1),(3,1),(3,2),(3,3)]
Test>
(0,0)(4,0)(4,4)
map3 :: Map
map3 = putObstacles (blankMap 4) [(2,1),(2,3),(2,2)]
Test>
(0,0)(3,0)(3,3)

Továbbhaladási irányok (1 pont)

Soroljuk fel egy koordináta négy szomszédját északról kezdve, óramutató járásával ellentétes irányban!

Megjegyzés: itt még nem foglalkozunk a koordináták helyességével, azaz szerepelhetnek negatív sorok vagy oszlopok.

directions :: Coordinate -> [Coordinate]

Test>
[(2, 3), (3, 2), (4, 3), (3, 4)] :: [Coordinate]
Test>
[(3, 5), (4, 4), (5, 5), (4, 6)] :: [Coordinate]
Test>
[(0, 1), (1, 0), (2, 1), (1, 2)] :: [Coordinate]
Test>
[(-1, 0), (0, -1), (1, 0), (0, 1)] :: [Coordinate]

Feltérképezett-e a koordináta? (1 pont)

Döntsük el, hogy egy adott koordináta feltérképezett-e vagy sem, azaz a térképen van-e!

isExplored :: Map -> Coordinate -> Bool

Test>
True :: Bool
Test>
True :: Bool
Test>
True :: Bool
Test>
False :: Bool

Járható-e? (2 pont)

Döntsük el, hogy a térkép egy adott koordinátáján áthaladhatunk-e, azaz nincs ott akadály! Feltehetjük, hogy a vizsgált koordináta megtalálható a térképen.

isPassable :: Map -> Coordinate -> Bool

Test>
False :: Bool
Test>
True :: Bool
Test>
[True, True, True, True, True, True, False, True, False, True, True, True, True, True, True, True, False, False, False, True, True, True, True, True, True] :: [Bool]

Legális pozíció (1 pont)

Döntsük el, hogy egy pozíció legálisnak minősül-e! Ezt akkor állíthatjuk, ha léphet-e oda a marsjáró, azaz a pozíció rajta van a térképen és nincs ott akadály.

legalCoordinate :: Map -> Coordinate -> Bool

Test>
True :: Bool
Test>
False :: Bool
Test>
False :: Bool
Test>
False :: Bool

Szomszédos koordináták (1 pont)

Gyűjtsük össze egy listába azokat a koordinátákat, ahová a marsjáró az adott pozícióról léphet! Használjuk a korábban definiált műveleteket!

neighbourFreeSectors :: Map -> Coordinate -> [Coordinate]

Test>
[(1, 1), (2, 0), (3, 1), (2, 2)] :: [Coordinate]
Test>
[(1, 0), (0, 1)] :: [Coordinate]
Test>
[(4, 4)] :: [Coordinate]
Test>
(0,0)(4,0)(4,4)

Útvonalak a térképen

A továbbiakban azzal foglalkozunk, hogy útvonalat keressünk egy adott koordinátából egy másikba. Az útvonalakra bevezetjük a Path típust, mely jelöli, hogy a marsjáró mely szektorokon menne végig.

type Path = [Coordinate]

Az útvonalakban a koordinátákat fordított sorrendben tároljuk. Tehát a kiindulási pont a lista utolsó eleme, és ha végig szeretnénk haladni az útvonalon, akkor a listát a végéről előre haladva kell bejárni.

Az ábrákon egy ‘X’ jelöli azt a koordinátát ahova eljutottunk az útvonalat követve.

Landolás (1 pont)

Vezessük be azt a műveletet, amely tervezett koordinátára landoltatja a marsjárót! Ha a koordináta nem járható, akkor adjunk hibaüzenetet az error függvénnyel (lásd példák)!

land :: Map -> Coordinate -> Path

Test>
[(0, 0)] :: Path
Test>
[(2, 4)] :: Path
Test>
⊥₁ :: Path
⊥₁: land: landing failed
CallStack (from HasCallStack):
  error, called at ./Pathfinder.lhs:300:21 in main:Pathfinder

Lehetséges lépések (2 pont)

Egészítsünk ki egy útvonalat egy új lépéssel! Próbáljuk ki az összes lehetséges irányt (neighbourFreeSectors)! Ne lépjünk újra olyan koordinátára, ahol korábban már jártunk, azaz szerepel az útvonalban!

Ha az útvonal nem folytatható egyik irányban sem, akkor adjunk vissza üres listát!

step :: Map -> Path -> [Path]

Test>
[[(0, 0), (1, 0), (2, 0)]] :: [Path]
Test>
[[(1, 0), (0, 0)], [(0, 1), (0, 0)]] :: [Path]
Test>
[[(1, 2), (2, 2), (2, 1), (2, 0)], [(2, 3), (2, 2), (2, 1), (2, 0)]] :: [Path]
Test>
[[(2, 2), (2, 3), (1, 3), (1, 2), (0, 2), (0, 3), (0, 4), (1, 4), (2, 4), (3, 4), (3, 2)]] :: [Path]
Test>
[] :: [Path]
Test>
(0,0)(4,0)(4,4)x
Test>
(0,0)(3,0)(3,3)x

Célba értünk-e (1 pont)

Döntsük el, hogy az útvonal követésével elérnénk-e a célunkat, azaz az útvonal első pozíciója megegyezik-e a céllal!

reached :: Path -> Coordinate -> Bool

Test>
True :: Bool
Test>
False :: Bool

Szélességi bejárás (3 pont)

Keressük meg a térképen az összes lehetséges útvonalat, mely elvezet a célba! A breadthFirst első paramétere a térkép, mely jelöli a járható szektorokat. A második paramétere a cél, ahova legvégül szeretnénk eljutni. A harmadik paramétere útvonalak listája, melyek a landolási koordinátából indulnak. Az egyes útvonalak legelső koordinátája az a koordináta, ahol éppen áll a marsjáró. Az eredmény azok az útvonalak lesznek, melyekben az első koordináta a céllal egyezik meg.

A breadthFirst működési elve a következő:

  1. Ha az útvonalak listája üres, akkor elakadtunk, nincs útvonal a célba. Adjunk vissza egy üres listát!

  2. Ha az útvonalak listája nem üres, akkor két lehetőség áll előttünk, attól függően, hogy elértük-e a célunkat:

    1. Ha az első útvonalon már elértünk a célba (reached), akkor tegyük be ezt az útvonalat a végeredmény listába, és keressünk további célba vezető útvonalakat a maradék útvonalak közül a breadthFirst-tel.

    2. Különben az első útvonalat ki kell egészíteni egy lépéssel (step). Ha több irányban indulhatunk el, akkor mindegyik irányt kipróbáljuk. Fűzzük be a legelső útvonal összes lehetséges folytatását az útvonalak listájának végére, és keressünk célba vezető útvonalakat a breadthFirst-tel!

breadthFirst  :: Map -> Coordinate -> [Path] -> [Path]

Test>
[[(0, 1), (0, 2), (0, 3)], [(0, 1), (1, 1), (1, 2), (0, 2), (0, 3)], [(0, 1), (0, 0), (1, 0), (1, 1), (1, 2), (0, 2), (0, 3)], [(0, 1), (1, 1), (2, 1), (2, 2), (1, 2), (0, 2), (0, 3)], [(0, 1), (0, 0), (1, 0), (2, 0), (2, 1), (1, 1), (1, 2), (0, 2), (0, 3)], [(0, 1), (0, 0), (1, 0), (1, 1), (2, 1), (2, 2), (1, 2), (0, 2), (0, 3)], [(0, 1), (0, 0), (1, 0), (2, 0), (2, 1), (2, 2), (1, 2), (0, 2), (0, 3)], [(0, 1), (1, 1), (1, 0), (2, 0), (2, 1), (2, 2), (1, 2), (0, 2), (0, 3)]] :: [Path]

Legrövidebb út (1 pont)

Keressük meg az egyik legrövidebb utat a célhoz egy adott kiindulási pontból! A shortestPath második paramétere a cél, a harmadik paramétere a landolási hely.

Megjegyzés: használjuk ki, hogy a szélességi bejárásnál az első útvonal az egyik legrövidebb lesz!

shortestPath :: Map -> Coordinate -> Coordinate -> Path

Test>
[(0, 1), (0, 2), (0, 3)] :: Path
Test>
[(4, 3), (4, 4), (3, 4), (2, 4), (2, 3)] :: Path
Test>
⊥₁ :: Path
⊥₁: land: landing failed
CallStack (from HasCallStack):
  error, called at ./Pathfinder.lhs:300:21 in main:Pathfinder
Test>
(0,0)(4,0)(4,4)
Test>
(0,0)(4,0)(4,4)x

Pontozás