Creative Commons License

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.