Yhteenveto: osoittimet
Tietyn muuttujan osoittimen saa osoiteoperaattorilla (&
). Tämä
on siis muistiosoite jota voi käyttää vastaavan tyyppisessä
osoitinmuuttujassa tai muussa vastaavassa paikassa. Esimerkiksi:
1 2 3 4 | int var; int *pointer; pointer = &var; int var2 = *pointer; |
Osoitinmuuttujan tyyppi riippuu aina osoitettavasta objektista, vaikka osoitinmuuttujan sisältö on sinällään aina muistiosoite, kuten yllä olevassa esimerkissä nähdään.
Osoittimeen voi vittata viittausoperaattorilla
(*
). Viittausoperaattori hakee siihen yhdistetyn osoittimen
osoittaman arvon lausekkeessa käytettäväksi tai esimerkiksi muuttujaan
sijoitettavaksi. Yllä olevassa esimerkissä nähdään näiden kahden
operaattorin välinen suhde, ja kuinka ne tavallaan ovat toistensa
vastakohtia: kun osoiteoperaattorilla on haettu osoitin johonkin
muuttujaan, viittausoperaattorilla voidaan sen jälkeen palata takaisin
kyseiseen arvoon. Osoiteoperaattori lisää yhden tähden kohteensa
tietotyyppiin, kun taas viittausoperaattori poistaa yhden tähden
kohteensa tietotyypistä.
Osoittimia voidaan ketjuttaa: on mahdollista että meillä on osoittimen
osoitin kokonaislukuun. Tällainen tietotyyppi olisi int**
. Samalla
mekanismilla voidaan rakentaa myös kaksiulotteisia taulukoita.
Tietotyyppejä määritellessä int *var
, int * var
ja int* var
toimivat samoin, eli välimerkkejä voi olla tähden molemmin
puolin. Tietenkin on hyvä noudattaa ohjelmassa kuitenkin yhdenmukaista
tyyliä tämän suhteen.
Pointteriluntti on kätevä yhteenveto osoittimien perusasioista.
Osoittimista oli enemmän asiaa modulissa 2.
Yhteenveto: taulukot
Taulukko on jono tietyn tyyppisiä objekteja jotka sijaitsevat peräkkäin muistissa. Taulukko voi olla staattisesti määritelty tietyn kokoiseksi:
int array[5];
Taulukkoon voi myös viitata osoitinmuuttujan avulla:
int *array2;
Jälkimmäisessä tapauksessa tila taulukolle voidaan varata dynaamisesti, jolloin taulukon kokokin voidaan määrätä dynaamisesti. Osoittimen voi toki myös laittaa osoittamaan staattisesti varattuun taulukkoon:
array2 = array;
Vaikka nämä kaksi muuttujaa ovat eri tyyppisiä, osoittimeksi sijoitus sujuu taulukosta mutkattomasti.
Taulukon yksittäiseen jäseneen viitataan indeksointioperaattorilla:
esimerkiksi array[2] = 10;
asettaa taulukon kolmannen jäsenen
sisällön kokonaisluvuksi 10. Taulukon indeksointi alkaa aina
0:sta. Indeksioperaattori toimii samalla tavalla myös osoittimen avulla
määriteltyyn taulukkoon, ja itse asiassa indeksi on oikeastaan vain
viittausoperaatio tiettyyn kohtaan taulukkoa. Edellä olevan voisi
sanoa myös: *(array + 2) = 10;
Viittausoperaattorin tapaan indeksointioperaattori siis tipauttaa taulukon tyypistä yhden tähden pois:
1 2 3 | int array[5]; int *array2 = array; int a = array2[1]; |
Osoiteoperaattoria voi siis käyttää yhdessä indeksoinnin kanssa,
jolloin tuloksena syntyy jälleen osoitin, joka ei tosin enää
välttämättä osoita taulukon alkuun, vaan viitattuun alkioon siinä:
int *pa = &array[1];
saa osoittimen pa viittaamaan taulukon
toiseen alkioon.
Lisää tietoa taulukoista modulissa 2.
Yhteenveto: merkkijonot
Merkkijonot ovat yleinen erikoistapaus taulukoista, jotka merkkijonojen tapauksessa muodostuvat char - tyyppisistä merkeistä. Merkkijonon loppu tunnistetaan erityisestä 0-merkistä. C-kielessä on määritelty erityinen syntaksi merkkijonojen esittämiseen seuraavaan tapaan:
1 | char str[10] = "a string"; |
Tällaisella syntaksilla esitetyt vakiomerkkijonot sisältävät aina automaattisesti nollamerkin merkkijonon perässä. Merkkijonolle täytyy varata tilaa samoin kuin muillekin taulukoille, ja kun merkkijonolle varataan tilaa, tulee muistaa jättää tilaa nollamerkillekin, joka vie yhten tavun muistista muiden merkkien tapaan.
Yllä olevassa esimerkissä merkkijono kopioitiin taulukkoon, joka
varattiin pinosta str-muuttujan esittelyn yhteydessä. Tällaista
merkkijonoa voi muokata sijoituksen jälkeen. Jos sen sijaan
viitattaisiin osoittimen kautta merkkijonoon: char *str = "a
string";
, merkkijonon muokkaaminen ei ole mahdollista, ja aiheuttaa
ohjelman keskeytymisen ajon aikana, koska vakiomerkkijonot sijaitsevat
kirjoitussuojatulla alueella muistissa. Tästä syystä ohjelmassa
olisikin hyvä käyttää const-määrettä: const
char *str = "a string";
. Tällöin virheellinen käyttö huomataan jo
käännösvaiheessa.
Merkkijonojen käsittelyn helpottamiseksi C:n standardikirjastossa on muutamia hyödyllisiä funktioita, jotka saa käyttöönsä sisällyttämällä ohjelmaan strings.h - otsakkeen. Esimerkiksi strlen on melko yleinen funktio, joka palauttaa annetun merkkijonon pituuden (poislukien lopussa olevan 0-merkin). strlen - funktiota ei tule sekoittaa sizeof - määreeseen, joka kertoo annetun tietotyypin vaatiman tilan muistista.
Modulissa 2 oli lisää asiaa merkkijonoista ja niiden käsittelyyn laadituista funktioista.
Task 01_polisher: Koodinsiistijä (3 pts)
Tavoite: Palautellaan mieliin merkkijonojen käsittelyä
Toteuta koodinsiistijä C-kielisille ohjelmille, joka poistaa kommentit ja korjaa rivien sisennykset ohjelmalohkojen mukaisesti.
a) Lue tiedosto
Toteuta funktio char *read_file(const char *filename)
, joka lukee
annetun tiedoston dynaamisesti varattuun muistiin. Funktio palauttaa
osoittimen muistilohkoon joka sisältää luetun tiedoston, tai NULL jos
tiedoston avaamisessa sattuu virhe.
b) Poista kommentit
Toteuta funktio char *remove_comments(char *input)
, joka poistaa
C-kommentit ohjelmasta, joka on tallennettu osoitteeseen
input. Huomaa että kyseessä on dynaamisesti varattu puskuri, eli se
joka (a)-kohdassa varattiin. Funktio palautaa osoittimen kommenteista
siivottuun ohjelmaan. Voit joko varata uuden muistilohkon muokattua
ohjelmaa varten, tai muokata ohjelmaa suoraan input - parametrissa
saamassasi muistissa.
Muistutuksena vielä C:n kommenttisäännöt, jotka sinun pitää siivota:
-
Komenttilohkot, jotka alkavat merkeillä
/*
ja päättyvät merkkeihin*/
. Nämä lohkot voivat olla usean rivin pituisia. Sinun tulee poistaa vain nämä lohkot: jos esimerkiksi lohkon loppua seuraa rivinvaihto, se jää edelleen ohjelmaan. -
Rivikommentit, jotka alkavat merkeillä
//
ja päättyvät rivinvaihtoon.
Funktiota kutsuva ohjelma on vastuussa vain siitä osoittimesta, jonka funktio palauttaa. Jos varaat uutta muistia funktion sisällä, sinun tulee huolehtia tarpeettoman muistin vapauttamisesta.
c) Sisennä koodi
(Huom: Tämä tehtäväkohta saattaa olla vaikeampi kuin kaksi edellistä, sekä jotkut seuraavista tehtävistä. Jos tuntuu vaikealta, palauta kaksi edellistä kohtaa TMC:hen, jotta saat niistä pisteet ja palaa myöhemmin tähän tehtävään jos aikaa jää)
Toteuta funktio char *indent(char *input, const char *pad)
, joka
lisää tarvittavat sisennyket input - puskurin sisältämään koodiin,
ja palauttaa osoittimen muokattuun ohjelmaan
paluuarvonaan. Sisennystyyli annetaan merkkijonona parametrissa pad:
parametri voi sisältää esimerkiksi merkkijonon, jossa on neljä
välilyöntiä, jolla ilmaistaan että sisennyksen tapahtuvat neljän
askelilla. Yhtälailla parametri voi sisältää mitä muuta tahansa:
pad - parametrin sisältöä toistetaan rivin alkuun yhtä monta kertaa
kuin sisennystasoja kyseisellä rivillä on. Mikäli rivillä on olemassa
oleva, välimerkkeistä tai tabeja koostuva sisennys, se tulee unohtaa,
ja korjata sisennys pad - parametrissa ilmaistun kaltaiseksi.
Voit olettaa että uusi ohjelmalohko alkaa aina aaltosululla {
ja
loppuu aina aaltosulkuun }
, ja muita sisennyssääntöjä ei
ole. Sisennystaso kasvaa vasta aaltosulkumerkin jälkeen ja loppuu
ennen lohkon lopettavaa aaltosulkua.
Kuten aiemmassakin kohdassa, mikäli päädyt varaamaan uuden puskurin muokattavaa merkkijonoa varten, funktion tulee vapauttaa tarpeeton muisti. Kutsuva funktio pitää huolen vain siitä muistilohkosta, joka palautetaan paluuarvossa. Kannattaa huomioida myös että sisennetty ohjelma saattaa tarvita enemmän muistia kuin alkuperäinen.
Alla on esimerkki, jossa sisennysfunktio on ajettu tehtäväpohjassa olevalle 'src/testfile.c' - tiedostolle. Esimerkissä on käytetty neljää välilyöntiä pad - parametrissa. Kuten aina, kannattaa ensi testata ohjelmaasi käyttäen src/main.c:tä, ennenkuin lähetät sen TMC:n käsiteltäväksi.
1 2 3 4 5 6 7 8 9 10 11 12 13 | // This is a test file int main(void) { /* comment block */ printf("juu\n"); while (1) { // Another comment while (2) { printf("jaa\n"); } } } |
Yhteenveto: muistinhallinta
(Moduli 3)
Usein on tarpeellista varata ohjelman tarvitsema muisti dynaamisesti, esimerkiksi kun tarvittavan muistin määrä ei ole tiedossa ohjelmaa kirjoittaessa, esimerkiksi koska se riippuu käyttäjän syötteestä tai jostain muusta ulkoisesta tekijästä. Muistin varaus tehdään malloc - kutsulla, jonka parametrina kerrotaan kuinka monta tavua tarvitaan. Paluuarvonaan onnistunut malloc - kutsu palauttaa osoittimeen varattuun muistilohkoon, kuten seuraavassa nähdään.
1 2 | char *buffer = malloc(1000); if (buffer == NULL) { /* allocation failed */ } |
Käytännössä virtuaalimuistia käyttävissä järjestelmissä muistin varaus epäonnistuu hyvin harvoin, koska käyttöjärjestelmä pystyy sivuttamaan osan muistia tarvittaessa levylle. Pienissä sulautetuissa järjestelmissä tilanne voi olla erilainen.
realloc - funktiolla voi muuttaa aiemmin varatun muistialueen kokoa.
Jotta muistin tarve voidaan laskea, täytyy olla tieto siitä kuinka paljon käytetyt tietorakenteet ja tietotyypit tarvitsevat tilaa. sizeof - operaattori kertoo annetun tietotyypin tilatarpeen kyseisessä arkkitehtuurissa. Sitä tulee käyttää, vaikka kaverilta olisikin kuullut kyseisen tietotyypin koon, koska monet tietotyypit C:ssä ovat sellaisia että niiden koko saattaa vaihdella eri arkkitehtuurien välillä. Kun varataan tilaa taulukolle, tulee kyseisen tietotyypin vaatima tila luonnollisesti kertoa taulukkoon mahtuvien alkioiden lukumäärällä.
1 | float *array = malloc(sizeof(float) * 10); // array of 10 float numbers |
Dynaamisesti varattu muisti täytyy vapauttaa sitten kun sitä ei enää tarvita, jottei kuluteta järjestelmän resursseja turhaan. Valgrind on hyödyllinen työkalu, joka kertoo mikäli ohjelma vuotaa muistia, eli ei vapauta kaikkea varaamaansa muistia, sekä auttaa jäljittämään monia muita muistinhallintaan liittyviä ongelmia.
Yhteenveto: tietorakenteet
(Moduli 3)
Tietorakenteet ovat tietotyyppejä jotka koostuvat useista nimetyistä kentistä, joilla kullakin on jokin määritelty tietotyyppi. Nämä tietotyypit voi olla jotain C:n perustietotyypeistä, taulukoita, tietorakenteita, tai vaikkapa funktio-osoittimia. Tässä yksi esimerkki:
1 2 3 4 5 | struct example { int value; // single integer char string[20]; // character array of 20 characters struct other *ptr; // pointer to another structure }; |
Kun tietorakennetta käytetään sellaisenaan, sen jäseniin voidaan
viitata jäsenoperaattorilla (.
). Toisaalta, jos lausekkeessa onkin
osoitin tietorakenteeseen, silloin kenttiin viitataan nuolella
(->
). Näiden kahden välistä eroa kannattaa tarkastella tarkkaan, ja
kokeilla vaikka erilaisia esimerkkejä, jolloin hahmottuu kumpaa tulee
missäkin tilanteessa käyttää. Edellämainittujen vaihtoehtojen
valinta riippuu siis siitä, onko operaattorin vasemmalla puolella
oleva tietotyyppi osoitin vain ei. Operaattorin oikealla puolella
olevien kenttien tyyppi ei vaikuta siihen kumpaa käytetään. Tämä
esimerkki pyrkii havainnollistamaan asiaa:
1 2 3 4 5 | struct example ex1; ex1.ptr = NULL; struct example *ex2 = &ex1; // make ex2 point to ex1 ex2->ptr = NULL; |
Task 02_parser: Komentoriviparseri (2 pts)
Tavoite: Perehdytään komentoriviargumentteihin, ja palautellaan mieliin linkitetyn listan toimintaa.
Komentoriviargumentteja käytetään ohjaamaan komentoriviltä käynnistettävän ohjelman toimintaa. Lyhyt yhteenveto komentoriviargumenteista ja komentorivioptioiden tyypillisestä toiminnasta annettiin modulissa 4. Tässä tehtävässä toteutetaan funktiot annettujen komentorivioptioiden käsittelemiseksi.
Ohjelmasi tulee komentorivioptiot normaalin mallisesta merkkijonotaulukosta (argv), ja sijoittaa havaitut komentorivioptiot linkitettyyn listaan, joka koostuu option - tietorakenteista. argv - taulukko sisältää siis ohjelmalle annetut komentoriviargumentit, ja taulukossa on argc alkiota.
Komentorivioption tunnistaa siitä, että se alkaa viivamerkillä
(-
). Viivamerkkiä seuraa yksi kirjainmerkki, jolla option
tunnistaa. Tällainen optio lisätään linkitettyyn listaan uudeksi
alkioksi, ja vastaava kirjainmerkki sijoitetaan options -
tietorakenteen optchar - kenttään. Mikäli optiota seuraa merkkijono,
joka ei ala viivalla, kyseessä on parametri kyseiselle optiolle, joka
tulee tallentaa samaan tietorakenteeseen. Tehtäväpohjassa tuleva
tietorakenne ei vielä sisällä kenttää tälle, joten sinun tulee
määritellä itse tarvittava lisäkenttä (tai kentät)
tietorakenteeseen. Jos optiota seuraa suoraan toinen optio,
kyseisellä optiolla ei ole määriteltyä parametria, ja optio tulkitaan
vain annetuksi (se voi olla esimerkiksi on/off - vipu, jolla säädetään
ohjelman toimintaa jotenkin).
Mikäli komentorivillä on merkkijono, joka ei ole edellä annetun option parametri, se tulee vain sivuuttaa.
Linkitettyjen listojen toimintaa voi kerrata modulista 3.
a) Parsi optiot
Toteuta funktio get_options, joka käsittelee komentoriviä vastaavan merkkijonotaulukon ja rakentaa sen perusteella edellä kuvatun kaltaisen linkitetyn listan, varaten tarvittavan muistin dynaamisesti. Jokaista optiota tulee siis vastata yksi alkio linkitetyssä listassa.
Lisäksi sinun tulee toteuttaa funktio free_options, joka vapauttaa edellä mainitun funktion varaaman muistin.
b) Tiedustele optioita
Toteuta funktio is_option joka palauttaa nollasta poikkeavan arvon, mikäli paramterissa optc annettu optio löytyy linkitetystä listasta opt. Mikäli optiota ei löydy, palautetaan 0.
Toteuta lisäksi funktio get_optarg, joka palauttaa optiota optc vastaavan optioparametrin. Mikäli optioparametria ei oltu määritelty, tai mikäli optio ei ylipäätään löydy linkitetystä listasta, palautetaan NULL.
src/main.c esittää kuinka funktioita käytetään. Voit testata ohjelmaa komentoriviltä käynnistämällä ja antaa erilaisia optioita. Käännetty ohjelma löytyy src-hakemistosta nimellä "main". Mikäli et halua käyttää komentoriviä, voit myös muokata main-funktiota esimerkiksi kysymään "komentoriviargumentteja" scanf-funktiota käyttäen.
Yhteenveto: I/O - virrat
Ohjelman syöte ja tuloste tapahtuu I/O - virtojen kautta. Uuden I/O-virran voi avata fopen - kutsulla, ja se pitää sulkea fclose - kutsulla. Usein I/O - virta kohdistuu levyllä olevaan tiedostoon, mutta se voi osoittaa myös muihin laitteisiin, kuten käyttäjän komentorivinäkymään. Kaikissa ohjelmissa on oletusarvoisesti auki kolme oletusvirtaa: standarditulostevirta (stdout), standardisyötevirta (stdin), ja standardivirhevirta (stderr).
printf - funktiolla voi tulostaa muotoiltua tulostetta standarditulostevirtaan. Funktiolle voi antaa parametreja, jotka tulostuvat muotoilumääreiden perusteella. fprintf on funktion yleisempi muoto, jolla vastaavat tulosteet voi ohjata mihin tahansa virtaan, esimerkiksi tiedostoon. Lisää tietoa esimerkiksi muotoilumääreistä löytyi modulista 1, ja virroista yleisesti modulista 5.
Vastaavasti scanf - funktiolla luetaan käyttäjän syötettä komentoriviltä, ja fscanf - funktiolla yleisemmin mistä tahansa virrasta. Näissä funktioissa parametrit annetaan osoittimien kautta, jotta scanf - funktio voi muokata niiden arvoa.
I/O - virrat ovat puskuroituja. Niihin kirjoitettu tieto ei välttämättä ilmesty heti näkyviin, eikä käyttäjän syöttämä tieto tule välttämättä heti ohjelmalle. Oletusarvoisesti noudatetaan rivipuskurointia, eli tieto toimitetaan, kun virtassa vaihdetaan riviä.
Task 06_election: Vaalijärjestelmä (2 pts)
Tavoite: Palautellaan mieliin tiedoston käsittelyä, dynaamisia taulukoita ja tietorakenteita, sekä järjestelyalgoritmien käyttöä.
Toteuta vaalijärjestelmä, joka laskee äänet tiedostosta jossa kukin ääni on listattu omalla rivillään. Järjestelmään toteutetaan kaksi funktiota seuraavasti:
(a) funktio read_votes joka lukee äänet annetusta tekstitiedostosta. Kukin ääni on annettu tiedostossa omalla rivillään, ja siinä on enintää 39 merkkiä. Tiedostossa src/votes.txt on lyhyt esimerkki. Tiedoston perusteella tulee rakentaa dynaaminen taulukko, jonka kukin elementti on votes - tietorakenne. Kukin erilainen tiedoston sisältämä nimi tulee sisältyä vain kerran taulukkoon, ja tietorakenteen tulee ilmaista kuinka monta kertaa kyseinen nimi esiintyi tiedostossa. Toisin sanoen taulukossa on niin monta alkiota, kuin tiedostossa on erilaisia nimiä. Taulukon loppu ilmaistaan alkiolla, jonka nimi on tyhjä merkkijono. Annettu esimerkkitiedosto tuottaa siten esimerkiksi taulukon jossa on neljä alkiota, sekä loppualkio. Osoitin tuotettuun taulukkoon palautetaan funktion paluuarvona.
Kannattaa lisäksi huomioida, että taulukon sisältämien nimien ei tule sisältää rivinvaihtomerkkiä, jollainen tiedostosta löytyy jokaisen nimen perässä.
(b) funktio results, joka tulostaa äänestyksen tuloksen edellisen tehtäväkohdan tuottaman taulukon perusteella seuraavassa formaatissa:
name: votes
Lisäksi tulokset tulee listata äänimäärän mukaisessa järjestyksessä siten että eniten ääniä saanut nimi tulostetaan ensin. Tapauksissa joissa äänimäärä on sama, nimet tulostetaan aakkosjärjestyksessä. Kannattaa muistaa, että C:n stadndardikirjastossa on hyödyllisiä apufunktioita järjestämisen toteuttamiseksi (toki saa sen toteuttaa itsekin).
Esimerkiksi kun ajetaan main.c - funktion sisältämä ohjelma tiedostolle src/votes.txt, seuraavaa pitäisi tulostua:
Trump: 4 Clinton: 2 Sanders: 2 Cruz: 1
(Esimerkki on täysin fiktiivinen.)
Kannattaa luoda omia testitiedostoja toteuttamiesi funktioiden testaamiseksi. Toteuta funktiot tiedostoon election.c sen pohjalta, mitä määrittelyt tiedostossa election.h sisältävät.
Yhteenveto: binäärioperaattorit
Binäärioperaattoreista oli enemmän tarinaa modulissa 4, mutta tässä pikainen yhteenveto.
Kenties yleisimmät binäärioperaattorit ovat binäärinen JA (&
),
binäärinen TAI (|
), sekä bittisiirto-operaattorit molempiin suuntiin
(<<
ja >>
). Binäärisiä operaattoreita ei tule sekoittaa loogisiin
operaattoreihin (&&
ja ||
), jotka palauttavat erilaisen
lopputuloksen (arvon 1 tai 0).
Binäärinen JA-operaattori soveltuu esimerkiksi tiettyjen bittien tilan testaamiseen isommasta kokonaisluvusta seuraavaan tyyliin:
1 | if (val & 0x10) { /* fifth bit is set */ } |
Tällä tavalla siis testataan onko viides bitti muuttujassa val asetettuna: mikäli se ei ole asetettuna, tuloksena on 0 (eli epätosi), muussa tapauksessa 0x10 (eli tosi).
Binääristä TAI-operaattoria käytetään usein kahden eri binäärisen arvon yhdistämiseen: lopputuloksessa bitti on päällä silloin kun jommassa kummassa operandissa vastaava bitti on päällä, esimerkiksi seuraavasti:
1 | int combined = 0xf1 | 0x03; // result: 0xf3 |
Task 03_mac: MAC-otsake (2 pts)
Tavoite: Kerrataan binäärioperaatioita.
Matalan tason protokollat pyrkivät tyypillisesti hyödyntämään tarvittavan tilan tehokkaasti, jotta protokolla aiheuttaisi mahdollisimman vähän turhaa tietoliikennettä. Tällä kertaa keskitytään 802.11 MAC-otsakkeeseen (eli WiFi-protokollaan) ja erityisesti otsakkeen kahteen ensimmäiseen tavuun, eli "Frame Control" - osioon.
802.11 - otsakeesta löytyy tietoa esimerkiksi täältä, joskin vastaava tieto löytyy myös muualtakin webistä. Frame Control - kentistä löytyy tarkempi kaavia kuvasta numerolla "3.3" kyseisellä sivulla (skrollaa hieman alaspäin). Tarvitset kaaviokuvaa parsiaksesi tehtävän vaatimat Frame Control - kentät.
a) Parsi otsake
Toteuta seuraavat funktiot, jotka kukin lukevat ja palauttavat yhden kentän MAC-otsakkeesta. Parsiaksesi kentät sinun tulee poimia vastaavat bitit MAC-otsakkeesta, esimerkiksi bittisiirtoja ja muita binäärioperaatioita käyttäen ja palauttaa lukuarvot seuraavassa kuvatun mukaisesti.
Kaikki funktiot saavat parametrikseen osoittimen otsakkeen alkuun.
-
get_proto_version
joka palauttaa protokollaversion (Protocol Version) otsakkeesta, eli sen tulee palauttaa arvoja välillä 0 - 3. -
get_type
joka palauttaa Type - kentän arvon (välillä 0 - 3) -
get_subtype
joka palauttaa Subtype - kentän arvon (välillä 0 - 15) -
get_to_ds
,get_from_ds
,get_retry
,get_more_data
jotka palauttavat kyseisten lipukkeiden arvon otsakkeessa. Funktiot voivat palauttaa jonkun nollasta poikkeavan arvon mikäli kyseinen bitti on asetettu, tai 0 jos kyseinen bitti ei ole asetettu.
b) Kirjoita otsake
Toteuta seuraavat funktiot, joiden avulla tuotetaan otsake (tai osa siitä):
-
set_proto_version
joka asettaa Protocol Version kentän version - parametrin ilmaisemalla tavalla. -
set_type
joka asettaa Type - kentän funktion parametrissa ilmaistulla tavalla. -
set_subtype
joka asettaa Subtype - kentän funktion parametrissa ilmaistulla tavalla. -
set_to_ds
,set_from_ds
,set_retry
,set_more_data
, jotka asettavat kyseiset lipukebitit joko päälle tai pois sen perusteella onko parametrina annettu 0 (pois päältä) tai nollasta poikkeava arvo (päällä).
Kutakin funktiota kutsuttaessa vain kyseinen osa otsakkeesta saa muuttua, ja muiden kenttien sisällön tulee säilyä ennallaan.
Task 04_dungeon: Luolapeli (5 pts)
Konsolipohjaiset luolapelit ovat olleet merkittävä peligenre viime vuosikymmeninä. Tällaisia pelejä ovat esimerkiksi Rogue, Nethack tai Angband. Vaikka Xbox- ja iPad-sukupolvi on pitkälti unohtanut nämä pelit, palaamme hetkeksi tämän mainion pelityypin pariin.
Tässä, hieman isommassa tehtävässä toteutetaan köyhän miehen versio luolaseikkailupelistä. Toteutettavasta pelistä tulee puuttumaan runsaasti esikuviensa ominaisuuksia, mutta voit tehtävän ratkaistuasi toki jatkaa pelin kehittämistä eteenpäin.
Harjoituspohja sisältää enemmän valmista ohjelmakoodia kuin aiemmat tehtävät, ja monet funktioista on annettu jo valmiina. Sinun tulee vain toteuttaa toimivan pelin tarvitsemat puuttuvat funktiot alla olevien tehtäväkohtien mukaisesti. src - hakemisto sisältää seuraavat ohjelmamodulit:
-
main.c sisältää pelin pääsilmukan, joka pyytää käyttäjältä komentoa ja siirtää pelaajaa, sekä pelin sisältämiä hirviöitä eteenpäin vastaavasti.
-
mapgen.c rakentaa pelin luolaston huoneineen ja niitä yhdistävine käytävineen.
-
userif.c sisältää käyttöliittymätominnalisuuden, kuten kartan piirtämisen ruudulle, sekä käyttäjän syötteeseen reagoimisen. Oletuskonfiguraatiossa pelaaja näkee viiden ruudun päähän, eikä tietystikään pysty näkemään seinien taakse. Voit halutessasi muuttaa näitä ominaisuuksia vaikuttamatta testien tuloksiin.
-
monster.c sisältää hirviöiden toimintalogiikan. Peli on vuoropohjainen: aina kun pelaaja liikkuu, kaikki kartalla olevat hirviöt liikkuvat niinikään pohjautuen algoritmeihin jotka tulet toteuttamaan. Suurin osa tässä tehtävässä toteutettavista funktioista tulee olemaan tässä tiedostossa.
-
dungeon.h sisältää ohjelman vaatimat määrittelyt, kuten tarvittavat tietorakenteet, sekä modulien välisten julkisten funktioiden rajapintamäärittelyt. Otsakkeessa ei siis ole kaikkia em. tiedostojen funktioita, koska osa funktioista on yksityisiä kyseiselle ohjelmamodulille.
Pelin pääasialliset tietorakenteet ovat seuraavat: Game sisältää pelitilanteen kokonaisuudessaan, ja näitä tietorakenteita on vain yksi kerrassaan pelin aikana. Tietorakenteessa on esimerkiksi viittaus pelikarttaan, sekä dynaaminen taulukko joka sisältää hirviöt. Map sisältää varsinaisen kartan kaksiulotteisessa taulukossa. Creature sisältää yhden hirviön tiedot, joita sisältyy Game - rakenteessa olevaan taulukkoon useita. numMonsters kertoo kuinka monta oliota tässä dynaamisessa taulukossa on.
Mukana tuleva tehtäväpohja sisältää lisää tietoa esimerkiksi yksittäisten funktioiden toiminnasta. Voit muuttaa tehtäväkoodia monella tapaa vaikuttamatta tarkistusten lopputulokseen: voit esimerkiksi lisätä uuden tyyppisiä hirviöitä, vaihtaa kartan esitystapaa, muuttaa hirviöiden ominaisuuksia, jne. kunhan et muuta tai poista mukana tulevia tietorakenteiden kenttiä, joihin saatetaan viitata TMC-testeissä. Nämä testit keskittyvät vain muutamaan funktioon alla kuvattujen tehtäväkohtien mukaisesti. Muita funktioita voit muuttaa vapaasti.
Peli käynnistetään ajamalla käännöksen tuottama src/main - tiedosto. Tämä ei kuitenkaan tee mitään järkevää ennenkuin olet toteuttanut tehtävän vaatimat funktiot.
Alla on "kuvakaappaus" pelistä, jossa kaikki tehtäväkohdat on toteutettu. Hirviö 'D' lähestyy pelihahmoa '*' vasemmalta. '#' kuvaa seinää ja '.' lattiaa jota pitkin voi kävellä. Pelaajan nykyiset ja maksimi-osumapisteet (hit points) kerrotaan kartan alapuolella, jossa sijaitsee myös paikka komennolle.
... ##.## #...# #...# # ####...##.. ..D..*..... ########### HP: 12(12) command >
Pelissä on seuraavat komennot:
n
: siirry pohjoiseen (ylös)s
: siirry etelään (alas)e
: siirry itään (oikealle)w
: siirry länteen (vasemmalle)q
: poistu pelistä
Sinun tulee painaa enter:iä kunkin komennon jälkeen. Komentoja voi muuttaa tai lisätä: TMC-testit eivät välitä niistä.
Monet testit on toteutettu käyttäen valmiiksi tallennettua karttaa tiedostossa test/testmap. Tätä tiedostoa ei kannata muuttaa, koska se vaikuttaisi paikallisiin testeihin, mutta ei palvelimen suorittamiin testeihin.
a) Voinko liikkua?
Toteuta funktio int isBlocked(Game *game, int x, int y)
joka
palauttaa 0, mikäli kyseinen sijainti kartalla on vapaa liikkumiseen,
eli siinä ei ole seinää, eikä hirviötä. Mikäli liikkuminen kyseiseen
kohtaan ei ole mahdollista edellä mainituista syistä, funktio
palauttaa jonkun nollasta poikkeavan arvon. Funktion tulee palauttaa
nollasta poikkeava arvo myös silloin, kun kyseinen sijainti sijaitsee
kartan rajojen ulkopuolella. Useat seuraavista funktioista hyötyvät
tämän funktion käytöstä. Funktio tulee toteuttaa tiedostoon
userif.c.
b) Luo hirviöt
Toteuta funktio void createMonsters(Game *game)
joka luo
opts.numMonsters hirviötä ja sijoittaa ne satunnaisiin pisteisiin
kartalla. Voit käyttää rand - funktiota satunnaisten pisteiden
generointiin. Hirviön voi sijoittaa vain paikkaan joka ei ole seinä,
ja jossa ei ole jo ennestään hirviötä (eli kuten funktio isBlocked
kertoo). Alusta kukin luotu hirviö asianmukaisesti antamalla niille
nimi, karttasymbolimerkki, osumapisteet, jne. Hirviöllä tulee olla
enemmän kuin 0 osumapistettä, ja alussa osumapisteitä (hp) tulee
olla maksimimäärä (maxhp). Muutoin voit asettaa hirviön ominaisuudet
haluamallasi tavalla, kunhan nimi on asetettu ja karttasymboli on
jokin kirjain.
c) Siirry kohti pelaajaa
Toteuta funktio void moveTowards(Game *game, Creature
*monst)
joka siirtää hirviötä monst yhden askeleen kohti
pelaajaa. Oletuksena annettu pelilogiikka toimii siten, että hirviö
käyttää tätä funktiota siirtyäkseen kohti pelaajaa, ellei sillä ole
alhaiset osumapisteet (jolloin se yrittää karkuun). TMC-testi
tarkistaa seuraavat kriteerit:
-
Jos mahdollista, hirviön ja pelaajan välisen etäisyyden tulee vähentyä kutsun seurauksena
-
Hirviö ei voi siirtyä kerrallaan enempää kuin yhden askeleen kartalla
-
Hirviö ei voi siirtyä seinän päälle
-
Hirviö ei voi olla samassa ruudussa toisen hirviön kanssa
-
Hirviö ei voi sijaita samassa ruudussa pelihahmon kanssa
Näiden rajoitteiden sisällä voit toteuttaa liikkumisalgoritmin haluamallasi tavalla. Voit olettaa että hirviöillä on taikavoimia, joilla he aistivat pelaajan sijainnin myös seinien läpi.
d) Karkaa pelaajalta
Toteuta funktio void moveAway(Game *game, Creature *monst)
, joka
siirtää hirviötä monst yhden askeleen poispäin pelaajan
hahmosta. Oletusarvoisesti tätä funktiota kutsutaan kun hirviö on
vähällä kuolla, eli sillä on vähän osumapisteitä. Testi tarkistaa
seuraavat asiat:
-
Jos mahdollista, hirviön ja pelaajan välisen etäisyyden tulee kasvaa kutsun seurauksena
-
Hirviö ei voi siirtyä kerrallaan enempää kuin yhden askeleen kartalla
-
Hirviö ei voi siirtyä seinän päälle
-
Hirviö ei voi olla samassa ruudussa toisen hirviön kanssa
-
Hirviö ei voi sijaita samassa ruudussa pelihahmon kanssa
Näiden rajoitteiden sisällä voit toteuttaa liikkumisalgoritmin haluamallasi tavalla.
e) Hirviön toiminta
Funktio void monsterAction(Game *game)
käy läpi jokaisen elossa
olevan hirviön ja suorittaa niillä jonkin toimenpiteen. Mikäli hirviö
on pelaajan viereisessä ruudussa, sen tulee hyökätä pelaajan kimppuun
käyttäen attack - funktio-osoittimen määräämää
toiminnallisuutta. Muussa tapauksessa sen tulee liikkua johonkin
suuntaan käyttäen move - funktio-osoittimen määräämää
toiminnallisuutta. Kuollut hirviö (HP == 0 tai vähemmän) ei tee
mitään.
Creature-rakenteessa olevat funktio-osoittimet attack ja move määrittävät mitä hirviö kussakin tilanteessa tekee. Mikäli jompi kumpi osoittimista on NULL, hirviö ei kyseisessä tilanteessa tee mitään.
Sinun tulee siis asettaa funktio-osoittimiin sopivat hyökkäys- ja liikkumisfunktiot kun luot hirviöitä. Tehtäväpohja sisältää yhden hyökkäystoiminnon, mutta voit määritellä muitakin.