This commit is contained in:
Anya Helene Bagge 2018-02-28 23:51:40 +01:00
parent c6692845f5
commit c1ad21da8c
53 changed files with 3884 additions and 2228 deletions

View File

@ -1,13 +1,17 @@
# [Semesteroppgave 1: “Rogue One-Oh-One”](https://retting.ii.uib.no/inf101.v18.sem1/blob/master/SEM-1.md)
# [Semesteroppgave 1: “Rogue One oh one”](https://retting.ii.uib.no/inf101.v18.sem1/blob/master/SEM-1.md)
Dette prosjektet inneholder [Semesteroppgave 1](SEM-1.md). Du kan også [lese oppgaven online](https://retting.ii.uib.no/inf101.v18.oppgaver/inf101.v18.sem1/blob/master/SEM-1.md) (kan evt. ha små oppdateringer i oppgaveteksten som ikke er med i din private kopi).
**Innleveringsfrist: Fredag 9. mars kl 2400**
**Innleveringsfrist:**
* Del A + minst to deloppgaver av Del B skal være ferdig til **fredag 9. mars kl. 2359**.
* Hele oppgaven skal være ferdig til **onsdag 14. mars kl. 2359**
**Utsettelse:** Hvis du trenger forlenget frist kan du få det om du ber om det (spør gruppeleder evt. foreleser/assistenter hvis det er en spesiell situasjon)
(Kryss av under her, i README.md, så kan vi følge med på om du anser deg som ferdig med ting eller ikke. Hvis du er helt ferdig til den første fristen, eller før den andre fristen, kan du si fra til gruppeleder slik at de kan begynne å rette.)
**Utsettelse:** Hvis du trenger forlenget frist er det mulig å be om det (spør gruppeleder evt. foreleser/assistenter hvis det er en spesiell situasjon). Hvis du ber om utsettelse bør du helst være i gang (ha gjort litt ting, og pushet) innen den første fristen.
* Noen dagers utsettelse går helt fint uten begrunnelse, siden oppgaven er litt forsinket.
* Hvis du jobber med labbene fremdeles (best om du er ferdig med Lab 4), si ifra om det, og så kan du få ekstra tid til å gjøre ferdig labbene før du går i gang med semesteroppgaven.
* Om det er spesielle grunner til at du vil trenge lengre tid, så er det bare å ta kontakt, så kan vi avtale noe. Du kan også ta kontakt om du [trenger annen tilrettelegging](http://www.uib.no/student/49241/trenger-du-tilrettelegging-av-ditt-studiel%C3%B8p).
* Hvis du jobber med labbene fremdeles, si ifra om det, og så kan du få litt ekstra tid til å gjøre ferdig labbene før du går i gang med semesteroppgaven. Det er veldig greit om du er ferdig med Lab 4 først.
* Om det er spesielle grunner til at du vil trenge lengre tid, så er det bare å ta kontakt, så kan vi avtale noe. Ta også kontakt om du [trenger annen tilrettelegging](http://www.uib.no/student/49241/trenger-du-tilrettelegging-av-ditt-studiel%C3%B8p).
# Fyll inn egne svar/beskrivelse/kommentarer til prosjektet under
@ -15,4 +19,17 @@ Dette prosjektet inneholder [Semesteroppgave 1](SEM-1.md). Du kan også [lese op
* Del A: [ ] helt ferdig, [ ] delvis ferdig
* Del B: [ ] helt ferdig, [ ] delvis ferdig
* Del C: [ ] helt ferdig, [ ] delvis ferdig
* [ ] hele semesteroppgaven er ferdig og klar til retting!
# Del A
## Svar på spørsmål
* ...
# Del B
## Svar på spørsmål
* ...
# Del C
## Oversikt over designvalg og hva du har gjort
* ... blah, blah, er implementert i klassen [KurtMario](src/inf101/v18/rogue101/player/KurtMario.java), blah, blah `ITurtleShell` ...

View File

@ -1,10 +1,10 @@
# [Semesteroppgave 1: “Rogue One Oh One”](https://retting.ii.uib.no/inf101.v18.sem1/blob/master/SEM-1.md)
* [Oversikt](SEM-1.md)
* [Praktisk informasjon](SEM-1.md#praktisk-informasjon)
* [Del A: Bakgrunn, modellering og utforskning](SEM-1_DEL-A.md)
* [Del B: Gjør ferdig nødvendige komponenter](SEM-1_DEL-B.md)
* [Del C: Selvvalgt del](SEM-1_DEL-C.md)
* **Oversikt**
* [Praktisk informasjon 5%](SEM-1.md#praktisk-informasjon)
* [Del A: Bakgrunn, modellering og utforskning 15%](SEM-1_DEL-A.md)
* [Del B: Fullfør basisimplementasjonen 40%](SEM-1_DEL-B.md)
* [Del C: Videreutvikling 40%](SEM-1_DEL-C.md)
# Praktisk informasjon

View File

@ -1,10 +1,10 @@
# [Semesteroppgave 1: “Rogue One oh one”](https://retting.ii.uib.no/inf101.v18.sem1/blob/master/SEM-1_DEL-A.md) Del A: Bakgrunn, modellering og utforskning
# [Semesteroppgave 1: “Rogue One oh one”](https://retting.ii.uib.no/inf101.v18.sem1/blob/master/SEM-1_DEL-A.md) Del A: Bakgrunn, modellering og utforskning *(15%)*
* [Oversikt](SEM-1.md)
* [Praktisk informasjon](SEM-1.md#praktisk-informasjon)
* [Del A: Bakgrunn, modellering og utforskning](SEM-1_DEL-A.md)
* [Del B: Gjør ferdig nødvendige komponenter](SEM-1_DEL-B.md)
* [Del C: Selvvalgt del](SEM-1_DEL-C.md)
* [Praktisk informasjon 5%](SEM-1.md#praktisk-informasjon)
* **Del A: Bakgrunn, modellering og utforskning 15%**
* [Del B: Fullfør basisimplementasjonen 40%](SEM-1_DEL-B.md)
* [Del C: Videreutvikling 40%](SEM-1_DEL-C.md)
## Kunnskap/konsepter du har bruk for til denne delen
@ -31,21 +31,27 @@ Et eksempel på et slikt labyrint-spill er [Rogue](https://en.wikipedia.org/wiki
Moderne roguelike spill (og moderne utgaver av de gamle) kommer gjerne med mer fancy grafikk og er ofte laget av uavhengige utviklere. I de litt bredere kategoriene “dungeon crawls” og “adventure” finner du en haug med vanlige, populære spill f.eks. [Zelda-serien](https://en.wikipedia.org/wiki/The_Legend_of_Zelda_(video_game)) (startet i 1986 med “gå rundt på et 2D-grid/kart, plukk opp ting, sloss med monstre og løs puzzles”; de nyeste versjonene tilbyr en åpen 3D-verden med detaljert fysikk, grafikk og lyd).
Vi har tenkt å holde det relativt enkelt (mer som 1980 enn som 2020), siden det antakelig er få av dere som er fulltids spilldesignere/spillprogrammører (og om du er det, har du neppe tid til å gjøre denne semesteroppgaven!). “Rogue101” (du kan selvfølgelig finne på et eget navn) skal
Vi har tenkt å holde det relativt enkelt (mer som 1980 enn som 2020), siden det antakelig er få av dere som er fulltids spilldesignere/spillprogrammører (og om du er det, har du neppe tid til å gjøre denne semesteroppgaven!).
“Rogue101” (du kan selvfølgelig finne på et eget navn) skal
* være [*turn-based*](https://en.wikipedia.org/wiki/Turn-based_strategy) dvs., spillet venter på at spilleren skal gjøre et trekk
* foregå på et [todimensjonalt rutenett/kart](https://en.wikipedia.org/wiki/Tile-based_video_game) basert på samme måte som Labyrint-labben, og sett ovenfra / i fuglepersektiv
* ha ting som spilleren kan plukke opp / gjøre noe med / legge fra seg
* ha andre aktører som går rundt på samme kartet og er styrt av datamaskinen (f.eks., monstre, kaniner, flyvende regnbueenhjørninger, zombier, amerikanske politikere, etc)
Om du nå er fristet til å gjøre litt [“research”](http://larn.org/larn/larn.html?mode=amiga) på [problemdomenet](http://store.steampowered.com/tags/en/Rogue-like), så trenger du ikke det oppgaven kommer med beskrivelse av reglene du skal implementere, og en del tips til mulige varianter. I siste del av semesteroppgaven kan du designe ting sånn som du selv har lyst til. Ellers så får du gjerne litt ideer selv etterhvert som du blir kjent med systemet. (Det er altså *ikke* et mål å lage en kopi av Rogue eller et annet spill, og ting trenger overhodet ikke ha noe med dungeons, monstre og sverd å gjøre du kan f.eks. lage spill der du er korrupt saksbehandler i et kontorlandskap som må plukke opp alle sakspapirene og putte de i makuleringsmaskinen før FBI-agenten tar deg...)
Om du nå er fristet til å gjøre litt [“research”](http://larn.org/larn/larn.html?mode=amiga) på [“problem](http://store.steampowered.com/tags/en/Rogue-like)-[domenet”](http://www.roguebasin.com/index.php?title=Main_Page), så trenger du ikke det oppgaven kommer med beskrivelse av reglene du skal implementere, og en del tips til mulige varianter. I siste del av semesteroppgaven kan du designe ting sånn som du selv har lyst til; du får gjerne litt ideer selv etterhvert som du blir kjent med systemet.
Det er altså *ikke* et mål å lage en kopi av Rogue eller et annet spill, og ting trenger overhodet ikke ha noe med dungeons, monstre og sverd å gjøre du kan f.eks. lage spill der du er korrupt saksbehandler i et kontorlandskap som må plukke opp alle sakspapirene og putte de i makuleringsmaskinen før FBI-agenten tar deg...
![original game screen shot](https://upload.wikimedia.org/wikipedia/commons/1/17/Rogue_Screen_Shot_CAR.PNG)
## Oversikt Modellering
Vi kan tenke på programmet vårt som en “modell” av et dungeon crawler spill. For å finne ut hvilke klasser og interfaces vi trenger, må vi
Vi kan tenke på programmet vårt som en “modell” av et dungeon crawler spill.
For å finne ut hvilke klasser og interfaces vi trenger, må vi
* a) først tenke oss hvilke elementer som inngår i spill-verdenen;
* b) finne ut hvordan vi representerer og implementerer disse på datamaskinen.
@ -69,7 +75,7 @@ Basert på tidligere erfaringer med slike spill, samt grunding tenking og lesing
* *eller* en aktør en “levende” ting som kan bevege seg rundt på kartet
* For enkelhets skyld sier vi at det bare kan være maks én aktør i hver kartrute men en aktør kan dele plass med andre ting.
* Aktørene er enten styrt av datamaskinen, eller styrt av spilleren.
* For å gjøre ting litt mer spill-aktig, har vi følgende regler for ting og aktører
* For å gjøre ting litt mer spill-aktig, har vi følgende regler for ting og aktører:
* alle ting (inkl aktører) har “helse-poeng” som indikerer i hvor god form/stand aktøren/tingen er; negative helsepoeng betyr at tingen er helt ødelagt og skal fjernes fra brettet
* alle ting (inkl aktører) har “forsvars-poeng” som indikerer hvor god den er til å forsvare seg (mot å bli angrepet, plukket opp, vasket bak ørene, e.l.)
* alle aktører har "angreps-poeng" som indikerer hvor god den er til å overgå andres forsvar (og f.eks. skade dem, plukke dem opp, vaske dem bak ørene, e.l.)
@ -80,11 +86,11 @@ Basert på dette tenker vi oss følgende typer objekter:
* [IMapView](src/inf101/v18/rogue101/map/IMapView.java), [IGameMap](src/inf101/v18/rogue101/map/IGameMap.java) spillkartet
* [IGame](src/inf101/v18/rogue101/game/IGame.java) selve spillet, som styrer reglene i spillverdenen
* [IItem](src/inf101/v18/rogue101/objects/IItem.java) en ting. Siden både småobjekter (sverd og gulrøtter), aktører og vegger er ting som befinner seg på kartet, er det praktisk å gjøre alle til IItem.
* [Wall](src/inf101/v18/rogue101/objects/Wall.java) en IItem som ikke kan dele plass med noe annet
* [IActor](src/inf101/v18/rogue101/objects/IActor.java) en IItem som bevege seg og ikke kan dele plass med en annen IActor
* [IPlayer](src/inf101/v18/rogue101/objects/IPlayer.java) en IActor som styres ved at brukeren trykker på tastene
* [INonPlayer](src/inf101/v18/rogue101/objects/INonPlayer.java) en IActor som styrer seg selv (datamaskinen styrer)
* [IItem](src/inf101/v18/rogue101/objects/IItem.java) en ting. Siden både småobjekter (sverd og gulrøtter), aktører og vegger er ting som befinner seg på kartet, er det praktisk å gjøre alle til `IItem`:
* [Wall](src/inf101/v18/rogue101/objects/Wall.java) en `IItem` som ikke kan dele plass med noe annet
* [IActor](src/inf101/v18/rogue101/objects/IActor.java) en `IItem` som bevege seg og ikke kan dele plass med en annen `IActor`
* [IPlayer](src/inf101/v18/rogue101/objects/IPlayer.java) en `IActor` som styres ved at brukeren trykker på tastene
* [INonPlayer](src/inf101/v18/rogue101/objects/INonPlayer.java) en `IActor` som styrer seg selv (datamaskinen styrer)
Vi har også et par andre mer abstrakte ting vi bør tenke på f.eks. koordinater. Det går an å bruke heltall som koordinater / indekser (`int x`, `int y`), men det er generelt ganske praktisk med en egen abstraksjon for grid-plasseringer; blant annet kan vi da slippe å gjøre kompliserte utregninger på koordinatene for å finne frem til andre koordinater. Vi har derfor også:
* [ILocation](src/inf101/v18/grid/ILocation.java) en lovlig (x,y)-koordinat på kartet. Hver ILocation har opptil åtte andre ILocations som naboer, og har metoder for å finne alle eller noen av naboene, og for å finne nabo i en spesifikk retning.
@ -92,34 +98,34 @@ Vi har også et par andre mer abstrakte ting vi bør tenke på f.eks. koordi
* [IArea](src/inf101/v18/grid/IArea.java) et rektangulært sett med ILocations. Brukes f.eks. av spillkartet for å lettvint gå gjennom alle cellene/rutene i kartet.
* ([IGrid<T>](src/inf101/v18/grid/IGrid.java) og [IMultiGrid<T>](src/inf101/v18/grid/IMultiGrid.java) IGrid<T> er tilsvarende til den du har brukt i labbene tidligere; IMultiGrid<T> er et grid der hver celle er en liste av T-er. Den blir brukt av spillkartet, men du trenger neppe bruke den selv.)
### Deloppgave A1: Tilstand, oppførsel og grensesnitt for objektene
*Du vil sikkert finne på lurere svar på spørsmålene etterhvert som du jobber med oppgaven. Det er fint om du lar de opprinnelige svarene stå (det er helt OK om de er totalt feil eller helt på jordet) og heller gjør tilføyelser. Du kan evt. bruke ~~overstryking~~ (putt dobbel tilde rundt teksten, `~~Rabbit.java funker fordi det bor en liten kanin inni datamaskinen~~`) for å markere det du ikke lenger synes er like lurt.
### *(4%)* Deloppgave A1: Tilstand, oppførsel og grensesnitt for objektene
*Du vil sikkert finne på lurere svar på spørsmålene etterhvert som du jobber med oppgaven. Det er fint om du lar de opprinnelige svarene stå (det er helt OK om de er totalt feil eller helt på jordet) og heller gjør tilføyelser. Du kan evt. bruke ~~overstryking~~ (putt dobbel tilde rundt teksten, `~~Rabbit.java funker fordi det bor en liten kanin inni datamaskinen~~`) for å markere det du ikke lenger synes er like lurt.*
Alle grensesnittene beskriver hvordan du kan håndtere objekter (objekter som er av klasser som implementerer grensesnittene). Selv om tilstanden til objektene er innkapslet (du vet ikke om feltvariablene), så lar metodene deg *observere* tilstanden, så ut fra de tilgjengelige metodene kan du spekulere litt rundt hvordan tilstanden må være.
Alle grensesnittene beskriver *hvordan du kan håndtere objekter* (dvs. objekter som er av klasser som implementerer grensesnittene). Selv om tilstanden til objektene er innkapslet (du vet ikke om feltvariablene), så lar metodene deg *observere* tilstanden, så ut fra de tilgjengelige metodene kan du spekulere litt rundt hvordan tilstanden må være.
Les gjennom grensesnittene vi har nevnt over [IGame](src/inf101/v18/rogue101/game/IGame.java), ([IMapView](src/inf101/v18/rogue101/map/IMapView.java), [IItem](src/inf101/v18/rogue101/objects/IItem.java), [IActor](src/inf101/v18/rogue101/objects/IActor.java), [INonPlayer](src/inf101/v18/rogue101/objects/INonPlayer.java), [IPlayer](src/inf101/v18/rogue101/objects/IPlayer.java) vent med å se på klassene) og svar på spørsmålene (skriv svarene i [README.md](README.md), det holder med én eller noen få setninger):
Les gjennom grensesnittene vi har nevnt over ([IGame](src/inf101/v18/rogue101/game/IGame.java), [IMapView](src/inf101/v18/rogue101/map/IMapView.java), [IItem](src/inf101/v18/rogue101/objects/IItem.java), [IActor](src/inf101/v18/rogue101/objects/IActor.java), [INonPlayer](src/inf101/v18/rogue101/objects/INonPlayer.java), [IPlayer](src/inf101/v18/rogue101/objects/IPlayer.java) vent med å se på klassene) og svar på spørsmålene (skriv svarene i [README.md](README.md), det holder med én eller noen få setninger):
* **1. Hva vil du si utgjør tilstanden til objekter som implementerer de nevnte grensesnittene?** *(F.eks. hvis du ser på `ILocation` så vil du gjerne se at ILocation-objekter må ha en tilstand som inkluderer `x`- og `y`-koordinater selv om de sikkert kan lagres på mange forskjellige måter)*
* **a)** Hva vil du si utgjør tilstanden til objekter som implementerer de nevnte grensesnittene? *(F.eks. hvis du ser på `ILocation` så vil du gjerne se at ILocation-objekter må ha en tilstand som inkluderer `x`- og `y`-koordinater selv om de sikkert kan lagres på mange forskjellige måter. De må også vite om eller være koblet til et `IArea`, siden en `ILocation` ser ut til å “vite” hvilke koordinater som er gyldige.)*
* **2. Hva ser ut til å være sammenhengen mellom grensesnittene?** Flere av dem er f.eks. laget slik at de utvider (extends) andre grensesnitt. Hvem ser ut til å ta imot / returnere objekter av de andre grensesnittene?
* **b)** Hva ser ut til å være sammenhengen mellom grensesnittene? Flere av dem er f.eks. laget slik at de utvider (extends) andre grensesnitt. Hvem ser ut til å ta imot / returnere objekter av de andre grensesnittene?
* **3. Det er to grensesnitt for kart, både [IGameMap](src/inf101/v18/rogue101/map/IGameMap.java) og [IMapView](src/inf101/v18/rogue101/map/IMapView.java). Hvorfor har vi gjort det slik?**
* **c)** Det er to grensesnitt for kart, både [IGameMap](src/inf101/v18/rogue101/map/IGameMap.java) og [IMapView](src/inf101/v18/rogue101/map/IMapView.java). Hvorfor har vi gjort det slik?
* **4. Hvorfor tror du [INonPlayer](src/inf101/v18/rogue101/objects/INonPlayer.java) og [IPlayer](src/inf101/v18/rogue101/objects/IPlayer.java) er forskjellige? Ville du gjort det annerledes?**
* **d)** Hvorfor tror du [INonPlayer](src/inf101/v18/rogue101/objects/INonPlayer.java) og [IPlayer](src/inf101/v18/rogue101/objects/IPlayer.java) er forskjellige? Ville du gjort det annerledes?
### Deloppgave A2: Eksempler på IItem og IActor
### *(3%)* Deloppgave A2: Eksempler på IItem og IActor
Til denne deloppgaven kan du se først på [Carrot](src/inf101/v18/rogue101/objects/Carrot.java) og [Rabbit](src/inf101/v18/rogue101/objects/Rabbit.java). Svar på spørsmålene (skriv svarene i [README.md](README.md), det holder med én eller noen få setninger):
* **5. Stemmer implementasjonen overens med hva du tenkte om tilstanden i Spørsmål 1 (over)? Hva er evt. likt / forskjellig?**
* **e)** Stemmer implementasjonen overens med hva du tenkte om tilstanden i Spørsmål 1 (over)? Hva er evt. likt / forskjellig?
Se på [Game](src/inf101/v18/rogue101/game/Game.java) og [GameMap](src/inf101/v18/rogue101/map/GameMap.java) også.
`Rabbit` trenger å vite hvor den er, fordi den skal prøve å spise gulroten (hvis den finner en) og fordi den må finne seg et gyldig sted å hoppe videre til.
* **6. Hvordan finner Rabbit ut hvor den er, hvilke andre ting som er på stedet og hvor den har lov å gå?**
* **f)** Hvordan finner Rabbit ut hvor den er, hvilke andre ting som er på stedet og hvor den har lov å gå?
* **7. Hvordan vet `Game` hvor `Rabbit` er når den spør / hvordan vet `Game` *hvilken* `Rabbit` som kaller `getLocation()`?**
* **g)** Hvordan vet `Game` hvor `Rabbit` er når den spør / hvordan vet `Game` *hvilken* `Rabbit` som kaller `getLocation()`?
### Deloppgave A3: Litt endringer
### *(8%)* Deloppgave A3: Litt endringer
* Du kan kjøre programmet ved å kjøre `inf101.v18.rogue101.Main`. Hendige tastetrykk (du skal få lov å legge til flere selv senere):
* *Return* gjør ett steg (selv om vi foreløpig ikke har en IPlayer på brettet)
* Ctrl-Q / Cmd-Q avslutt
@ -129,7 +135,7 @@ Se på [Game](src/inf101/v18/rogue101/game/Game.java) og [GameMap](src/inf101/v1
Hvis du kjører programmet og trykker litt på returtasten vil du se at kaninene (merket med `r`) hopper rundt litt, at gulrøttene (oransje dingser med grønn topp) forsvinner og at kaninene så etterhvert forsvinner.
#### Smart kanin
Hvis du ser på koden for [Rabbit.java]() finner du gjerne også ut hvorfor ting oppfører seg slik: kaninenes helse er avhengig av at de finner noe å spise, og bevegelsene er helt tilfeldige. Prøv ut noen forskjellige endringer:
Hvis du ser på koden for [Rabbit.java](src/inf101/v18/rogue101/examples/Rabbit.java) finner du gjerne også ut hvorfor ting oppfører seg slik: kaninenes helse er avhengig av at de finner noe å spise, og bevegelsene er helt tilfeldige. Prøv ut noen forskjellige endringer:
* **a)** Juster maksimale helsepoeng på kaninene (evt. også på gulrøttene), og om du merker noen forskjell (husk at programmet foreløpig ikke gjør noe før du trykker retur/enter)
* **b)** La kaninene alltid gå i samme retning (f.eks. `game.move(GridDirection.NORTH` kommenter ut den gamle koden). Prøv ut hva som skjer når de treffer vegger.
@ -139,6 +145,7 @@ Hvis du ser på koden for [Rabbit.java]() finner du gjerne også ut hvorfor ting
* du kan finne ut hva som ligger i nabofeltet ved hjelp av kartet (`game.getMap()`); f.eks. med metoden `getItems()`.
* kaninen har allerede kode for å sjekke gjennom tingene og se om den finner en `Carrot` du kan kopiere og tilpasse denne
* hvis kaninen finner en gulrot kan den gjøre `game.move(...)` og så returnere med en gang
** *e)** Kaninens jobb blir litt enklere om den får litt hjelp fra `Game` med å finne ut hvor den kan gå. Implementer metoden `getPossibleMoves()` i `Game`.
#### Bedre gulrøtter
@ -154,7 +161,7 @@ Prøv også å justere gulrøttene litt ([Carrot](src/inf101/v18/rogue101/exampl
* For å finne en tilfeldig `ILocation` kan du bruke `map.getLocation(x, y)` med tilfeldig x og y (innenfor `getWidth()`/`getHeight()`). Du kan også plukke et tilfeldig element fra `map.getArea().locations()`.
* Før du evt. putter en `new Carrot()` på kartet må du også passe på at kartruten du har funnet er ledig (ihvertfall at den ikke inneholder en vegg).
### Deloppgave A4: Oversikt
### *(0%*) Deloppgave A4: Oversikt
**Tegn en liten oversikt over det du tenker er de viktigste grensesnittene/klassene i programmet.**
@ -164,10 +171,10 @@ Prøv også å justere gulrøttene litt ([Carrot](src/inf101/v18/rogue101/exampl
(Du trenger ikke legge ved tegningen din, men du kan gjerne lage og legge ved en oppdatert utgave når du har fått bedre/full forståelse av systemet.)
### Deloppgave A5: Ting du ikke trenger å se på (0/100)
### *(0%)* Deloppgave A5: Ting du ikke trenger å se på (0/100)
* Du trenger ikke se på koden i `gfx` (grafikkbibliotek), `grid` (utvidet IGrid) eller `util` (generering av testdata).
* Hvis du lager grafikk selv, vil du gjerne komme til å *bruke* [`ITurtle`](src/inf101/v18/gfx/gfxmode/ITurtle.java) (fra `gfx`), men du trenger ikke se på implementasjone.
* GameMap gjør bruk av `grid`-pakken, men du trenger antakelig ikke gjøre noe med den selv.
# Gå videre til [**DEL B**](SEM-1DEL-B.md)
# Gå videre til [**DEL B**](SEM-1_DEL-B.md)

View File

@ -1,42 +1,48 @@
# [Semesteroppgave 1: “Rogue One oh one”](https://retting.ii.uib.no/inf101.v18.sem1/blob/master/SEM-1_DEL-B.md) Del B: Implementasjonsarbeid
# [Semesteroppgave 1: “Rogue One oh one”](https://retting.ii.uib.no/inf101.v18.sem1/blob/master/SEM-1_DEL-B.md) Del B: Fullfør basisimplementasjonen
* [Oversikt](SEM-1.md)
* [Praktisk informasjon](SEM-1.md#praktisk-informasjon)
* [Del A: Bakgrunn, modellering og utforskning](SEM-1_DEL-A.md)
* [Del B: Gjør ferdig nødvendige komponenter](SEM-1_DEL-B.md)
* [Del C: Selvvalgt del](SEM-1_DEL-C.md)
* [Praktisk informasjon 5%](SEM-1.md#praktisk-informasjon)
* [Del A: Bakgrunn, modellering og utforskning 15%](SEM-1_DEL-A.md)
* **Del B: Fullfør basisimplementasjonen 40%**
* [Del C: Videreutvikling 40%](SEM-1_DEL-C.md)
I denne delen av semesteroppgaven skal du implementere en del konkrete metoder.
### Deloppgave B1: Objekt-fabrikk
* a) Se på konstruktøren i Game-klassen, og se hvordan kartet blir fylt inn. Finn metoden som lager nye objekter basert på strenger ('#', '.', 'R', osv) dette er en såkalt [factory/fabrikk-metode](https://en.wikipedia.org/wiki/Factory_method_pattern). Det er vanlig å bruke når vi har mange klasser som implementerer det samme grensesnittet, og vi vil gjøre det lett for andre deler av programmet å opprette objekter uten å kjenne til klassene.
### Konsepter
* *Factory pattern* *fabrikk-metoder* brukes til å lage nye objekter; ofte ved at du velger hva du vil ha med parameterne, uten at du trenger å kjenne til klassen. Passer veldig bra sammen med `interface` hvor du vet hvordan du skal bruke objektene men ikke hvordan du kan lage dem.
* (*Design pattern* er en vanlig måte å løse noe på uten at det finnes noen egen mekanisme for det i programmerinsspråket. Å bruke fabrikk-metoder og fabrikk-objekter til å lage objekter av en interface type er en vanlig design pattern.)
* *Event-drevet kjøring* du har ingen løkke i en `main`-metode som leser inn input fra brukeren; i stedet blir programmet vårt kalt opp når det skjer noe. Vanligvis vil datamaskinen da “sove” eller gjøre noe annet i mellomtiden. Dette kan også gjøre det litt lettere å teste spiller-objektet vårt vil ikke merke forskjell på om det er en faktisk bruker som har trykket en tast, eller vi bare simulerer et tastetrykk.
## *(3%)* Deloppgave B1: Objekt-fabrikk
* a) Se på konstruktøren i Game-klassen, og se hvordan kartet blir fylt inn. Finn metoden som lager nye objekter basert på strenger (`"#"`, `"."`, `"R"`, osv) dette er en såkalt [factory/fabrikk-metode](https://en.wikipedia.org/wiki/Factory_method_pattern). Det er vanlig å bruke når vi har mange klasser som implementerer det samme grensesnittet, og vi vil gjøre det lett for andre deler av programmet å opprette objekter uten å kjenne til klassene.
* **b)** Lag deg en ny klasse som implementerer [IItem](src/inf101/v18/rogue101/objects/IItem.java). Du kan ta utgangspunkt i en av klassene du har fra før (f.eks. `Carrot`) og du kan velge navn selv (f.eks. `CarrotCake`). Funksjonaliteten kan (foreløpig) være helt lik.
* **c)** Legg klassen til i fabrikk-metoden. Du må velge et tegn som skal representere objekter av klassen (f.eks. `"c"`). Bruk dette tegnet både i fabrikk-metoden i `Game` og i `getSymbol()`-metoden i den nye klassen din (det er ingen direkte sammenheng mellom disse, men greit om de stemmer overens).
* **d)** Legg til det nye tegnet ditt i standard-kartet (ligger i [src/inf101/v18/rogue101/map/maps/level1.txt](src/inf101/v18/rogue101/map/maps/level1.txt) eventuelt ligger det et innebygget kart i `Main`-klassen hvis vi av en eller annen grunn ikke finner kartfilen), og se at gulrotkaken (eller det du har laget) dukker opp på skjermen.
* **d)** Legg til det nye tegnet ditt i standard-kartet (ligger i [src/inf101/v18/rogue101/map/maps/level1.txt](src/inf101/v18/rogue101/map/maps/level1.txt) eventuelt ligger det et innebygget kart i `Main`-klassen hvis vi av en eller annen grunn ikke finner kartfilen; vi bruker forøvrig samme kartformat som [semesteroppgave 1 i fjor](https://retting.ii.uib.no/inf101.v17.oppgaver/inf101.v17.sem1/)), og se at gulrotkaken (eller det du har laget) dukker opp på skjermen.
Symbolene på skjermen blir hentet fra `getSymbol()` (evt. `getPrintSymbol()` om du har definert den). I tillegg til å kunne tegne tekst-tegn kaller programmet `draw()`-metoden hvor du kan legge inn egen grafikk; hvis denne returnerer `true` blir tegnet fra `getSymbol()` ikke tegnet på skjermen. Så hvis du bare ser ekstra `X`-er, så har du kopiert `ExampleItem` uten å endre `getSymbol()`, og hvis du bare ser ekstra gulrøtter, så har du kopiert `Carrot` uten å oppdatere grafikken (du kan bare slette draw-metoden og returnere false).
*(Avansert mulighet (ikke del av oppgaven): i stedet for å lage en stor fabrikk med `switch` går det an å bygge en oversikt over fabrikkene mens programmet kjører. Du kan registrere symbolet for den nye klassen + en kodesnutt som lager objekt i `itemFactories` i `Game`: f.eks. `itemFactories.put("R", () -> new Rabbit())`. `default`-tilfellet i `switch`en vil prøve å slå opp i `itemFactories` og kalle kodesnutten hvis den finner noe. Registreringen kan gjøres i konstruktøren til `Game`, eller du kan lage en ekstra `addFactory`-metode.)*
### Deloppgave B2: Minimal Player
## *(7%)* Deloppgave B2: Minimal Player
Du er sikkert lei av at det bare er kaninene som får lov til å ha det gøy.
* **a)** Lag en klasse `Player implements` [`IPlayer`](src/inf101/v18/rogue101/objects/IPlayer.java) (putt den i `objects` eller en egen ny `player` pakke). Du kan følge mønsteret fra de andre IItem-klassene, men du trenger litt andre metoder. Der hvor `Rabbit` har `doTurn()` skal `Player` ha `keyPressed()`. Du kan la denne være tom til å begynne med.
* **b)** Du må oppdatere fabrikken i `Game` så den også lager `Player`-objekter; de har tradisjonelt symboled `@` (i utlevert kode gir det deg en `ExampleItem`).
* **b)** Du må oppdatere fabrikken i `Game` så den også lager `Player`-objekter; de har tradisjonelt symbolet `@` (i utlevert kode gir det deg en `ExampleItem`).
* **c)** Metoden `keyPressed()` i `IPlayer` tar et `KeyCode`-object, og kalles hver gang spilleren
trykker på en tast. Knappen indikeres med KeyCode-en (en for hver tast på tastaturet + at man kan sjekke f.eks. om *Ctrl*-tasten er trykket inn). F.eks. vil følgende kode sjekke om
venstre-tasten ble trykket, og kalle `tryToMove()` (denne metoden er ikke implementert ennå):
```
public void keyPressed(IGame game, KeyCode key) {
if (key == KeyCode.LEFT) {
tryToMove(game, GridDirection.WEST);
}
}
```
Gjør ferdig denne metoden så den kaller `tryToMove` med `GridDirection.EAST`, `GridDirection.NORTH`, eller `GridDirection.SOUTH` når spilleren trykker `KeyCode.RIGHT`, `KeyCode.UP`, eller `KeyCode.DOWN`.
(Dette er typisk enkel *event-drevet programmering* hvor metoder i programmet vårt blir kalt når
ting skjer utenfor programmet. Dette er vanlig for spill, nettverkstjenere og programmer med grafiske brukergrensesnitt man har en metode som blir kalt for hvert tidssteg, og metoder som blir kalt når tastatur eller mus brukes eller det kommer inn en ny forespørsel over nettet.)
ting skjer utenfor programmet. Dette er vanlig for spill, nettverkstjenere og programmer med grafiske brukergrensesnitt man har en metode som blir kalt for hvert tidssteg, og metoder som blir kalt når tastatur eller mus brukes eller det kommer inn en ny forespørsel over nettet. Legg merke til at det ikke er slik at Player gjør noe for å sjekke om brukeren har trykket på en tast derimot blir metoden `keyPressed` kalt når tasten trykkes, og Player vet ingenting om hvordan det skjer eller hvor tastetrykkene kommer fra.)
* **d)** Du trenger også metoden `tryToMove()` (du kan kalle den hva du vil, men det er greit å ha den som en egen metode, siden du gjør nesten det samme for alle retningene).
* Den må spørre `game` om det er lov å gå i den aktuelle retningen, og i såfall kalle `game.move()` for å flytte i den gitte retningen. Du kan se hvordan `Rabbit` løser dette hos Player slipper du heldigvis å tenke på hvor det er *lurt* å flytte, siden brukeren alt har bestemt det.
@ -44,7 +50,9 @@ ting skjer utenfor programmet. Dette er vanlig for spill, nettverkstjenere og pr
* **e)** Brukeren trenger sikkert også litt statusinformasjon, f.eks. om antall helsepoeng. Lag en metode `showStatus(game)` i `Player`-klassen din. Den kan f.eks. bruke `game.displayStatus("...")` eller `game.formatStatus("... %d ...", verdi)` (hvis du er komfortabel med `printf`-liknende strengformattering). Begge disse printer en linje med tekst på skjermen (Main-klassen holder rede på de passende linjene, status havner på linjen `Main.LINE_STATUS` som er 21 (rett under kartet hvis kartet er 20 linjer høyt)). Kall `showStatus()` på slutten av `keyPressed()` (hvis du senere lager andre metoder som kan endre tilstanden til spilleren, bør du kalle `showStatus()` på slutten av disse også).
### Deloppgave B3: Sortering av items
* **f)** Lag noen tester for Player-klassen din, som sjekker bevegelsene. Du kan ta utgangspunkt i den lille testen som ligger i [PlayerTest.java](src/inf101/v18/rogue101/tests/PlayerTest.java); test at det funker å flytte seg i forskjellige retninger, og at det *ikke* funker å flytte seg inn i veggen.
## *(5%)* Deloppgave B3: Sortering av items
Vi er ikke så veldig lure med hvordan vi samler en mengde IItems i en List i kart-cellene. F.eks. blir grafikken antakelig veldig rotete om vi prøver å tegne opp mer én ting i hver rute så vi trenger gjerne en bedre måte å bestemme rekkefølgen de ligger i listen på (eller evt. hvordan vi velger hvilken ting vi skal tegne). I den utleverte koden tegner vi alltid bare den første tingen i hver celle, uavhengig av om den er interessant eller ikke. Du har kanskje lagt merke til at kaninene “forsvinner” når de står på et felt med en gulrot (hvis ikke, kan du prøve å flytte spilleren til en gulrot og se hva som skjer).
@ -67,8 +75,10 @@ For å fikse dette vil vi at kartet skal ha tingene liggende i sortert rekkeføl
* så må du opprette noen IItems og legge dem til på kartposisjonen ved å kalle `GameMap.add()`
* Til slutt kan du teste at tingene ligger i den rekkefølgen du forventer (`GameMap.getAll()`, og sjekk listeelementene med `get()`)
* *(Litt mer avansert:)* Hvis du vil teste med litt større mengder data er det gjerne upraktisk å sjekke nøyaktig hvordan elementene er plassert da kan du i stedet gå gjennom listen og sjekke at `list.get(i).compareTo(list.get(i+1)) >= 0` for `0 <= i < list.size()-1`. Et godt utvalg data kan du få ved å lage deg en metode som lager tilfeldige IItems den kan uansett være nyttig for å lage tilfeldige kart og slikt. (Du skal lære dette mer grundig i neste lab-oppgave.)
### Deloppgave B4: Plukk opp / dropp ting
*(Avansert mulighet (ikke del av oppgaven): En mer generell løsning er å lage en sortert liste (kanskje helst `IList/MyList` fra Lab 2/3/4, men evt `List/ArrayList`) og så bruke den i stedet for vanlig liste i `IMultiGrid` og `MultiGrid`. Du trenger litt mer avansert Java-kunnskap for å få det til å funke, du må bla. si at elementtypen er sammenliknbar (f.eks. `ISortedList<T extends Comparable<T>>`) vi ser mer på dette senere i semesteret. )*
## *(5%)* Deloppgave B4: Plukk opp / dropp ting
Vi vil gjerne la brukeren kunne plukke opp og legge fra seg ting på kartet halvspiste gulrøtter, for eksempel.
@ -78,13 +88,13 @@ Vi vil gjerne la brukeren kunne plukke opp og legge fra seg ting på kartet
* `game.pickUp()` plukker opp et spesifikt objekt som ligger i kartruten du står i, så du er nødt til å finne ut *hvilket objekt* du vil prøve å plukke opp hvis det er flere mulighetre (foreløpig har brukeren ingen måte å be om et spesifikt IItem på);
* du finner alle tingene som ligger i kartruten med `game.getLocalItems()`, og du kan f.eks. prøve å ta den første. Du kan også finne tingene ved å få tak i kartet (game.getMap()) og undersøke det direkte bare pass på at spilleren ikke ender opp med å plukke opp seg selv! (getLocalItems() gir deg bare items som ikke er IActor, så den er “trygg”)
* husk at ruten kan være tom, dvs at `game.getLocalItems()` gir tom liste da kan du f.eks. gi bruken beskjed om at det ikke er noe å plukke opp.
* **c)** For å kunne legge fra deg ting må du ha lagret objektet du plukket opp, f.eks. i en feltvariabel i `Player`. Foreløpig går det greit å tenke seg at du bare kan holde på én ting, så da er det lett å vite hva dus skal legge fra deg. Implementér “dropp / legg fra deg”-funksjonaliteten. Hint:
* **c)** For å kunne legge fra deg ting må du ha lagret objektet du plukket opp, f.eks. i en feltvariabel i `Player`. Foreløpig går det greit å tenke seg at du bare kan holde på én ting, så da er det lett å vite hva du skal legge fra deg. Implementér “dropp / legg fra deg”-funksjonaliteten. Hint:
* Du kan gi en passende melding til brukeren om du ikke har noe å legge fra deg.
* Pass på at når du har lagt fra deg objektet (med `game.drop()`) så sletter du det fra `Player`-objektet ellers kan du massekopiere ting ved å plukke opp én ting og så legge den fra deg mange ganger.
* `game.drop()` kan i prinsippet feile (fullt på bakken, kanskje?), i såfall returnerer den `false` og du bør *ikke* slette tingen fra `Player`-objektet
* **d)** Det er praktisk for brukeren å vite hva man bærer på, så legg inn navnet på objektet i status-meldingen (fra B2.e)
### Deloppgave B5: Finne synlige ting / ting i nærheten
## *(5%)* Deloppgave B5: Finne synlige ting / ting i nærheten
[IGame](src/inf101/v18/rogue101/game/IGame.java) spesifiserer en metode `getVisible()` som skal gi deg alle locations i nærheten som du kan se. Denne er foreløpig ikke skikkelig implementert. Den kaller `map.getNeighbourhood(currentLocation, dist)`, som mangler implementasjon for dist > 0.
* **a)** Lag en enkel versjon av `getNeighbourhood()` i [GameMap](src/inf101/v18/rogue101/map/GameMap.java) som bare returnerer alle de direkte naboene til lokasjonen. ILocation har metoder som kan hjelpe deg (men du må kanskje flytte resultatet over i en liste). Med *naboer* mener vi her alle de åtte cellene som er rundt cellene i midten, *unntatt* de av naboene som er utenfor kartet (du vil uansett ikke få tak i ILocations for dissse); og med *nabolag* tenker også på naboene til naboene osv.
@ -92,20 +102,20 @@ Vi vil gjerne la brukeren kunne plukke opp og legge fra seg ting på kartet
* **c)** Du bør lage tester for dette også, slik som i B3. Du trenger et GameMap, og en ILocation på kartet.
* Du kan sjekke at resultatet fra `getNeighbourhood()` er riktig ved å gå gjennom hele listen og så sjekke at `centre.gridDistanceTo(element) <= dist`.
* Du må også teste at du får det forventede antall naboer (f.eks. så lenge du er midt inne i kartet skal du ha 8 naboer for `dist=1`, 24 for `dist=2`). Sjekk også at du får riktig antall naboer når du er ute i kanten eller hjørnene av kartet. Du bør gjerne ha tester for minst 3 scenarier.
* **d)** *(Ekstra:)* nabolagslisten er litt mer praktisk hvis de nærmeste naboenen kommer først. Det kan være din versjon allerede funker slik men i såfall bør du også teste det.
* *d)* *(Ekstra:)* nabolagslisten er litt mer praktisk hvis de nærmeste naboenen kommer først. Det kan være din versjon allerede funker slik men i såfall bør du også teste det.
* Gjør om nødvendig om på `getNeighbourhood()` slik at de nærmeste cellene kommer først i listen.
* Lag en test / juster testene over, slik at du sjekker avstanden (med `gridDistanceTo()`) til senere elementer i listen alltid er like eller større en avstanden til de tidligere elementene (du kan bruke liknende teknikk som når du testet at items var sortert etter størrelse).
Du vil kanskje ha lyst på en annen (mer fornuftig) `getVisible()` senere da kan du enten legge til en ny metode i IGame og Game, og bruke den i stedet, eller evt. kommentere ut svar et ditt på B5.b).
Du vil kanskje ha lyst på en annen (mer fornuftig) `getVisible()` senere; du kan endre denne så den bruker noe annet enn `getNeighbourhood()` (eller evt. gjør justeringer på resultatet). Du kan bruke `getVisible()`/`getNeighbourhood()` til å gjøre kaninens gulrot-leting mer effektiv, f.eks.
Hvis du vil ha *skikkelig* synlighet, f.eks. slik at aktørene ikke kan oppdage det som er bak vegger, trenger du en betydelig mer komplisert synlighetsalgoritme. Den går an å lage en veldig simplistisk synlighetstest ved hjelp av `ILocation.gridLineTo(ILocation)`, som gir deg alle locations om ligger på en linje mellom denne og en annen location men dette er ikke en del av oppgave (du kan selvfølgelig likevel prøve deg i Del C!).
Hvis du vil ha *skikkelig* synlighet, f.eks. slik at aktørene ikke kan oppdage det som er bak vegger, trenger du en [betydelig mer komplisert synlighetsalgoritme](http://ncase.me/sight-and-light/). Den går an å lage en veldig simplistisk synlighetstest ved hjelp av `ILocation.gridLineTo(ILocation)`, som gir deg alle locations om ligger på en linje mellom denne og en annen location men dette er ikke en del av oppgaven (du kan selvfølgelig likevel prøve deg i Del C!).
### Deloppgave B6: Enkelt “Angrep”
Tradisjonelle roguelikes er *veldig* opptatt av slossing (kan med rette kalles “hack-and-slash”), så vi bør ha med en eller annen slik mekanikk. Du kan selvfølgelig velge selv (i del C) hva slags betydning dette skal ha for historien i spillet (om du har en historie).
## *(5%)* Deloppgave B6: Enkelt “Angrep”
Tradisjonelle roguelikes er *veldig* opptatt av slossing (kan med rette kalles “[hack-and-slash](https://en.wikipedia.org/wiki/Hack_and_slash)”), så vi bør ha med en eller annen slik mekanikk. Du kan selvfølgelig velge selv (i del C) hva slags betydning dette skal ha for historien i spillet (om du har en historie).
Kampmekanikken er en forenkling av vanlige regler fra liknende dataspill og bord-rollespill:
* A *gjør et angrep* på B
* A har en “attack score” og B har en “defence score”
* A har en *attack score* og B har en *defence score*
* Vi tilsetter litt tilfeldighet, og hvis *attack* > *defence* har A vunnet, ellers har B vunnet.
* Hvis A vinner, blir B skadet A har en “damage score” som sier hvor mye og B sine “health points” holder rede på skade som har skjedd og hvor alvorlig den er
* Hvis B vinner, skjer det ingen ting bortsett fra at det nå antakelig er B sin tur, og B kan angripe A (eller løpe sin vei)
@ -115,24 +125,21 @@ En grei formel for angrep er f.eks.: `attack+random.nextInt(20)+1 >= defence+10`
* **a)** Du må implementere ferdig metoden `Game.attack(dir, target)` (vi har lagt den i Game-klassen, slik at den kan håndtere spillereglene uten at aktørene "jukser"). Du kan justere reglene litt etter hva du ønsker selv, men formelen over er at bra utgangspunkt.
* Når du har avgjort vinneren kan du gi en passende melding på skjermen. F.eks., `formatMessage("%s hits %s for %d damage", currentActor.getName(), target.getName(), damage);` og en tilsvarende hvis angrepet mislykkes.
* Du skal også kalle `handleDamage()` på target-objektet. Denne metoden returnerer skaden som *faktisk* skjedde det kan f.eks. brukes i tilfeller hvor forsvareren hadde en eller annen form for beskyttelse.
* **b)** Selv om Game nå kan avgjøre hva som skjer med angrep, er det foreløpig ingen som vil prøve seg på å angripe! Oppdater `Rabbit` slik at `doTurn()` metoden kaller `attack()` i stedet for `move()` (enten når det er mulig, eller når spilleren ved siden av):
* **b)** Selv om Game nå kan avgjøre hva som skjer med angrep, er det foreløpig ingen som vil prøve seg på å angrip.! Oppdater `Rabbit` slik at `doTurn()` metoden kaller `attack()` i stedet for `move()` (enten når det er mulig, eller når spilleren ved siden av):
* Du trenger å sjekke alle gyldige naboer, ikke bare de du kan gå til du er kanskje særlig interessert i å angripe andre IActors, og de befinner seg i felter du ikke kan gå inn i (ja, i prinsippet vil det også være mulig å angripe og ødelegge veggene men de har ganske mange helsepoeng!).
* I prinsippet kan du angripe et hvilket som helst IItem så lenge det er i et nabofelt (evt. kan du oppgi `GridDirection.CENTER` og angripe noe i samme felt), men du har kanskje lyst til å sjekke mot `instanceof IActor` eller `instanceof IPlayer`.
* Du må kalle `attack()`-metoden med et spesifikk IItem som mål, og med en spesifikk retning.
* I prinsippet kan du angripe et hvilket som helst `IItem` så lenge det er i et nabofelt (evt. kan du oppgi `GridDirection.CENTER` og angripe noe i samme felt), men du har kanskje lyst til å sjekke mot `instanceof IActor` eller `instanceof IPlayer`.
* Du må kalle `attack()`-metoden med et spesifikk `IItem` som mål, og med en spesifikk retning.
* En mulighet er f.eks.: gå gjennom alle nabocellene; hvis du ser en gulrot, flytt dit, hvis du ser en IPlayer, angrip og eller beveg i en tilfeldig retning.
* Prøv spillet og se hva som skjer.
* **c)** Spilleren bør også kunne angripe. En grei mekanikk for det er at når spilleren prøver å “gå på” en annen aktør, så telles det som angrip (i stedet for å resultere i en “Ouch!” (eller enda verre, IllegalMoveException)). Går helt fint å la spilleren få lov å angripe veggene også. Siden du må velge et spesifikt item å angripe, kan du f.eks. velge det første fra `game.getLocalItems()` (evt. finne noe som er en IActor hvis mulig)
* **c)** Spilleren bør også kunne angripe. En grei mekanikk for det er at når spilleren prøver å “gå på” en annen aktør, så telles det som angrip (i stedet for å resultere i en “Ouch!”, eller enda verre IllegalMoveException). Går helt fint å la spilleren få lov å angripe veggene også. Siden du må velge et spesifikt item å angripe, kan du f.eks. velge det første fra `game.getLocalItems()` (evt. finne noe som er en IActor hvis mulig).
* Prøv spillet og se hva som skjer.
*MERK:* det er forskjell om en kartcelle er “lovlig”, “lovlig og opptatt” og “ulovlig”. Hvis du prøver å gå `NORTH` fra (0,0) vil du f.eks. havne utenfor kartet, så dette er en ulovlig celle (egentlig ikke en celle i det hele tatt). Hvis du antar at du har en location `loc` (f.eks. din nåværende plassering fra `game.getLocation()`):
* Hvis du gjør `loc.canGo(GridDirection.NORTH)`, så får du vite om det finnes en lovlig location nord for nåværende location; tilsvarende med `game.getMap().hasNeighbour(loc, GridDirection.NORTH)`. Det kan likevel godt være at du ikke får lov til å gjøre `game.move(GridDirection.NORTH)` likevel, f.eks. fordi det er en vegg der.
* Hvis du gjør `loc.canGo(GridDirection.NORTH)`, så får du vite om det finnes en lovlig location nord for nåværende location; tilsvarende med `game.getMap().hasNeighbour(loc, GridDirection.NORTH)`. Det kan likevel godt være at du ikke får lov til å gjøre `game.move(GridDirection.NORTH)`, f.eks. fordi det er en vegg der.
* Hvis du gjør `game.canGo(GridDirection.NORTH)` eller `map.canGo(GridDirection.NORTH)` får du vite om det går an å gå nordover, altså at naboen i nord ikke er ulovlig og ikke er opptatt (av en vegg eller en aktør). Litt forskjellige deler av systemet har altså litt forskjellig oppfatning av “canGo” og om man skal sjekke om ting er opptatt eller ikke.
* For angrep er du antakelig særlig interessert i feltene som er lovlige men opptatte.
i Rabbit, Player og Game.attack()
### Deloppgave B7: Spørsmål
## *(5%)* Deloppgave B7: Spørsmål
* **a)** Du har måttet gjøre en del arbeid med nabo-celler og slikt. Håndterer du dette på en annen måte enn vi gjorde i labbene (f.eks. cell-automatene og labyrinten)? Hva synes du er mest praktisk?
* **b)** Hvorfor går de fleste av spill-"trekkene" (slik som at noen flytter seg, plukker en ting, legger ned en ting, angriper naboen, etc.) gjennom Game? Kan du se for deg fordeler / ulemper ved dette?
@ -141,5 +148,8 @@ En grei formel for angrep er f.eks.: `attack+random.nextInt(20)+1 >= defence+10`
* **d)** Tenker du annerledes om noen av spørsmålene fra Del A nå?
## *(5%)* Deloppgave B*n*
# Gå videre til [**DEL C**](SEM-1DEL-C.md)
*Ca. 5% for generell ryddighet og kvalitet.*
# Gå videre til [**DEL C**](SEM-1_DEL-C.md)

View File

@ -1,21 +1,64 @@
# [Semesteroppgave 1: “Rogue One oh one”](https://retting.ii.uib.no/inf101.v18.sem1/blob/master/SEM-1_DEL-C.md) Del A: Fri utfoldelse
# [Semesteroppgave 1: “Rogue One oh one”](https://retting.ii.uib.no/inf101.v18.sem1/blob/master/SEM-1_DEL-C.md) Del C: Videreutvikling
* [Oversikt](SEM-1.md)
* [Praktisk informasjon](SEM-1.md#praktisk-informasjon)
* [Del A: Bakgrunn, modellering og utforskning](SEM-1_DEL-A.md)
* [Del B: Gjør ferdig nødvendige komponenter](SEM-1_DEL-B.md)
* [Del C: Selvvalgt del](SEM-1_DEL-C.md)
* [Praktisk informasjon 5%](SEM-1.md#praktisk-informasjon)
* [Del A: Bakgrunn, modellering og utforskning 15%](SEM-1_DEL-A.md)
* [Del B: Fullfør basisimplementasjonen 40%](SEM-1_DEL-B.md)
* **Del C: Videreutvikling 40%**
## Videreutvikling av Rogue-101
Vi overlater nå ansvaret for utviklingen til deg du finner noen forslag under, men du må selv bestemme hva mer som skal legges til av funksjonalitetet, og hva du evt. vil gjøre for å gi spillet litt mer “flavour” (grafikk, kanskje?).
Vi overlater nå ansvaret for utviklingen til deg du finner noen forslag under som du kan jobbe ut ifra, eller så kan du finne på din egen utvikdelse av koden. For å få maks poengsum på oppgaven må du gjøre to av forslagene, eventuelt erstatte ett eller begge med noe du kommer på selv, som er tilsvarende stort i omfang.
Som nevnt tidligere kan du velge “setting” og “storyline” som du vil du trenger ikke å lage huleutforskning med magi, sverd, orker og hobbiter eller med kaniner og gulrøtter. Skriv ned forklaring til det du gjør i README.md både funksjonalitet som du legger til, og kreative ting du finner på vi legger ikke nødvendigvis merke til alt når vi prøvekjører ting, så det er greit å vite hva vi skal se etter.
Når du nå er ferdig med Del A og B skal du ha et dungeon crawler (evt. rabbit hopping) spill med en spiller som kan
* bevege seg rundt på kartet
* se synlige ting rundt seg
* plukke opp ting
* bære én ting
* legge fra seg ting
* angripe
Du har også gulrøtter som kan plukkes opp, og kaniner som kan spise gulrøtter og angripe ting.
### Styling
Herfra kan du enten fortsette å legge på funksjonalitet på koden du har, eller brette opp ermene og lage ditt eget spill med andre klasser enn de vi har gitt her det kanskje mest aktuelt å bytte ut item-klassene. Du kan gjerne skrive en “intro” til spillet, og “flavour” i `displayMessage`. Uansett om du vil bruke klassene vi har gitt deg eller lage dine egne, må du huske å levere klassene du jobbet med i del A og B.
Du kan kommet et godt stykke på vei med litt kreativ bruk av tekst-symbolene. For enkel blokk-grafikk (til vegger, f.eks.) så finnes det en del forskjellige tegn du kan bruke i [BlocksAndBoxes](inf101/v18/gfx/textmode/BlocksAndBoxes.java).
Du må selv bestemme hva mer som skal legges til av funksjonalitetet, og hva du evt. vil gjøre for å gi spillet litt mer stemning (grafikk, kanskje?). Som nevnt tidligere kan du velge “setting” og “storyline” som du vil du trenger ikke å lage huleutforskning med magi, sverd, orker og hobbiter eller med kaniner og gulrøtter. Skriv ned forklaring til det du gjør i README.md både funksjonalitet som du legger til, og kreative ting du finner på vi legger ikke nødvendigvis merke til alt når vi prøvekjører ting, så det er greit å vite hva vi skal se etter.
Siden vi ikke vet hva slags lure ting dere kommer på å implementere, skriver vi de følgende forslagene ut ifra koden vi har gitt dere. Du står fritt til å i stedet lage tilsvarende funksjonalitet for klasser i spillet du lager selv. Når vi skriver “spilleren”, kan det altså være at du vil bruke en annen klasse enn `Player`-klassen, eller at du vil gi denne funksjonaliteten til en `INonPlayer`.
Følgende er forslag til hva du kan gjøre, ikke nødvendigvis i den rekkefølgen.
### C1: Ting/items som påvirker spillet
Spilleren vår vil gjerne kunne finne andre ting i labyrinten enn gulrøtter. I denne delen kan du lage flere ting, for eksempel noe som gjør spilleren flinkere til å angripe, eller øker helsen den har. Du kan lage “healings potions” som øker helsepoengene til spilleren. Dersom du vil at spilleren skal kunne slåss bedre, kan du lage våpen, for eksempel av typen langkost. Dersom du angriper en kanin med en langkost gir det deg kanskje en høyere attack score enn uten. (Du kan selv velge hvordan type ting påvirker angrep, helsepoeng, og kanskje også hverandre).
Det kan være at tingen tar effekt når du plukker den opp, eller kanskje spilleren må trykke på en tast (klassiske roguelikes bruker gjerne `q` for “quaff a potion” eller `w` for “wield a weapon”).
For å implementere disse tingene må du lage klasser for dem og extende IItem. De vil likne på gulrot-klassen. Du må selv finne ut hvordan de skal tegnes. Se styling-seksjonen for tips til grafikk.
### C2: Inventory - bærenett
Dersom spilleren kan finne flere forskjellige typer ting i labyrinten, er det kjekt å kunne bære mer enn én ting av gangen. Hvis labyrinten enda bare har gulrøtter, vil spilleren kanskje kunne samle mange av dem, og bære dem med seg og alt du trenger å holde rede på er antall gulrøtter. Men for varierende typer ting, trenger vi en bedre løsning.
I objektorientering er vi opptatt av abstraksjon og forståelig kode, så selv om vi kunne ha latt spilleren få flere feltvariabler for å holde styr på alle tingene den bærer, så vil vi heller implementere en egen klasse for en *samling* (eller Collection) av ting. Java har standard lister, men her trenger vi kanskje noe litt annet: det bør jo gjerne ha en begrensning på hvor mye man kan bære med seg.
* Samlingen bør lagre `IItem`-objekter, eventuelt at den er generisk med `<T extends IItem>`.
* Du kan velge om begrensningen er på antall elementer, eller på total størrelse (`getSize()`) for alle elementene i samlingen.
* Du må ha metoder for å putte noe inn i samlingen, hente noe ut, sjekke om det er ledig plass, osv. Du trenger ikke ha indekser det holder å kunne putte inn og hente ut.
* Hvis noen prøver å putte noe inn i en full samling (eller noe det ikke er plass til i samlingen) bør du kaste en exception.
For tips til implementasjon av samling kan du se på [IList/MyList-listene fra tidligere labber](https://retting.ii.uib.no/inf101.v18.oppgaver/inf101.v18.lab4/tree/master/src/inf101/v18/datastructures). Du kan gjerne bruke Java sitt [standard-bibliotek for samlinger](https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html) i implementasjonen (legg merke til at du trenger en konkret implementasjon av interfacet, for eksempel en [ArrayList](https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html)).
* For litt mer solid INF101-design, bør du gjerne designe et eget grensesnitt, f.eks. `IContainer<T extends IItem>` da kan du også ha flere varianter av samlinger (som f.eks. virker på forskjellig måte).
* Spilleren (og kaninene) kan bruke samlingen du har laget som bærenett eller ryggsekk til å lagre items som blir plukket opp. Brukeren vil antakelig ha lyst til å ha oversikt over hva som er i “sekken” det er ledig plass på skjermen hvor du kan vise mer informasjon se på metodene `getPrinter()` (du kan skrive ting på skjermen med `printAt(x,y, text)`), `getFreeTextAreaBounds()` og `clearFreeTextArea()`.
* En mer avansert og artig bruk av `IContainer` er å la den også utvide `IItem`. Da kan du putte ting i sekken din, og legge sekken på bakken eller putte en kanin inn i en hatt inn i en sekk inn i en koffert og plukke opp og sette fra deg kofferten.
### C3 Styling
Du kan kommet et godt stykke på vei med litt kreativ bruk av tekst-symbolene. For enkel blokk-grafikk (til vegger, f.eks.) så finnes det en del forskjellige tegn du kan bruke i [BlocksAndBoxes](inf101/v18/gfx/textmode/BlocksAndBoxes.java). Hvis du implementer `getPrintSymbol()` så kan du bruke et eget tegn til grafikk-visningen (hvis du bruker bokstaven i `getSymbol()` til noe fancy fabrikk, f.eks.).
#### Fancy vegger
*(Du kan selvfølgelig gjøre tilsvarende med andre ting enn vegger)*
F.eks. bruker `Wall`-objektene `BlocksAndBoxes.BLOCK_FULL` som symbol. Det kan være litt tricky å få `Wall` til å variere `getSymbol()` avhengig av hvor den har andre vegger som naboer, men du kan i prinsippet lage flere varianter av wall:
* Her er det mest praktisk å bruk *arv*, f.eks.:
@ -34,13 +77,41 @@ public class DiagonalWall extends Wall {
* Du trenger også å legge til alle vegg-variantene i `createItem`-metoden.
* Du trenger at alle vegger har `Wall`-typen, fordi kartet bruker det til å se om et felt er opptatt (bla gjennom items på en lokasjon, sjekk om `item instanceof Wall`). Når du sier at `DiagonalWall extends Wall`, så vi alle diagonale vegger også telle som vanlige vekker.
* Alternativet ville vært å enten legge til en metode `isWall()` i `IItem`; eller si at en kartcelle er opptatt hvis det er en veldig stor ting der (`getSize()` større enn 100 eller 1000, f.eks.); eller lage et ekstra grensesnitt `IWall` (og både `Wall` og `DiagonalWall` `implements IWall`) og bruke det istedenfor der hvor man trenger å sjekke etter vegger.
En annen mulighet for fancy vegg er å la veggene tilpasse seg omgivelsene. Det krever litt samarbeid mellom Game og Wall f.eks. kan du finne alle veggene (enten i `beginTurn()` eller når du setter opp kartet) og kalle en ny metode som du lager (f.eks. `setup(IGame game)`). Veggen kan så utforske naboene sine, og finne ut hvilke av dem som er vegger, og velge riktig box-tegn enten en av `BLOCK_*`-tegnene fra `BlocksAndBoxes`, eller med [Box-drawing characters](https://en.wikipedia.org/wiki/Box-drawing_character) (disse var mye brukt på gamle DOS-datamaskiner, men det er nå en standard del av Unicode-tegnsettet, og `Printer`-klassen støtter dem uavhengig av hvilken font du bruker.). Du kan paste box-tegn (og block-tegn) rett inn i koden din (så lenge [Eclipse er riktig satt opp med UTF-8](https://retting.ii.uib.no/inf101/inf101.v18/wikis/oppsett#fiks-for-tegnsett)), eller bruke [`\uXXXX` escape-koder](https://docs.oracle.com/javase/tutorial/java/data/characters.html) i strengen. F.eks., `"╣"` er `"\u2563"`.
#### Emojis
Hvis du bytter font vil du kunne bruke en haug med praktiske symboler. Du kan sette fonten i `start()` metoden i [Main](inf101/v18/rogue101/Main.java)-klassen. “Symbola” inneholder standard [Unicode emojis](https://en.wikipedia.org/wiki/Emoji#Unicode_blocks) i tillegg til vanlige bokstaver. Du kan [laste den ned herfra](http://users.teilar.gr/~g1951d/).
* Hvis du putter `Symbola.ttf` i `src/inf101/v18/gfx/fonts/`, skal du kunne kjøre `printer.setFont(Printer.FONT_SYMBOLA);` i `start()`, og så bruke f.eks. `"☺️"` (`"\u263a"`) som symbol for spilleren.
* Merk at en del mer obskure Unicode-emojis, som [`"🦆"`](https://en.wikipedia.org/wiki/%F0%9F%A6%86) må skrives med to escape koder (`"\ud83e\udd86"`) og er ikke nødvendigvis med i fonten du bruker.
* Vanlige bokstaver ser dessverre ikke så veldig fine ut i Symbola-fonten, siden grafikken vår baserer seg på monospaced tekst (det går foreløpig ikke an å bruke mer enn én font i systemet).
* Hvis du prøver deg med andre fonter (som du selvfølgelig må oppgi kilde til og ha rettigheter til å bruke og finne ut hvordan du setter de opp med `Printer`), vil de gjerne ikke passe så veldig godt på skjermen. Det går an å justere størrelse og posisjon på bokstavene med `inf101.v18.gfx.textmode.TextFondAdjuster`.
#### Farger
Du kan lage farger med [ANSI escape-koder](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors). For eksempel `"\u001b[31m" + "@" + "\u001b[0m"` for å lage et rødt @-tegn. `"\u001b[31m"` velger rød tekstfarge og `"\u001b[0m"` skifter tilbake til standard (hvis du ikke har med den så blir all teksten rød).
#### Skilpaddegrafikk
Alternativet til tekstgrafikken er å bruke `ITurtle`/`TurtlePainter`, som du har sett litt i bruk på forelesningene for å lage frosker og ender. Gulroten er tegnet slik hvis du implementerer `draw()`-metoden for et item, og lar den returnere `true` blir teksten ikke tegnet. Du kan se på gulrot-eksempelet, og på [koden fra forelesningene](https://retting.ii.uib.no/inf101/inf101.v18/wikis/kode-fra-forelesninger) for eksempler.
* Les mer om [skilpaddegrafikk her](https://en.wikipedia.org/wiki/Turtle_graphics)
* Draw metoden får bredden og høyden til kartcellen som parametre. Som standard er ting laget til slik at bredde og høyde er 32 selv om skjermen viser smale tegn (16x32). Hvis du trenger kontroll over bredden, is stedet for at tegningen blir “skvist” til halv bredde, kan du sette `Main.MAP_AUTO_SCALE_ITEM_DRAW` til `false`.
* Foreløpig støtter grafikksystemet ikke at du kan bruke egne bilder (i jpg eller png filer, f.eks.) men det er mulig vi kan legge til dette etterhvert.
### C5: Meldingsvindu
* lage meldings"vindu"
### C6: Win condition
Det går foreløpig ikke an å vinne spillet her må du eventuelt være kreativ selv. F.eks. at spilleren vinner når alle gulrøttene er samlet i det ene hjørnet eller når spilleren er alene igjen eller noe helt annet.
### C?: Noe du finner på / noe annet vi finner på
* Du står fritt til å finne på ting selv; og det kan også være vi legger ut litt flere ideer underveis.
# Diverse
## Åpne kilder til grafikk / lyd / media
*Foreløpig støtter grafikksystemet ikke at du kan bruke egne bilder (i jpg eller png filer, f.eks.) men det er mulig vi kan legge til dette etterhvert. Du kan uansett tegne med skilpaddegrafikken (TurtlePainter). Det ligger heller ikke med kode for å spille av lyd men det kan være vi har noe slikt på lur til når du har kommet skikkelig i gang med ting.*
* Om du ikke er flink til å tegne selv, kan du finne glimrende grafikk på [OpenGameArt](http://opengameart.org/) **husk å skrive i oversiktsdokumentet hvor du har fått grafikken fra** (webside, opphavsperson, copyright-lisens om du bruker OpenGameArt, finner du opplysningene i *License(s)* og *Copyright/Attribution Notice*).
* [Wikimedia Commons](https://commons.wikimedia.org/wiki/Main_Page) har en god del bilder og andre mediafiler tilgjengelig du får til og med en “You need to attribute this author show me how” instruks når du laster ned ting.
@ -49,3 +120,4 @@ public class DiagonalWall extends Wall {
# Gå videre til [**DEL D**](SEM-1.md#praktisk-informasjon)

View File

@ -9,11 +9,6 @@ public interface IPaintLayer {
*/
void clear();
/**
* Send this layer to the front, so it will be drawn on top of any other layers.
*/
void layerToFront();
/**
* Send this layer to the back, so it will be drawn behind any other layers.
*
@ -24,4 +19,10 @@ public interface IPaintLayer {
* {@link Screen#getBackgroundContext()}.
*/
void layerToBack();
/**
* Send this layer to the front, so it will be drawn on top of any other layers.
*/
void layerToFront();
}

View File

@ -108,330 +108,15 @@ public class Screen {
public static final int CONFIG_FLAG_NO_AUTOHIDE_MOUSE = 2 << CONFIG_FLAG_SHIFT;
public static final int CONFIG_FLAG_DEBUG = 4 << CONFIG_FLAG_SHIFT;
private static final int CONFIG_FLAG_MASK = 7;
private final double rawCanvasWidth;
private final double rawCanvasHeight;
private boolean logKeyEvents = false;
private final SubScene subScene;
private final List<Canvas> canvases = new ArrayList<>();
private final Map<IPaintLayer, Canvas> layerCanvases = new IdentityHashMap<>();
private final Canvas background;
private final Group root;
private Paint bgColor = Color.CORNFLOWERBLUE;
private int aspect = 0;
private double scaling = 0;
private double currentScale = 1.0;
private double currentFit = 1.0;
private double resolutionScale = 1.0;
private int maxScale = 1;
private Predicate<KeyEvent> keyOverride = null;
private Predicate<KeyEvent> keyPressedHandler = null;
private Predicate<KeyEvent> keyTypedHandler = null;
private Predicate<KeyEvent> keyReleasedHandler = null;
private boolean debug = true;
private List<Double> aspects;
private boolean hideFullScreenMouseCursor = true;
private Cursor oldCursor;
/** @return the keyTypedHandler */
public Predicate<KeyEvent> getKeyTypedHandler() {
return keyTypedHandler;
}
/**
* @param keyTypedHandler
* the keyTypedHandler to set
*/
public void setKeyTypedHandler(Predicate<KeyEvent> keyTypedHandler) {
this.keyTypedHandler = keyTypedHandler;
}
/** @return the keyReleasedHandler */
public Predicate<KeyEvent> getKeyReleasedHandler() {
return keyReleasedHandler;
}
/**
* @param keyReleasedHandler
* the keyReleasedHandler to set
*/
public void setKeyReleasedHandler(Predicate<KeyEvent> keyReleasedHandler) {
this.keyReleasedHandler = keyReleasedHandler;
}
/** @return the keyOverride */
public Predicate<KeyEvent> getKeyOverride() {
return keyOverride;
}
/**
* @param keyOverride
* the keyOverride to set
*/
public void setKeyOverride(Predicate<KeyEvent> keyOverride) {
this.keyOverride = keyOverride;
}
/** @return the keyHandler */
public Predicate<KeyEvent> getKeyPressedHandler() {
return keyPressedHandler;
}
/**
* @param keyHandler
* the keyHandler to set
*/
public void setKeyPressedHandler(Predicate<KeyEvent> keyHandler) {
this.keyPressedHandler = keyHandler;
}
public Screen(double width, double height, double pixWidth, double pixHeight, double canvasWidth,
double canvasHeight) {
root = new Group();
subScene = new SubScene(root, Math.floor(width), Math.floor(height));
resolutionScale = pixWidth / canvasWidth;
this.rawCanvasWidth = Math.floor(pixWidth);
this.rawCanvasHeight = Math.floor(pixHeight);
double aspectRatio = width / height;
aspect = 0;
for (double a : STD_ASPECTS)
if (Math.abs(aspectRatio - a) < 0.01) {
break;
} else {
aspect++;
}
aspects = new ArrayList<>(STD_ASPECTS);
if (aspect >= STD_ASPECTS.size()) {
aspects.add(aspectRatio);
}
background = new Canvas(rawCanvasWidth, rawCanvasHeight);
background.getGraphicsContext2D().scale(resolutionScale, resolutionScale);
setBackground(bgColor);
clearBackground();
root.getChildren().add(background);
subScene.layoutBoundsProperty()
.addListener((ObservableValue<? extends Bounds> observable, Bounds oldBounds, Bounds bounds) -> {
recomputeLayout(false);
});
}
public void clearBackground() {
getBackgroundContext().setFill(bgColor);
getBackgroundContext().fillRect(0.0, 0.0, background.getWidth(), background.getHeight());
}
public void cycleAspect() {
aspect = (aspect + 1) % aspects.size();
recomputeLayout(false);
}
public void zoomCycle() {
scaling++;
if (scaling > maxScale)
scaling = ((int) scaling) % maxScale;
recomputeLayout(true);
}
public void zoomIn() {
scaling = Math.min(10, currentScale + 0.2);
recomputeLayout(false);
}
public void zoomOut() {
scaling = Math.max(0.1, currentScale - 0.2);
recomputeLayout(false);
}
public void zoomFit() {
scaling = 0;
recomputeLayout(false);
}
public void zoomOne() {
scaling = 1;
recomputeLayout(false);
}
public void fitScaling() {
scaling = 0;
recomputeLayout(true);
}
public int getAspect() {
return aspect;
}
public GraphicsContext getBackgroundContext() {
return background.getGraphicsContext2D();
}
public TurtlePainter createPainter() {
Canvas canvas = new Canvas(rawCanvasWidth, rawCanvasHeight);
canvas.getGraphicsContext2D().scale(resolutionScale, resolutionScale);
canvases.add(canvas);
root.getChildren().add(canvas);
return new TurtlePainter(this, canvas);
}
public Printer createPrinter() {
Canvas canvas = new Canvas(rawCanvasWidth, rawCanvasHeight);
canvas.getGraphicsContext2D().scale(resolutionScale, resolutionScale);
canvases.add(canvas);
root.getChildren().add(canvas);
return new Printer(this, canvas);
}
private void recomputeLayout(boolean resizeWindow) {
double xScale = subScene.getWidth() / getRawWidth();
double yScale = subScene.getHeight() / getRawHeight();
double xMaxScale = getDisplayWidth() / getRawWidth();
double yMaxScale = getDisplayHeight() / getRawHeight();
currentFit = Math.min(xScale, yScale);
maxScale = (int) Math.max(1, Math.ceil(Math.min(xMaxScale, yMaxScale)));
currentScale = scaling == 0 ? currentFit : scaling;
if (resizeWindow) {
Scene scene = subScene.getScene();
Window window = scene.getWindow();
double hBorder = window.getWidth() - scene.getWidth();
double vBorder = window.getHeight() - scene.getHeight();
double myWidth = getRawWidth() * currentScale;
double myHeight = getRawHeight() * currentScale;
if (debug)
System.out.printf(
"Resizing before: screen: %1.0fx%1.0f, screen: %1.0fx%1.0f, scene: %1.0fx%1.0f, window: %1.0fx%1.0f,%n border: %1.0fx%1.0f, new window size: %1.0fx%1.0f, canvas size: %1.0fx%1.0f%n", //
javafx.stage.Screen.getPrimary().getVisualBounds().getWidth(),
javafx.stage.Screen.getPrimary().getVisualBounds().getHeight(), subScene.getWidth(),
subScene.getHeight(), scene.getWidth(), scene.getHeight(), window.getWidth(),
window.getHeight(), hBorder, vBorder, myWidth, myHeight, getRawWidth(), getRawHeight());
// this.setWidth(myWidth);
// this.setHeight(myHeight);
window.setWidth(myWidth + hBorder);
window.setHeight(myHeight + vBorder);
if (debug)
System.out.printf(
"Resizing after : screen: %1.0fx%1.0f, screen: %1.0fx%1.0f, scene: %1.0fx%1.0f, window: %1.0fx%1.0f,%n border: %1.0fx%1.0f, new window size: %1.0fx%1.0f, canvas size: %1.0fx%1.0f%n",
javafx.stage.Screen.getPrimary().getVisualBounds().getWidth(),
javafx.stage.Screen.getPrimary().getVisualBounds().getHeight(), subScene.getWidth(),
subScene.getHeight(), scene.getWidth(), scene.getHeight(), window.getWidth(),
window.getHeight(), hBorder, vBorder, myWidth, myHeight, getRawWidth(), getRawHeight());
}
if (debug)
System.out.printf("Rescaling: subscene %1.2fx%1.2f, scale %1.2f, aspect %.4f (%d), canvas %1.0fx%1.0f%n",
subScene.getWidth(), subScene.getHeight(), currentScale, aspects.get(aspect), aspect, getRawWidth(),
getRawHeight());
for (Node n : root.getChildren()) {
n.relocate(Math.floor(subScene.getWidth() / 2),
Math.floor(subScene.getHeight() / 2 + (rawCanvasHeight - getRawHeight()) * currentScale / 2));
n.setTranslateX(-Math.floor(rawCanvasWidth / 2));
n.setTranslateY(-Math.floor(rawCanvasHeight / 2));
if (debug)
System.out.printf(" * layout %1.2fx%1.2f, translate %1.2fx%1.2f%n", n.getLayoutX(), n.getLayoutY(),
n.getTranslateX(), n.getTranslateY());
n.setScaleX(currentScale);
n.setScaleY(currentScale);
}
}
public void setAspect(int aspect) {
this.aspect = (aspect) % aspects.size();
recomputeLayout(false);
}
public void setBackground(Paint bgColor) {
this.bgColor = bgColor;
subScene.setFill(bgColor instanceof Color ? ((Color) bgColor).darker() : bgColor);
}
public boolean minimalKeyHandler(KeyEvent event) {
KeyCode code = event.getCode();
if (event.isShortcutDown()) {
if (code == KeyCode.Q) {
System.exit(0);
} else if (code == KeyCode.PLUS) {
zoomIn();
return true;
} else if (code == KeyCode.MINUS) {
zoomOut();
return true;
}
} else if (!(event.isAltDown() || event.isControlDown() || event.isMetaDown() || event.isShiftDown())) {
if (code == KeyCode.F11) {
setFullScreen(!isFullScreen());
return true;
}
}
return false;
}
public boolean isFullScreen() {
Window window = subScene.getScene().getWindow();
if (window instanceof Stage)
return ((Stage) window).isFullScreen();
else
return false;
}
public void setFullScreen(boolean fullScreen) {
Window window = subScene.getScene().getWindow();
if (window instanceof Stage) {
((Stage) window).setFullScreenExitHint("");
((Stage) window).setFullScreen(fullScreen);
if (hideFullScreenMouseCursor) {
if (fullScreen) {
oldCursor = subScene.getScene().getCursor();
subScene.getScene().setCursor(Cursor.NONE);
} else if (oldCursor != null) {
subScene.getScene().setCursor(oldCursor);
oldCursor = null;
} else {
subScene.getScene().setCursor(Cursor.DEFAULT);
}
}
}
}
/**
* Get the native physical width of the screen, in pixels.
* Get the resolution of this screen, in DPI (pixels per inch).
*
* <p>
* This will not include such things as toolbars, menus and such (on a desktop),
* or take pixel density into account (e.g., on high resolution mobile devices).
*
* @return Raw width of the display
* @see javafx.stage.Screen#getBounds()
* @return The primary display's DPI
* @see javafx.stage.Screen#getDpi()
*/
public static double getRawDisplayWidth() {
return javafx.stage.Screen.getPrimary().getBounds().getWidth();
}
/**
* Get the native physical height of the screen, in pixels.
*
* <p>
* This will not include such things as toolbars, menus and such (on a desktop),
* or take pixel density into account (e.g., on high resolution mobile devices).
*
* @return Raw width of the display
* @see javafx.stage.Screen#getBounds()
*/
public static double getRawDisplayHeight() {
return javafx.stage.Screen.getPrimary().getBounds().getHeight();
}
/**
* Get the width of the display, in pixels.
*
* <p>
* This takes into account such things as toolbars, menus and such (on a
* desktop), and pixel density (e.g., on high resolution mobile devices).
*
* @return Width of the display
* @see javafx.stage.Screen#getVisualBounds()
*/
public static double getDisplayWidth() {
return javafx.stage.Screen.getPrimary().getVisualBounds().getWidth();
public static double getDisplayDpi() {
return javafx.stage.Screen.getPrimary().getDpi();
}
/**
@ -449,13 +134,45 @@ public class Screen {
}
/**
* Get the resolution of this screen, in DPI (pixels per inch).
* Get the width of the display, in pixels.
*
* @return The primary display's DPI
* @see javafx.stage.Screen#getDpi()
* <p>
* This takes into account such things as toolbars, menus and such (on a
* desktop), and pixel density (e.g., on high resolution mobile devices).
*
* @return Width of the display
* @see javafx.stage.Screen#getVisualBounds()
*/
public static double getDisplayDpi() {
return javafx.stage.Screen.getPrimary().getDpi();
public static double getDisplayWidth() {
return javafx.stage.Screen.getPrimary().getVisualBounds().getWidth();
}
/**
* Get the native physical height of the screen, in pixels.
*
* <p>
* This will not include such things as toolbars, menus and such (on a desktop),
* or take pixel density into account (e.g., on high resolution mobile devices).
*
* @return Raw width of the display
* @see javafx.stage.Screen#getBounds()
*/
public static double getRawDisplayHeight() {
return javafx.stage.Screen.getPrimary().getBounds().getHeight();
}
/**
* Get the native physical width of the screen, in pixels.
*
* <p>
* This will not include such things as toolbars, menus and such (on a desktop),
* or take pixel density into account (e.g., on high resolution mobile devices).
*
* @return Raw width of the display
* @see javafx.stage.Screen#getBounds()
*/
public static double getRawDisplayWidth() {
return javafx.stage.Screen.getPrimary().getBounds().getWidth();
}
/**
@ -607,27 +324,174 @@ public class Screen {
return pScene;
}
public double getRawWidth() {
return rawCanvasWidth;
private final double rawCanvasWidth;
private final double rawCanvasHeight;
private boolean logKeyEvents = false;
private final SubScene subScene;
private final List<Canvas> canvases = new ArrayList<>();
private final Map<IPaintLayer, Canvas> layerCanvases = new IdentityHashMap<>();
private final Canvas background;
private final Group root;
private Paint bgColor = Color.CORNFLOWERBLUE;
private int aspect = 0;
private double scaling = 0;
private double currentScale = 1.0;
private double currentFit = 1.0;
private double resolutionScale = 1.0;
private int maxScale = 1;
private Predicate<KeyEvent> keyOverride = null;
private Predicate<KeyEvent> keyPressedHandler = null;
private Predicate<KeyEvent> keyTypedHandler = null;
private Predicate<KeyEvent> keyReleasedHandler = null;
private boolean debug = true;
private List<Double> aspects;
private boolean hideFullScreenMouseCursor = true;
private Cursor oldCursor;
public Screen(double width, double height, double pixWidth, double pixHeight, double canvasWidth,
double canvasHeight) {
root = new Group();
subScene = new SubScene(root, Math.floor(width), Math.floor(height));
resolutionScale = pixWidth / canvasWidth;
this.rawCanvasWidth = Math.floor(pixWidth);
this.rawCanvasHeight = Math.floor(pixHeight);
double aspectRatio = width / height;
aspect = 0;
for (double a : STD_ASPECTS)
if (Math.abs(aspectRatio - a) < 0.01) {
break;
} else {
aspect++;
}
aspects = new ArrayList<>(STD_ASPECTS);
if (aspect >= STD_ASPECTS.size()) {
aspects.add(aspectRatio);
}
background = new Canvas(rawCanvasWidth, rawCanvasHeight);
background.getGraphicsContext2D().scale(resolutionScale, resolutionScale);
setBackground(bgColor);
clearBackground();
root.getChildren().add(background);
subScene.layoutBoundsProperty()
.addListener((ObservableValue<? extends Bounds> observable, Bounds oldBounds, Bounds bounds) -> {
recomputeLayout(false);
});
}
public double getRawHeight() {
return Math.floor(rawCanvasWidth / aspects.get(aspect));
public void clearBackground() {
getBackgroundContext().setFill(bgColor);
getBackgroundContext().fillRect(0.0, 0.0, background.getWidth(), background.getHeight());
}
public double getWidth() {
return Math.floor(getRawWidth() / resolutionScale);
public TurtlePainter createPainter() {
Canvas canvas = new Canvas(rawCanvasWidth, rawCanvasHeight);
canvas.getGraphicsContext2D().scale(resolutionScale, resolutionScale);
canvases.add(canvas);
root.getChildren().add(canvas);
return new TurtlePainter(this, canvas);
}
public Printer createPrinter() {
Canvas canvas = new Canvas(rawCanvasWidth, rawCanvasHeight);
canvas.getGraphicsContext2D().scale(resolutionScale, resolutionScale);
canvases.add(canvas);
root.getChildren().add(canvas);
return new Printer(this, canvas);
}
public void cycleAspect() {
aspect = (aspect + 1) % aspects.size();
recomputeLayout(false);
}
public void fitScaling() {
scaling = 0;
recomputeLayout(true);
}
public int getAspect() {
return aspect;
}
public GraphicsContext getBackgroundContext() {
return background.getGraphicsContext2D();
}
public double getHeight() {
return Math.floor(getRawHeight() / resolutionScale);
}
public void moveToFront(IPaintLayer layer) {
Canvas canvas = layerCanvases.get(layer);
if (canvas != null) {
canvas.toFront();
/** @return the keyOverride */
public Predicate<KeyEvent> getKeyOverride() {
return keyOverride;
}
/** @return the keyHandler */
public Predicate<KeyEvent> getKeyPressedHandler() {
return keyPressedHandler;
}
/** @return the keyReleasedHandler */
public Predicate<KeyEvent> getKeyReleasedHandler() {
return keyReleasedHandler;
}
/** @return the keyTypedHandler */
public Predicate<KeyEvent> getKeyTypedHandler() {
return keyTypedHandler;
}
public double getRawHeight() {
return Math.floor(rawCanvasWidth / aspects.get(aspect));
}
public double getRawWidth() {
return rawCanvasWidth;
}
public double getWidth() {
return Math.floor(getRawWidth() / resolutionScale);
}
public void hideMouseCursor() {
subScene.getScene().setCursor(Cursor.NONE);
}
public boolean isFullScreen() {
Window window = subScene.getScene().getWindow();
if (window instanceof Stage)
return ((Stage) window).isFullScreen();
else
return false;
}
public boolean minimalKeyHandler(KeyEvent event) {
KeyCode code = event.getCode();
if (event.isShortcutDown()) {
if (code == KeyCode.Q) {
System.exit(0);
} else if (code == KeyCode.PLUS) {
zoomIn();
return true;
} else if (code == KeyCode.MINUS) {
zoomOut();
return true;
}
} else if (!(event.isAltDown() || event.isControlDown() || event.isMetaDown() || event.isShiftDown())) {
if (code == KeyCode.F11) {
setFullScreen(!isFullScreen());
return true;
}
}
return false;
}
public void moveToBack(IPaintLayer layer) {
@ -638,16 +502,93 @@ public class Screen {
}
}
public void hideMouseCursor() {
subScene.getScene().setCursor(Cursor.NONE);
public void moveToFront(IPaintLayer layer) {
Canvas canvas = layerCanvases.get(layer);
if (canvas != null) {
canvas.toFront();
}
}
public void showMouseCursor() {
subScene.getScene().setCursor(Cursor.DEFAULT);
private void recomputeLayout(boolean resizeWindow) {
double xScale = subScene.getWidth() / getRawWidth();
double yScale = subScene.getHeight() / getRawHeight();
double xMaxScale = getDisplayWidth() / getRawWidth();
double yMaxScale = getDisplayHeight() / getRawHeight();
currentFit = Math.min(xScale, yScale);
maxScale = (int) Math.max(1, Math.ceil(Math.min(xMaxScale, yMaxScale)));
currentScale = scaling == 0 ? currentFit : scaling;
if (resizeWindow) {
Scene scene = subScene.getScene();
Window window = scene.getWindow();
double hBorder = window.getWidth() - scene.getWidth();
double vBorder = window.getHeight() - scene.getHeight();
double myWidth = getRawWidth() * currentScale;
double myHeight = getRawHeight() * currentScale;
if (debug)
System.out.printf(
"Resizing before: screen: %1.0fx%1.0f, screen: %1.0fx%1.0f, scene: %1.0fx%1.0f, window: %1.0fx%1.0f,%n border: %1.0fx%1.0f, new window size: %1.0fx%1.0f, canvas size: %1.0fx%1.0f%n", //
javafx.stage.Screen.getPrimary().getVisualBounds().getWidth(),
javafx.stage.Screen.getPrimary().getVisualBounds().getHeight(), subScene.getWidth(),
subScene.getHeight(), scene.getWidth(), scene.getHeight(), window.getWidth(),
window.getHeight(), hBorder, vBorder, myWidth, myHeight, getRawWidth(), getRawHeight());
// this.setWidth(myWidth);
// this.setHeight(myHeight);
window.setWidth(myWidth + hBorder);
window.setHeight(myHeight + vBorder);
if (debug)
System.out.printf(
"Resizing after : screen: %1.0fx%1.0f, screen: %1.0fx%1.0f, scene: %1.0fx%1.0f, window: %1.0fx%1.0f,%n border: %1.0fx%1.0f, new window size: %1.0fx%1.0f, canvas size: %1.0fx%1.0f%n",
javafx.stage.Screen.getPrimary().getVisualBounds().getWidth(),
javafx.stage.Screen.getPrimary().getVisualBounds().getHeight(), subScene.getWidth(),
subScene.getHeight(), scene.getWidth(), scene.getHeight(), window.getWidth(),
window.getHeight(), hBorder, vBorder, myWidth, myHeight, getRawWidth(), getRawHeight());
}
if (debug)
System.out.printf("Rescaling: subscene %1.2fx%1.2f, scale %1.2f, aspect %.4f (%d), canvas %1.0fx%1.0f%n",
subScene.getWidth(), subScene.getHeight(), currentScale, aspects.get(aspect), aspect, getRawWidth(),
getRawHeight());
for (Node n : root.getChildren()) {
n.relocate(Math.floor(subScene.getWidth() / 2),
Math.floor(subScene.getHeight() / 2 + (rawCanvasHeight - getRawHeight()) * currentScale / 2));
n.setTranslateX(-Math.floor(rawCanvasWidth / 2));
n.setTranslateY(-Math.floor(rawCanvasHeight / 2));
if (debug)
System.out.printf(" * layout %1.2fx%1.2f, translate %1.2fx%1.2f%n", n.getLayoutX(), n.getLayoutY(),
n.getTranslateX(), n.getTranslateY());
n.setScaleX(currentScale);
n.setScaleY(currentScale);
}
}
public void setMouseCursor(Cursor cursor) {
subScene.getScene().setCursor(cursor);
public void setAspect(int aspect) {
this.aspect = (aspect) % aspects.size();
recomputeLayout(false);
}
public void setBackground(Paint bgColor) {
this.bgColor = bgColor;
subScene.setFill(bgColor instanceof Color ? ((Color) bgColor).darker() : bgColor);
}
public void setFullScreen(boolean fullScreen) {
Window window = subScene.getScene().getWindow();
if (window instanceof Stage) {
((Stage) window).setFullScreenExitHint("");
((Stage) window).setFullScreen(fullScreen);
if (hideFullScreenMouseCursor) {
if (fullScreen) {
oldCursor = subScene.getScene().getCursor();
subScene.getScene().setCursor(Cursor.NONE);
} else if (oldCursor != null) {
subScene.getScene().setCursor(oldCursor);
oldCursor = null;
} else {
subScene.getScene().setCursor(Cursor.DEFAULT);
}
}
}
}
public void setHideFullScreenMouseCursor(boolean hideIt) {
@ -664,4 +605,71 @@ public class Screen {
}
hideFullScreenMouseCursor = hideIt;
}
/**
* @param keyOverride
* the keyOverride to set
*/
public void setKeyOverride(Predicate<KeyEvent> keyOverride) {
this.keyOverride = keyOverride;
}
/**
* @param keyHandler
* the keyHandler to set
*/
public void setKeyPressedHandler(Predicate<KeyEvent> keyHandler) {
this.keyPressedHandler = keyHandler;
}
/**
* @param keyReleasedHandler
* the keyReleasedHandler to set
*/
public void setKeyReleasedHandler(Predicate<KeyEvent> keyReleasedHandler) {
this.keyReleasedHandler = keyReleasedHandler;
}
/**
* @param keyTypedHandler
* the keyTypedHandler to set
*/
public void setKeyTypedHandler(Predicate<KeyEvent> keyTypedHandler) {
this.keyTypedHandler = keyTypedHandler;
}
public void setMouseCursor(Cursor cursor) {
subScene.getScene().setCursor(cursor);
}
public void showMouseCursor() {
subScene.getScene().setCursor(Cursor.DEFAULT);
}
public void zoomCycle() {
scaling++;
if (scaling > maxScale)
scaling = ((int) scaling) % maxScale;
recomputeLayout(true);
}
public void zoomFit() {
scaling = 0;
recomputeLayout(false);
}
public void zoomIn() {
scaling = Math.min(10, currentScale + 0.2);
recomputeLayout(false);
}
public void zoomOne() {
scaling = 1;
recomputeLayout(false);
}
public void zoomOut() {
scaling = Math.max(0.1, currentScale - 0.2);
recomputeLayout(false);
}
}

Binary file not shown.

Binary file not shown.

View File

@ -132,6 +132,7 @@ public class Direction {
return Math.atan2(yDir, xDir);
}
@Override
public String toString() {
return String.format("%.2f", toDegrees());
}

View File

@ -5,13 +5,13 @@ import javafx.scene.paint.Paint;
public interface IPainter extends IPaintLayer {
IShape shape();
ITurtle turtle();
IPainter restore();
IPainter save();
IPainter setInk(Paint ink);
IShape shape();
ITurtle turtle();
}

View File

@ -6,13 +6,51 @@ import javafx.scene.shape.Shape;
public interface IShape {
void draw();
/**
* Add another point to the line path
*
* @param xy
* @return
*/
IShape addPoint(double x, double y);
void draw(GraphicsContext context);
/**
* Add another point to the line path
*
* @param xy
* @return
*/
IShape addPoint(Point xy);
Shape toFXShape();
/**
* Set the arc angle for the subsequent draw commands
*
* <p>
* For use with {@link #arc()}
*
* @param a
* The angle, in degrees
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape angle(double a);
String toSvg();
/**
* Draw an arc with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #length(double)}
* <li>{@link #angle(double)}
* <li>{@link #gravity(Gravity)}
* <li>{@link #stroke(Paint)}, {@link #fill(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape arc();
/**
* Set the (x,y)-coordinates of the next draw command
@ -24,6 +62,160 @@ public interface IShape {
*/
IShape at(Point p);
/**
* Close the line path, turning it into a polygon.
*
* @return
*/
IShape close();
void draw();
void draw(GraphicsContext context);
/**
* Draw an ellipse with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #width(double)}, {@link #height(double)}
* <li>{@link #gravity(Gravity)}
* <li>{@link #stroke(Paint)}, {@link #fill(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape ellipse();
/**
* Fill the current shape
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape fill();
/**
* Set fill colour for the subsequent draw commands
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape fillPaint(Paint p);
/**
* Set gravity for the subsequent draw commands
*
* Gravity determines the point on the shape that will be used for positioning
* and rotation.
*
* @param g
* The gravity
* @return
*/
IShape gravity(Gravity g);
/**
* Set the height of the next draw command
*
* @param h
* The height
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape height(double h);
/**
* Set the length of the following draw commands
*
* <p>
* For use with {@link #line()} and {@link #arc()}
*
* @param l
* The length
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape length(double l);
/**
* Draw a line with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #length(double)}
* <li>{@link #angle(double)}
* <li>{@link #gravity(Gravity)} (flattened to the horizontal axis, so, e.g.,
* {@link Gravity#NORTH} = {@link Gravity#SOUTH} = {@link Gravity#CENTER})
* <li>{@link #stroke(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape line();
/**
* Draw a rectangle with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #width(double)}, {@link #height(double)}
* <li>{@link #gravity(Gravity)}
* <li>{@link #stroke(Paint)}, {@link #fill(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape rectangle();
/**
* Sets rotation for subsequent draw commands.
*
* <p>
* Shapes will be rotate around the {@link #gravity(Gravity)} point.
*
* @param angle
* Rotation in degrees
* @return
*/
IShape rotation(double angle);
/**
* Stroke the current shape
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape stroke();
/**
* Set stroke colour for the subsequent draw commands
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape strokePaint(Paint p);
Shape toFXShape();
String toSvg();
/**
* Set the width of the next draw command
*
* @param w
* The width
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape width(double w);
/**
* Set the x-coordinate of the next draw command
*
@ -44,196 +236,4 @@ public interface IShape {
*/
IShape y(double y);
/**
* Set gravity for the subsequent draw commands
*
* Gravity determines the point on the shape that will be used for positioning
* and rotation.
*
* @param g
* The gravity
* @return
*/
IShape gravity(Gravity g);
/**
* Sets rotation for subsequent draw commands.
*
* <p>
* Shapes will be rotate around the {@link #gravity(Gravity)} point.
*
* @param angle
* Rotation in degrees
* @return
*/
IShape rotation(double angle);
/**
* Add another point to the line path
*
* @param xy
* @return
*/
IShape addPoint(Point xy);
/**
* Add another point to the line path
*
* @param xy
* @return
*/
IShape addPoint(double x, double y);
/**
* Close the line path, turning it into a polygon.
*
* @return
*/
IShape close();
/**
* Draw an ellipse with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #width(double)}, {@link #height(double)}
* <li>{@link #gravity(Gravity)}
* <li>{@link #stroke(Paint)}, {@link #fill(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape ellipse();
/**
* Draw a rectangle with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #width(double)}, {@link #height(double)}
* <li>{@link #gravity(Gravity)}
* <li>{@link #stroke(Paint)}, {@link #fill(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape rectangle();
/**
* Draw an arc with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #length(double)}
* <li>{@link #angle(double)}
* <li>{@link #gravity(Gravity)}
* <li>{@link #stroke(Paint)}, {@link #fill(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape arc();
/**
* Draw a line with the current drawing parameters
*
* <p>
* Relevant parameters:
* <li>{@link #at(Point)}, {@link #x(double)}, {@link #gravity(double)}
* <li>{@link #length(double)}
* <li>{@link #angle(double)}
* <li>{@link #gravity(Gravity)} (flattened to the horizontal axis, so, e.g.,
* {@link Gravity#NORTH} = {@link Gravity#SOUTH} = {@link Gravity#CENTER})
* <li>{@link #stroke(Paint)}
* <li>{@link #rotation(double)}
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape line();
/**
* Set the arc angle for the subsequent draw commands
*
* <p>
* For use with {@link #arc()}
*
* @param a
* The angle, in degrees
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape angle(double a);
/**
* Set fill colour for the subsequent draw commands
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape fillPaint(Paint p);
/**
* Fill the current shape
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape fill();
/**
* Set the length of the following draw commands
*
* <p>
* For use with {@link #line()} and {@link #arc()}
*
* @param l
* The length
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape length(double l);
/**
* Stroke the current shape
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape stroke();
/**
* Set stroke colour for the subsequent draw commands
*
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape strokePaint(Paint p);
/**
* Set the width of the next draw command
*
* @param w
* The width
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape width(double w);
/**
* Set the height of the next draw command
*
* @param h
* The height
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape height(double h);
}

View File

@ -1,36 +1,63 @@
package inf101.v18.gfx.gfxmode;
import javafx.scene.canvas.GraphicsContext;
public interface ITurtle extends IPainter {
<T> T as(Class<T> class1);
/**
* Move to the given position while drawing a curve
*
* <p>
* The resulting curve is a cubic Bézier curve with the control points located
* at <code>getPos().move(getDirection, startControl)</code> and
* <code>to.move(Direction.fromDegrees(endAngle+180), endControl)</code>.
* <p>
* The turtle is left at point <code>to</code>, facing <code>endAngle</code>.
* <p>
* The turtle will start out moving in its current direction, aiming for a point
* <code>startControl</code> pixels away, then smoothly turning towards its
* goal. It will approach the <code>to</code> point moving in the direction
* <code>endAngle</code> (an absolute bearing, with 0° pointing right and 90°
* pointing up).
*
* @param to
* Position to move to
* @param startControl
* Distance to the starting control point.
* @return {@code this}, for sending more draw commands
*/
ITurtle curveTo(Point to, double startControl, double endAngle, double endControl);
void debugTurtle();
/**
* Start drawing a shape at the current turtle position.
* Move forward the given distance while drawing a line
*
* <p>
* The shape's default origin and rotation will be set to the turtle's current
* position and direction, but can be modified with {@link IShape#at(Point)} and
* {@link IShape#rotation(double)}.
* <p>
* The turtle's position and attributes are unaffected by drawing the shape.
*
* @return An IDrawParams object for setting up and drawing the shape
*/
IShape shape();
/**
* Draw a line from the current position to the given position.
*
* <p>
* This method does not change the turtle position.
*
* @param to
* Other end-point of the line
* @param dist
* Distance to move
* @return {@code this}, for sending more draw commands
*/
ITurtle line(Point to);
ITurtle draw(double dist);
/**
* Move to the given position while drawing a line
*
* @param x
* X-position to move to
* @param y
* Y-position to move to
* @return {@code this}, for sending more draw commands
*/
ITurtle drawTo(double x, double y);
/**
* Move to the given position while drawing a line
*
* @param to
* Position to move to
* @return {@code this}, for sending more draw commands
*/
ITurtle drawTo(Point to);
/**
* @return The current angle of the turtle, with 0° pointing to the right and
@ -79,57 +106,16 @@ public interface ITurtle extends IPainter {
ITurtle jumpTo(Point to);
/**
* Move forward the given distance while drawing a line
* Draw a line from the current position to the given position.
*
* @param dist
* Distance to move
* @return {@code this}, for sending more draw commands
*/
ITurtle draw(double dist);
/**
* Move to the given position while drawing a line
*
* @param x
* X-position to move to
* @param y
* Y-position to move to
* @return {@code this}, for sending more draw commands
*/
ITurtle drawTo(double x, double y);
/**
* Move to the given position while drawing a line
* <p>
* This method does not change the turtle position.
*
* @param to
* Position to move to
* Other end-point of the line
* @return {@code this}, for sending more draw commands
*/
ITurtle drawTo(Point to);
/**
* Move to the given position while drawing a curve
*
* <p>
* The resulting curve is a cubic Bézier curve with the control points located
* at <code>getPos().move(getDirection, startControl)</code> and
* <code>to.move(Direction.fromDegrees(endAngle+180), endControl)</code>.
* <p>
* The turtle is left at point <code>to</code>, facing <code>endAngle</code>.
* <p>
* The turtle will start out moving in its current direction, aiming for a point
* <code>startControl</code> pixels away, then smoothly turning towards its
* goal. It will approach the <code>to</code> point moving in the direction
* <code>endAngle</code> (an absolute bearing, with 0° pointing right and 90°
* pointing up).
*
* @param to
* Position to move to
* @param startControl
* Distance to the starting control point.
* @return {@code this}, for sending more draw commands
*/
ITurtle curveTo(Point to, double startControl, double endAngle, double endControl);
ITurtle line(Point to);
/**
* Set the size of the turtle's pen
@ -141,6 +127,21 @@ public interface ITurtle extends IPainter {
*/
ITurtle setPenSize(double pixels);
/**
* Start drawing a shape at the current turtle position.
*
* <p>
* The shape's default origin and rotation will be set to the turtle's current
* position and direction, but can be modified with {@link IShape#at(Point)} and
* {@link IShape#rotation(double)}.
* <p>
* The turtle's position and attributes are unaffected by drawing the shape.
*
* @return An IDrawParams object for setting up and drawing the shape
*/
@Override
IShape shape();
/**
* Change direction the given number of degrees (relative to the current
* direction).
@ -235,6 +236,7 @@ public interface ITurtle extends IPainter {
*/
ITurtle turnTowards(double degrees, double percent);
<T> T as(Class<T> class1);
double getWidth();
double getHeight();
}

View File

@ -88,6 +88,7 @@ public class Point {
return new Point(newX, newY);
}
@Override
public String toString() {
return String.format("(%.2f,%.2f)", x, y);
}

View File

@ -7,158 +7,7 @@ import javafx.scene.paint.Paint;
import javafx.scene.shape.Shape;
public class ShapePainter implements IShape {
private double x = 0, y = 0, w = 0, h = 0, rot = 0, strokeWidth = 0;
private List<Double> lineSegments = null;
private Paint fill = null;
private Paint stroke = null;
private Gravity gravity = Gravity.CENTER;
private DrawCommand cmd = null;
private boolean closed = false;
private final GraphicsContext context;
public ShapePainter(GraphicsContext context) {
super();
this.context = context;
}
public ShapePainter at(Point p) {
if (p != null) {
this.x = p.getX();
this.y = p.getY();
} else {
this.x = 0;
this.y = 0;
}
return this;
}
public ShapePainter x(double x) {
this.x = x;
return this;
}
public ShapePainter y(double y) {
this.y = y;
return this;
}
public ShapePainter width(double w) {
this.w = w;
return this;
}
public ShapePainter height(double h) {
this.h = h;
return this;
}
public IShape ellipse() {
cmd = new DrawEllipse();
return this;
}
public IShape rectangle() {
cmd = new DrawRectangle();
return this;
}
public IShape arc() {
// TODO Auto-generated method stub
return this;
}
public IShape line() {
cmd = new DrawLine();
return this;
}
public ShapePainter length(double l) {
w = l;
h = l;
return this;
}
public ShapePainter angle(double a) {
return this;
}
public ShapePainter fill() {
if (cmd != null)
cmd.fill(context, this);
return this;
}
public ShapePainter stroke() {
if (cmd != null)
cmd.stroke(context, this);
return this;
}
public ShapePainter fillPaint(Paint p) {
fill = p;
return this;
}
public ShapePainter strokePaint(Paint p) {
stroke = p;
return this;
}
public ShapePainter gravity(Gravity g) {
gravity = g;
return this;
}
public ShapePainter rotation(double angle) {
rot = angle;
return this;
}
public void draw() {
draw(context);
}
public Shape toFXShape() {
// TODO Auto-generated method stub
return null;
}
public String toSvg() {
// TODO Auto-generated method stub
return null;
}
private abstract static class DrawCommand {
public void stroke(GraphicsContext ctx, ShapePainter p) {
ctx.save();
ctx.setStroke(p.stroke);
if (p.strokeWidth != 0)
ctx.setLineWidth(p.strokeWidth);
ctx.translate(p.x, p.y);
if (p.rot != 0)
ctx.rotate(-p.rot);
strokeIt(ctx, p);
ctx.restore();
}
public void fill(GraphicsContext ctx, ShapePainter p) {
ctx.save();
ctx.setFill(p.fill);
ctx.translate(p.x, p.y);
if (p.rot != 0)
ctx.rotate(-p.rot);
fillIt(ctx, p);
ctx.restore();
}
protected abstract void strokeIt(GraphicsContext ctx, ShapePainter p);
protected abstract void fillIt(GraphicsContext ctx, ShapePainter p);
// public abstract Shape toFXShape(DrawParams p);
//
// public abstract String toSvg(DrawParams p);
protected double calcX(Gravity g, double w) {
switch (g) {
default:
@ -206,32 +55,70 @@ public class ShapePainter implements IShape {
return h / 2;
}
}
}
private static class DrawRectangle extends DrawCommand {
public void strokeIt(GraphicsContext ctx, ShapePainter p) {
ctx.strokeRect(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
public void fill(GraphicsContext ctx, ShapePainter p) {
ctx.save();
ctx.setFill(p.fill);
ctx.translate(p.x, p.y);
if (p.rot != 0)
ctx.rotate(-p.rot);
fillIt(ctx, p);
ctx.restore();
}
public void fillIt(GraphicsContext ctx, ShapePainter p) {
ctx.fillRect(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
protected abstract void fillIt(GraphicsContext ctx, ShapePainter p);
// public abstract Shape toFXShape(DrawParams p);
//
// public abstract String toSvg(DrawParams p);
public void stroke(GraphicsContext ctx, ShapePainter p) {
ctx.save();
ctx.setStroke(p.stroke);
if (p.strokeWidth != 0)
ctx.setLineWidth(p.strokeWidth);
ctx.translate(p.x, p.y);
if (p.rot != 0)
ctx.rotate(-p.rot);
strokeIt(ctx, p);
ctx.restore();
}
protected abstract void strokeIt(GraphicsContext ctx, ShapePainter p);
}
private static class DrawEllipse extends DrawCommand {
public void strokeIt(GraphicsContext ctx, ShapePainter p) {
ctx.strokeOval(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
}
@Override
public void fillIt(GraphicsContext ctx, ShapePainter p) {
ctx.fillOval(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
}
@Override
public void strokeIt(GraphicsContext ctx, ShapePainter p) {
ctx.strokeOval(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
}
}
private static class DrawLine extends DrawCommand {
@Override
public void fillIt(GraphicsContext ctx, ShapePainter p) {
if (p.lineSegments != null) {
int nPoints = (p.lineSegments.size() / 2) + 1;
double xs[] = new double[nPoints];
double ys[] = new double[nPoints];
xs[0] = -calcX(p.gravity, p.w);
ys[0] = -calcY(p.gravity, p.h);
for (int i = 0; i < p.lineSegments.size(); i++) {
xs[i] = p.lineSegments.get(i * 2) - p.x;
ys[i] = p.lineSegments.get(i * 2 + 1) - p.y;
}
ctx.fillPolygon(xs, ys, nPoints);
}
}
@Override
public void strokeIt(GraphicsContext ctx, ShapePainter p) {
if (p.lineSegments == null) {
double x = -calcX(p.gravity, p.w);
@ -253,37 +140,37 @@ public class ShapePainter implements IShape {
ctx.strokePolyline(xs, ys, nPoints);
}
}
}
private static class DrawRectangle extends DrawCommand {
@Override
public void fillIt(GraphicsContext ctx, ShapePainter p) {
if (p.lineSegments != null) {
int nPoints = (p.lineSegments.size() / 2) + 1;
double xs[] = new double[nPoints];
double ys[] = new double[nPoints];
xs[0] = -calcX(p.gravity, p.w);
ys[0] = -calcY(p.gravity, p.h);
for (int i = 0; i < p.lineSegments.size(); i++) {
xs[i] = p.lineSegments.get(i * 2) - p.x;
ys[i] = p.lineSegments.get(i * 2 + 1) - p.y;
}
ctx.fillPolygon(xs, ys, nPoints);
}
ctx.fillRect(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
}
@Override
public void strokeIt(GraphicsContext ctx, ShapePainter p) {
ctx.strokeRect(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
}
}
public void draw(GraphicsContext context) {
if (cmd != null) {
if (fill != null)
cmd.fill(context, this);
if (stroke != null)
cmd.stroke(context, this);
}
}
private double x = 0, y = 0, w = 0, h = 0, rot = 0, strokeWidth = 0;
private List<Double> lineSegments = null;
private Paint fill = null;
private Paint stroke = null;
@Override
public IShape addPoint(Point xy) {
lineSegments.add(xy.getX());
lineSegments.add(xy.getY());
return this;
private Gravity gravity = Gravity.CENTER;
private DrawCommand cmd = null;
private boolean closed = false;
private final GraphicsContext context;
public ShapePainter(GraphicsContext context) {
super();
this.context = context;
}
@Override
@ -293,9 +180,150 @@ public class ShapePainter implements IShape {
return this;
}
@Override
public IShape addPoint(Point xy) {
lineSegments.add(xy.getX());
lineSegments.add(xy.getY());
return this;
}
@Override
public ShapePainter angle(double a) {
return this;
}
@Override
public IShape arc() {
throw new UnsupportedOperationException();
}
@Override
public ShapePainter at(Point p) {
if (p != null) {
this.x = p.getX();
this.y = p.getY();
} else {
this.x = 0;
this.y = 0;
}
return this;
}
@Override
public IShape close() {
closed = true;
return this;
}
@Override
public void draw() {
draw(context);
}
@Override
public void draw(GraphicsContext context) {
if (cmd != null) {
if (fill != null)
cmd.fill(context, this);
if (stroke != null)
cmd.stroke(context, this);
}
}
@Override
public IShape ellipse() {
cmd = new DrawEllipse();
return this;
}
@Override
public ShapePainter fill() {
if (cmd != null)
cmd.fill(context, this);
return this;
}
@Override
public ShapePainter fillPaint(Paint p) {
fill = p;
return this;
}
@Override
public ShapePainter gravity(Gravity g) {
gravity = g;
return this;
}
@Override
public ShapePainter height(double h) {
this.h = h;
return this;
}
@Override
public ShapePainter length(double l) {
w = l;
h = l;
return this;
}
@Override
public IShape line() {
cmd = new DrawLine();
return this;
}
@Override
public IShape rectangle() {
cmd = new DrawRectangle();
return this;
}
@Override
public ShapePainter rotation(double angle) {
rot = angle;
return this;
}
@Override
public ShapePainter stroke() {
if (cmd != null)
cmd.stroke(context, this);
return this;
}
@Override
public ShapePainter strokePaint(Paint p) {
stroke = p;
return this;
}
@Override
public Shape toFXShape() {
throw new UnsupportedOperationException();
}
@Override
public String toSvg() {
throw new UnsupportedOperationException();
}
@Override
public ShapePainter width(double w) {
this.w = w;
return this;
}
@Override
public ShapePainter x(double x) {
this.x = x;
return this;
}
@Override
public ShapePainter y(double y) {
this.y = y;
return this;
}
}

View File

@ -32,6 +32,8 @@ public class TurtlePainter implements IPaintLayer, ITurtle {
}
private final Screen screen;
private final double width;
private final double height;
private final GraphicsContext context;
private final List<TurtleState> stateStack = new ArrayList<>();
@ -39,68 +41,45 @@ public class TurtlePainter implements IPaintLayer, ITurtle {
private final Canvas canvas;
private boolean path = false;
public TurtlePainter(double width, double height, Canvas canvas) {
screen = null;
if (canvas == null)
canvas = new Canvas(width, height);
this.canvas = canvas;
this.context = canvas.getGraphicsContext2D();
this.width = width;
this.height = height;
stateStack.add(new TurtleState());
state.dir = new Direction(1.0, 0.0);
state.pos = new Point(width / 2, height / 2);
}
public TurtlePainter(Screen screen, Canvas canvas) {
this.screen = screen;
this.canvas = canvas;
this.context = canvas.getGraphicsContext2D();
this.width = screen.getWidth();
this.height = screen.getHeight();
stateStack.add(new TurtleState());
state.dir = new Direction(1.0, 0.0);
state.pos = new Point(screen.getWidth() / 2, screen.getHeight() / 2);
}
@Override
@SuppressWarnings("unchecked")
public <T> T as(Class<T> clazz) {
if (clazz == GraphicsContext.class)
return (T) context;
else
return null;
}
@Override
public void clear() {
context.clearRect(0, 0, getWidth(), getHeight());
}
@Override
public void debugTurtle() {
System.err.println("[" + state.pos + " " + state.dir + "]");
}
@Override
public IShape shape() {
ShapePainter s = new ShapePainter(context);
return s.at(getPos()).rotation(getAngle()).strokePaint(state.ink);
}
@Override
public ITurtle line(Point to) {
// context.save();
context.setStroke(state.ink);
context.setLineWidth(state.penSize);
context.strokeLine(state.pos.getX(), state.pos.getY(), to.getX(), to.getY());
// context.restore();
return this;
}
@Override
public double getAngle() {
return state.dir.toDegrees();
}
@Override
public Direction getDirection() {
return state.dir;
}
public double getHeight() {
return screen.getHeight();
}
public Screen getScreen() {
return screen;
}
@Override
public Point getPos() {
return state.pos;
}
public double getWidth() {
return screen.getWidth();
}
public ITurtle curveTo(Point to, double startControl, double endAngle, double endControl) {
Point c1 = state.pos.move(state.dir, startControl);
Point c2 = to.move(Direction.fromDegrees(endAngle + 180), endControl);
@ -123,6 +102,64 @@ public class TurtlePainter implements IPaintLayer, ITurtle {
return this;
}
@Override
public void debugTurtle() {
System.err.println("[" + state.pos + " " + state.dir + "]");
}
@Override
public ITurtle draw(double dist) {
Point to = state.pos.move(state.dir, dist);
return drawTo(to);
}
@Override
public ITurtle drawTo(double x, double y) {
Point to = new Point(x, y);
return drawTo(to);
}
@Override
public ITurtle drawTo(Point to) {
if (path) {
context.lineTo(to.getX(), to.getY());
} else {
line(to);
}
state.inDir = state.dir;
state.pos = to;
return this;
}
@Override
public double getAngle() {
return state.dir.toDegrees();
}
@Override
public Direction getDirection() {
return state.dir;
}
@Override
public double getHeight() {
return height;
}
@Override
public Point getPos() {
return state.pos;
}
public Screen getScreen() {
return screen;
}
@Override
public double getWidth() {
return width;
}
@Override
public ITurtle jump(double dist) {
state.inDir = state.dir;
@ -145,26 +182,24 @@ public class TurtlePainter implements IPaintLayer, ITurtle {
}
@Override
public ITurtle draw(double dist) {
Point to = state.pos.move(state.dir, dist);
return drawTo(to);
public void layerToBack() {
if (screen != null)
screen.moveToBack(this);
}
@Override
public ITurtle drawTo(double x, double y) {
Point to = new Point(x, y);
return drawTo(to);
public void layerToFront() {
if (screen != null)
screen.moveToFront(this);
}
@Override
public ITurtle drawTo(Point to) {
if (path) {
context.lineTo(to.getX(), to.getY());
} else {
line(to);
}
state.inDir = state.dir;
state.pos = to;
public ITurtle line(Point to) {
// context.save();
context.setStroke(state.ink);
context.setLineWidth(state.penSize);
context.strokeLine(state.pos.getX(), state.pos.getY(), to.getX(), to.getY());
// context.restore();
return this;
}
@ -196,6 +231,12 @@ public class TurtlePainter implements IPaintLayer, ITurtle {
return this;
}
@Override
public IShape shape() {
ShapePainter s = new ShapePainter(context);
return s.at(getPos()).rotation(getAngle()).strokePaint(state.ink);
}
@Override
public ITurtle turn(double degrees) {
state.dir = state.dir.turn(degrees);
@ -245,29 +286,12 @@ public class TurtlePainter implements IPaintLayer, ITurtle {
return this;
}
@Override
public void layerToFront() {
screen.moveToFront(this);
}
@Override
public void layerToBack() {
screen.moveToBack(this);
}
@Override
public ITurtle turtle() {
TurtlePainter painter = new TurtlePainter(screen, canvas);
TurtlePainter painter = screen != null ? new TurtlePainter(screen, canvas)
: new TurtlePainter(width, height, canvas);
painter.stateStack.set(0, new TurtleState(state));
return painter;
}
@SuppressWarnings("unchecked")
public <T> T as(Class<T> clazz) {
if (clazz == GraphicsContext.class)
return (T) context;
else
return null;
}
}

View File

@ -6,10 +6,26 @@ import java.util.List;
import java.util.function.BiFunction;
public class BlocksAndBoxes {
public enum PixelOrder implements Iterable<Integer> {
LEFT_TO_RIGHT(8, 4, 2, 1), RIGHT_TO_LEFT(4, 8, 1, 2), LEFT_TO_RIGHT_UPWARDS(2, 1, 8,
4), RIGHT_TO_LEFT_UPWARDS(1, 2, 4, 8);
private List<Integer> order;
private PixelOrder(int a, int b, int c, int d) {
order = Arrays.asList(a, b, c, d);
}
@Override
public Iterator<Integer> iterator() {
return order.iterator();
}
}
public static final String[] unicodeBlocks = { " ", "", "", "", "", "", "", "", "", "", "", "", "", "",
"", "", "" };
public static final int[] unicodeBlocks_NumPixels = { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 2 };
public static final int[] unicodeBlocks_NumPixels = { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 2 };
public static final String unicodeBlocksString = String.join("", unicodeBlocks);
public static final String BLOCK_EMPTY = " ";
public static final String BLOCK_BOTTOM_RIGHT = "";
@ -27,7 +43,19 @@ public class BlocksAndBoxes {
public static final String BLOCK_REVERSE_BOTTOM_LEFT = "";
public static final String BLOCK_REVERSE_BOTTOM_RIGHT = "";
public static final String BLOCK_FULL = "";
public static final String BLOCK_HALF = "";
public static final String BLOCK_HALF = "";;
public static String blockAddOne(String s, PixelOrder order) {
int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s);
if (i >= 0) {
for (int bit : order) {
if ((i & bit) == 0)
return unicodeBlocks[i | bit];
}
}
return s;
}
/**
* Convert a string into a Unicode block graphics character.
@ -122,7 +150,20 @@ public class BlocksAndBoxes {
}
throw new IllegalArgumentException(
"Expected length 4 string of \" \" and \"*\", or \"++++\", got \"" + s + "\"");
};
}
public static String blockCompact(String s) {
int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s);
if (i > 0) {
int lower = i & 3;
int upper = (i >> 2) & 3;
i = (lower | upper) | ((lower & upper) << 2);
// System.out.println("Compact: " + s + " -> " + BlocksAndBoxes.unicodeBlocks[i]
// + "\n");
return BlocksAndBoxes.unicodeBlocks[i];
}
return s;
}
public static String blockCompose(String b1, String b2, BiFunction<Integer, Integer, Integer> op) {
int i1 = unicodeBlocksString.indexOf(b1);
@ -146,33 +187,6 @@ public class BlocksAndBoxes {
return unicodeBlocks[op.apply(i1, i2)];
}
public enum PixelOrder implements Iterable<Integer> {
LEFT_TO_RIGHT(8, 4, 2, 1), RIGHT_TO_LEFT(4, 8, 1, 2), LEFT_TO_RIGHT_UPWARDS(2, 1, 8,
4), RIGHT_TO_LEFT_UPWARDS(1, 2, 4, 8);
private List<Integer> order;
private PixelOrder(int a, int b, int c, int d) {
order = Arrays.asList(a, b, c, d);
}
@Override
public Iterator<Integer> iterator() {
return order.iterator();
}
}
public static String blockAddOne(String s, PixelOrder order) {
int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s);
if (i >= 0) {
for (int bit : order) {
if ((i & bit) == 0)
return unicodeBlocks[i | bit];
}
}
return s;
}
public static String blockRemoveOne(String s, PixelOrder order) {
int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s);
if (i >= 0) {
@ -184,17 +198,4 @@ public class BlocksAndBoxes {
return s;
}
public static String blockCompact(String s) {
int i = BlocksAndBoxes.unicodeBlocksString.indexOf(s);
if (i > 0) {
int lower = i & 3;
int upper = (i >> 2) & 3;
i = (lower | upper) | ((lower & upper) << 2);
// System.out.println("Compact: " + s + " -> " + BlocksAndBoxes.unicodeBlocks[i]
// + "\n");
return BlocksAndBoxes.unicodeBlocks[i];
}
return s;
}
}

View File

@ -19,7 +19,6 @@ public class DemoPages {
printer.println(s);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

View File

@ -36,6 +36,16 @@ public class Printer implements IPaintLayer {
4.0000, -8.5000, 1.5000, 1.0000, true);
public static final TextFont FONT_ZXSPECTRUM7 = new TextFont("ZXSpectrum-7.otf", 22.00, TextMode.CHAR_BOX_SIZE,
3.1000, -3.8000, 1.0000, 1.0000, true);
/**
* TTF file can be found here: http://users.teilar.gr/~g1951d/ in this ZIP file:
* http://users.teilar.gr/~g1951d/Symbola.zip
* <p>
* (Put the extracted Symbola.ttf in src/inf101/v18/gfx/fonts/)
*/
public static final TextFont FONT_SYMBOLA = new TextFont("Symbola.ttf", 26.70, TextMode.CHAR_BOX_SIZE, -0.4000,
-7.6000, 1.35000, 1.0000, true);
/**
* TTF file can be found here:
* http://www.kreativekorp.com/software/fonts/c64.shtml
@ -49,8 +59,6 @@ public class Printer implements IPaintLayer {
*/
public static final TextFont FONT_C64 = new TextFont("PetMe64.ttf", 31.50, TextMode.CHAR_BOX_SIZE, 0.0000, -4.000,
1.0000, 1.0000, true);
Color DEFAULT_FILL = Color.BLACK;
Color DEFAULT_STROKE = Color.TRANSPARENT;
private static final Paint DEFAULT_BACKGROUND = Color.TRANSPARENT;
private static final TextMode DEFAULT_MODE = TextMode.MODE_40X22;
@ -67,6 +75,10 @@ public class Printer implements IPaintLayer {
return r;
}
Color DEFAULT_FILL = Color.BLACK;
Color DEFAULT_STROKE = Color.TRANSPARENT;
private TextMode textMode;
private Color fill;
private Color stroke;
@ -88,6 +100,10 @@ public class Printer implements IPaintLayer {
private int csiMode = 0;
public Printer(double width, double height) {
this(null, new Canvas(width, height));
}
public Printer(Screen screen, Canvas page) {
this.screen = screen;
this.textPage = page;
@ -142,17 +158,6 @@ public class Printer implements IPaintLayer {
});
}
private void drawChar(int x, int y, Char c) {
if (c != null) {
GraphicsContext context = textPage.getGraphicsContext2D();
context.setFill(c.fill);
context.setStroke(c.stroke);
font.drawTextAt(context, (x - 1) * getCharWidth(), y * getCharHeight(), c.s,
textMode.getCharWidth() / textMode.getCharBoxSize(), c.mode, c.bg);
}
}
private String addToCsiBuffer(String s) {
if (csiMode == 1) {
switch (s) {
@ -207,6 +212,7 @@ public class Printer implements IPaintLayer {
y = topMargin;
}
@Override
public void clear() {
print("\f");
}
@ -215,6 +221,31 @@ public class Printer implements IPaintLayer {
printAt(x, y, " ");
}
public void clearLine(int y) {
y = constrainY(y);
if (y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
Arrays.fill(lineBuffer.get(y - 1), null);
redrawTextPage();
}
}
public void clearRegion(int x, int y, int width, int height) {
if (x > getLineWidth() || y > getPageHeight())
return;
int x2 = Math.min(x + width - 1, getLineWidth());
int y2 = Math.min(y + height - 1, getPageHeight());
if (x2 < 1 || y2 < 1)
return;
int x1 = Math.max(1, x);
int y1 = Math.max(1, y);
// Char fillWith = new Char("*", Color.BLACK, Color.GREEN, Color.TRANSPARENT,
// 0);
for (int i = y1; i <= y2; i++) {
Arrays.fill(lineBuffer.get(i - 1), x1 - 1, x2, null);
}
redrawTextPage();
}
private int constrainX(int x) {
return x; // Math.min(LINE_WIDTH_HIRES, Math.max(1, x));
}
@ -249,26 +280,52 @@ public class Printer implements IPaintLayer {
public void cycleMode(boolean adjustDisplayAspect) {
textMode = textMode.nextMode();
if (adjustDisplayAspect)
if (adjustDisplayAspect && screen != null)
screen.setAspect(textMode.getAspect());
redrawTextPage();
}
public void drawCharCells() {
GraphicsContext context = screen.getBackgroundContext();
screen.clearBackground();
double w = getCharWidth();
double h = getCharHeight();
context.save();
context.setGlobalBlendMode(BlendMode.EXCLUSION);
context.setFill(Color.WHITE.deriveColor(0.0, 1.0, 1.0, 0.3));
for (int x = 0; x < getLineWidth(); x++) {
for (int y = 0; y < getPageHeight(); y++) {
if ((x + y) % 2 == 0)
context.fillRect(x * w, y * h, w, h);
}
private void drawChar(int x, int y, Char c) {
if (c != null) {
GraphicsContext context = textPage.getGraphicsContext2D();
context.setFill(c.fill);
context.setStroke(c.stroke);
font.drawTextAt(context, (x - 1) * getCharWidth(), y * getCharHeight(), c.s,
textMode.getCharWidth() / textMode.getCharBoxSize(), c.mode, c.bg);
}
context.restore();
}
public void drawCharCells() {
if (screen != null) {
GraphicsContext context = screen.getBackgroundContext();
screen.clearBackground();
double w = getCharWidth();
double h = getCharHeight();
context.save();
context.setGlobalBlendMode(BlendMode.EXCLUSION);
context.setFill(Color.WHITE.deriveColor(0.0, 1.0, 1.0, 0.1));
for (int x = 0; x < getLineWidth(); x++) {
for (int y = 0; y < getPageHeight(); y++) {
if ((x + y) % 2 == 0)
context.fillRect(x * w, y * h, w, h);
}
}
context.restore();
}
}
public Color getBackground(int x, int y) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
Color bg = Color.TRANSPARENT;
if (c != null && c.bg instanceof Color)
bg = (Color) c.bg;
else if (background instanceof Color)
bg = (Color) background;
return bg;
}
public boolean getBold() {
@ -305,41 +362,6 @@ public class Printer implements IPaintLayer {
return fill;
}
public Color getBackground(int x, int y) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
Color bg = Color.TRANSPARENT;
if (c != null && c.bg instanceof Color)
bg = (Color) c.bg;
else if (background instanceof Color)
bg = (Color) background;
return bg;
}
public void setBackground(int x, int y, Paint bg) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
if (c != null) {
c.bg = bg;
drawChar(x, y, c);
}
}
public void setColor(int x, int y, Color fill) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
if (c != null) {
c.fill = fill;
drawChar(x, y, c);
}
}
public TextFont getFont() {
return font;
}
@ -367,6 +389,10 @@ public class Printer implements IPaintLayer {
return (videoAttrs & TextFont.ATTR_INVERSE) != 0;
}
public TextMode getTextMode() {
return textMode;
}
/**
* @return the topMargin
*/
@ -390,6 +416,20 @@ public class Printer implements IPaintLayer {
return !getChar(x, y).equals(" ");
}
@Override
public void layerToBack() {
if (screen != null) {
screen.moveToBack(this);
}
}
@Override
public void layerToFront() {
if (screen != null) {
screen.moveToFront(this);
}
}
public void move(int deltaX, int deltaY) {
x = constrainX(x + deltaX);
y = constrainYOrScroll(y + deltaY);
@ -550,6 +590,17 @@ public class Printer implements IPaintLayer {
return old;
}
public void setBackground(int x, int y, Paint bg) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
if (c != null) {
c.bg = bg;
drawChar(x, y, c);
}
}
public void setBackground(Paint bgColor) {
this.background = bgColor != null ? bgColor : DEFAULT_BACKGROUND;
}
@ -570,6 +621,17 @@ public class Printer implements IPaintLayer {
return null;
}
public void setColor(int x, int y, Color fill) {
Char c = null;
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
c = lineBuffer.get(y - 1)[x - 1];
}
if (c != null) {
c.fill = fill;
drawChar(x, y, c);
}
}
public void setFill(Color fill) {
this.fill = fill != null ? fill : DEFAULT_FILL;
}
@ -615,6 +677,19 @@ public class Printer implements IPaintLayer {
this.stroke = stroke != null ? stroke : DEFAULT_STROKE;
}
public void setTextMode(TextMode mode) {
setTextMode(mode, false);
}
public void setTextMode(TextMode mode, boolean adjustDisplayAspect) {
if (mode == null)
throw new IllegalArgumentException();
textMode = mode;
if (adjustDisplayAspect && screen != null)
screen.setAspect(textMode.getAspect());
redrawTextPage();
}
public void setTopMargin() {
this.topMargin = y;
}
@ -627,10 +702,6 @@ public class Printer implements IPaintLayer {
this.topMargin = constrainY(topMargin);
}
public void setVideoAttrs(int attr) {
videoAttrs = attr;
}
public void setVideoAttrDisabled(int attr) {
videoAttrs &= ~attr;
}
@ -639,42 +710,12 @@ public class Printer implements IPaintLayer {
videoAttrs |= attr;
}
public void setTextMode(TextMode mode) {
setTextMode(mode, false);
}
public void setTextMode(TextMode mode, boolean adjustDisplayAspect) {
if (mode == null)
throw new IllegalArgumentException();
textMode = mode;
if (adjustDisplayAspect)
screen.setAspect(textMode.getAspect());
redrawTextPage();
}
public TextMode getTextMode() {
return textMode;
public void setVideoAttrs(int attr) {
videoAttrs = attr;
}
public void unplot(int x, int y) {
plot(x, y, (a, b) -> a & ~b);
}
@Override
public void layerToFront() {
screen.moveToFront(this);
}
@Override
public void layerToBack() {
screen.moveToBack(this);
}
public void clearLine(int y) {
y = constrainY(y);
if (y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
Arrays.fill(lineBuffer.get(y - 1), null);
redrawTextPage();
}
}
}

View File

@ -19,6 +19,7 @@ import javafx.scene.paint.Paint;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.text.Font;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
/**
* TextFont for grid-based text / character graphics
@ -935,7 +936,7 @@ public class TextFont {
target.save(); // save 3
if ((mode & ATTR_ITALIC) != 0) {
target.translate(-0.2, 0);
target.transform(new Affine(Affine.shear(-0.2, 0)));
target.transform(new Affine(Transform.shear(-0.2, 0)));
}
setGraphicsContext(target, xScaleFactor);
if (fill != null)

View File

@ -9,7 +9,7 @@ import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class TextFontAdjuster extends Application {
private static final String FONT_NAME = "PetMe64.ttf";
// private static final String FONT_NAME = "PetMe64.ttf";
// new TextFont(FONT_NAME, 22.2, TextModes.CHAR_BOX_SIZE, 0.0, 0.0, 1.0, 1.0);
private static TextFontAdjuster demo;
@ -21,8 +21,9 @@ public class TextFontAdjuster extends Application {
launch(args);
}
private TextFont textFont = new TextFont("ZXSpectrum-7.otf", 22.00, TextMode.CHAR_BOX_SIZE, 3.1000, -3.8000, 1.0000,
1.0000, true);
private TextFont textFont = Printer.FONT_SYMBOLA;//
// new TextFont("ZXSpectrum-7.otf", 22.00, TextMode.CHAR_BOX_SIZE, 3.1000,
// -3.8000, 1.0000, 1.0000, true);
private Screen screen;
private boolean paused;
@ -107,11 +108,9 @@ public class TextFontAdjuster extends Application {
textFont.getFont().getSize()));
printer.println(String.format(" xTr=%-1.1f yTr=%-1.1f xSc=%-1.1f ySc=%-1.1f ", textFont.getxTranslate(),
textFont.getyTranslate(), textFont.getxScale(), textFont.getyScale()));
// System.out.printf("new TextFont(\"%s\", %1.2f, Printer.CHAR_HEIGHT, %1.4f,
// %1.4f, %1.4f, %1.4f)%n", FONT_NAME,
// textFont.getSize(), textFont.getxTranslate(), textFont.getyTranslate(),
// textFont.getxScale(),
// textFont.getyScale());
System.out.printf("new TextFont(\"%s\", %1.2f, Printer.CHAR_HEIGHT, %1.4f, %1.4f, %1.4f, %1.4f)%n",
textFont.getFont().getName(), textFont.getSize(), textFont.getxTranslate(), textFont.getyTranslate(),
textFont.getxScale(), textFont.getyScale());
printer.moveTo(1, 15);
}

View File

@ -43,17 +43,15 @@ public interface IArea extends Iterable<ILocation> {
boolean equals(Object other);
/**
* Get a location object corresponding to (x,y)
* Convert a 1D coordinate to a location
* <p>
* Returns a location <code>l = fromIndex(i)</code> such that
* <code>toIndex(l.getX(), l.getY()) == i</code>.
*
* @param x
* X-coordinate
* @param y
* Y-coordinate
* @return The location object associated with (x,y)
* @throws IndexOutOfBoundsException
* if {@link #contains(int, int)} returns false for (x,y)
* @param i
* @return A location
*/
ILocation location(int x, int y);
ILocation fromIndex(int i);
/** @return Height of the area */
int getHeight();
@ -71,6 +69,32 @@ public interface IArea extends Iterable<ILocation> {
@Override
int hashCode();
/**
* Get a location object corresponding to (x,y)
*
* @param x
* X-coordinate
* @param y
* Y-coordinate
* @return The location object associated with (x,y)
* @throws IndexOutOfBoundsException
* if {@link #contains(int, int)} returns false for (x,y)
*/
ILocation location(int x, int y);
/**
* Get all locations in area
* <p>
* Since IArea is @{@link Iterable}, you can also use directly in a for-loop to
* iterate over the locations.
* <p>
* All locations in the list are guaranteed to be valid according to
* {@link #isValid(ILocation)}. The returned list cannot be modified.
*
* @return An unmodifiable list with all the locations in the area
*/
List<ILocation> locations();
/**
* Return an object for iterating over all the neighbours of the given position,
* suitable for use in a new-style for-loop.
@ -91,6 +115,23 @@ public interface IArea extends Iterable<ILocation> {
*/
Iterable<ILocation> neighboursOf(ILocation pos);
/** @return A (possibly) parallel {@link Stream} of all locations in the area */
Stream<ILocation> parallelStream();
/** @return A {@link Stream} of all locations in the area */
Stream<ILocation> stream();
/**
* Convert a 2D coordinate to 1D
*
* @param x
* X-coordinate
* @param y
* Y-coordinate
* @return x + y*getWidth()
*/
int toIndex(int x, int y);
@Override
String toString();
@ -117,45 +158,4 @@ public interface IArea extends Iterable<ILocation> {
* @return True if the area wraps around vertically
*/
boolean wrapsVertically();
/** @return A {@link Stream} of all locations in the area */
Stream<ILocation> stream();
/** @return A (possibly) parallel {@link Stream} of all locations in the area */
Stream<ILocation> parallelStream();
/**
* Convert a 2D coordinate to 1D
*
* @param x
* X-coordinate
* @param y
* Y-coordinate
* @return x + y*getWidth()
*/
int toIndex(int x, int y);
/**
* Convert a 1D coordinate to a location
* <p>
* Returns a location <code>l = fromIndex(i)</code> such that
* <code>toIndex(l.getX(), l.getY()) == i</code>.
*
* @param i
* @return A location
*/
ILocation fromIndex(int i);
/**
* Get all locations in area
* <p>
* Since IArea is @{@link Iterable}, you can also use directly in a for-loop to
* iterate over the locations.
* <p>
* All locations in the list are guaranteed to be valid
* according to {@link #isValid(ILocation)}. The returned list cannot be modified.
*
* @return An unmodifiable list with all the locations in the area
*/
List<ILocation> locations();
}

View File

@ -13,123 +13,19 @@ public interface IGrid<T> extends Iterable<T> {
IGrid<T> copy();
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param x
* The column of the cell to get the contents of.
* @param y
* The row of the cell to get contents of.
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
T get(int x, int y);
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @throws IndexOutOfBoundsException
* if !isValid(pos)
*/
T get(ILocation pos);
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param x
* The column of the cell to get the contents of.
* @param y
* The row of the cell to get contents of.
* @param defaultResult
* A default value to be substituted if the (x,y) is out of bounds or
* contents == null.
*/
T getOrDefault(int x, int y, T defaultResult);
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param defaultResult
* A default value to be substituted if the (x,y) is out of bounds or
* contents == null.
*/
T getOrDefault(ILocation pos, T defaultResult);
/** @return The height of the grid. */
int getHeight();
/** @return The width of the grid. */
int getWidth();
/**
* Check if coordinates are valid.
* Create a parallel {@link Stream} with all the elements in this grid.
*
* Valid coordinates are 0 <= pos.getX() < getWidth(), 0 <= pos.getY() <
* getHeight().
*
* @param pos
* A position
* @return true if the position is within the grid
* @return A stream
* @see {@link java.util.Collection#parallelStream()}
*/
boolean isValid(ILocation pos);
Stream<T> elementParallelStream();
/**
* Check if coordinates are valid.
* Create a {@link Stream} with all the elements in this grid.
*
* Valid coordinates are 0 <= x < getWidth(), 0 <= y < getHeight().
*
* @param x
* an x coordinate
* @param y
* an y coordinate
* @return true if the (x,y) position is within the grid
* @return A stream
*/
boolean isValid(int x, int y);
/**
* Set the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param element
* The contents the cell is to have.
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
void set(int x, int y, T element);
/**
* Set the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param element
* The contents the cell is to have.
* @throws IndexOutOfBoundsException
* if !isValid(pos)
*/
void set(ILocation pos, T element);
Stream<T> elementStream();
/**
* Initialise the contents of all cells using an initialiser function.
@ -161,6 +57,108 @@ public interface IGrid<T> extends Iterable<T> {
*/
void fill(T element);
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @throws IndexOutOfBoundsException
* if !isValid(pos)
*/
T get(ILocation pos);
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param x
* The column of the cell to get the contents of.
* @param y
* The row of the cell to get contents of.
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
T get(int x, int y);
IArea getArea();
/** @return The height of the grid. */
int getHeight();
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param defaultResult
* A default value to be substituted if the (x,y) is out of bounds or
* contents == null.
*/
T getOrDefault(ILocation pos, T defaultResult);
/**
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param x
* The column of the cell to get the contents of.
* @param y
* The row of the cell to get contents of.
* @param defaultResult
* A default value to be substituted if the (x,y) is out of bounds or
* contents == null.
*/
T getOrDefault(int x, int y, T defaultResult);
/** @return The width of the grid. */
int getWidth();
/**
* Check if coordinates are valid.
*
* Valid coordinates are 0 <= pos.getX() < getWidth(), 0 <= pos.getY() <
* getHeight().
*
* @param pos
* A position
* @return true if the position is within the grid
*/
boolean isValid(ILocation pos);
/**
* Check if coordinates are valid.
*
* Valid coordinates are 0 <= x < getWidth(), 0 <= y < getHeight().
*
* @param x
* an x coordinate
* @param y
* an y coordinate
* @return true if the (x,y) position is within the grid
*/
boolean isValid(int x, int y);
/**
* Create a parallel {@link Stream} with all the locations in this grid.
* <p>
* All locations obtained through the stream are guaranteed to be valid
* according to {@link #isValid(ILocation)}.
*
* @return A stream
* @see {@link java.util.Collection#parallelStream()}
*/
Stream<ILocation> locationParallelStream();
/**
* Iterate over all grid locations
* <p>
@ -185,31 +183,33 @@ public interface IGrid<T> extends Iterable<T> {
Stream<ILocation> locationStream();
/**
* Create a parallel {@link Stream} with all the locations in this grid.
* <p>
* All locations obtained through the stream are guaranteed to be valid
* according to {@link #isValid(ILocation)}.
*
* @return A stream
* @see {@link java.util.Collection#parallelStream()}
* Set the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param element
* The contents the cell is to have.
* @throws IndexOutOfBoundsException
* if !isValid(pos)
*/
Stream<ILocation> locationParallelStream();
void set(ILocation pos, T element);
/**
* Create a {@link Stream} with all the elements in this grid.
*
* @return A stream
* Set the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param element
* The contents the cell is to have.
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
Stream<T> elementStream();
/**
* Create a parallel {@link Stream} with all the elements in this grid.
*
* @return A stream
* @see {@link java.util.Collection#parallelStream()}
*/
Stream<T> elementParallelStream();
IArea getArea();
void set(int x, int y, T element);
}

View File

@ -32,6 +32,21 @@ import java.util.List;
*/
public interface ILocation extends IPosition {
/**
* Iterate over neighbours in eight directions.
* <p>
* (The iterator may yield fewer than eight locations if the current location is
* at the edge of its containing area.
*
* @return The neighbours in the eight cardinal and intercardinal directions
* ({@link GridDirection#NORTH}, @link GridDirection#SOUTH}, @link
* GridDirection#EAST}, @link GridDirection#WEST},
* {@link GridDirection#NORTHEAST}, @link
* GridDirection#NORTHWEST}, @link GridDirection#SOUTHEAST}, @link
* GridDirection#SOUTHWEST}, )
*/
Collection<ILocation> allNeighbours();
/**
* Checks whether you can go towards direction dir without going outside the
* area bounds
@ -41,6 +56,22 @@ public interface ILocation extends IPosition {
*/
boolean canGo(GridDirection dir);
/**
* Iterate over north/south/east/west neighbours.
* <p>
* (The iterator may yield fewer than four locations if the current location is
* at the edge of its containing area.
*
* @return The neighbours in the four cardinal directions
* ({@link GridDirection#NORTH}, @link GridDirection#SOUTH}, @link
* GridDirection#EAST}, @link GridDirection#WEST}
*/
Collection<ILocation> cardinalNeighbours();
IArea getArea();
int getIndex();
/**
* Return the next location in direction dir.
* <p>
@ -56,37 +87,6 @@ public interface ILocation extends IPosition {
*/
ILocation go(GridDirection dir);
/**
* Iterate over north/south/east/west neighbours.
* <p>
* (The iterator may yield fewer than four locations if the current location is
* at the edge of its containing area.
*
* @return The neighbours in the four cardinal directions
* ({@link GridDirection#NORTH}, @link GridDirection#SOUTH}, @link
* GridDirection#EAST}, @link GridDirection#WEST}
*/
Collection<ILocation> cardinalNeighbours();
/**
* Iterate over neighbours in eight directions.
* <p>
* (The iterator may yield fewer than eight locations if the current location is
* at the edge of its containing area.
*
* @return The neighbours in the eight cardinal and intercardinal directions
* ({@link GridDirection#NORTH}, @link GridDirection#SOUTH}, @link
* GridDirection#EAST}, @link GridDirection#WEST},
* {@link GridDirection#NORTHEAST}, @link
* GridDirection#NORTHWEST}, @link GridDirection#SOUTHEAST}, @link
* GridDirection#SOUTHWEST}, )
*/
Collection<ILocation> allNeighbours();
IArea getArea();
int getIndex();
/**
* Find the grid cells between this location (exclusive) and another location
* (inclusive).

View File

@ -5,6 +5,20 @@ import java.util.function.Predicate;
import java.util.stream.Collectors;
public interface IMultiGrid<T> extends IGrid<List<T>> {
/**
* Add to the cell at the given location.
*
* @param loc
* The (x,y) position of the grid cell to get the contents of.
* @param element
* An element to be added to the cell.
* @throws IndexOutOfBoundsException
* if !isValid(loc)
*/
default void add(ILocation loc, T element) {
get(loc).add(element);
}
/**
* Add to the cell at the given x,y location.
*
@ -23,35 +37,23 @@ public interface IMultiGrid<T> extends IGrid<List<T>> {
}
/**
* Add to the cell at the given location.
* Check if a cell contains an element.
*
*
* @param loc
* The (x,y) position of the grid cell to get the contents of.
* @param element
* An element to be added to the cell.
* The (x,y) position of the grid cell
* @param predicate
* Search predicate.
* @return true if an element matching the predicate was found
* @throws IndexOutOfBoundsException
* if !isValid(loc)
*/
default void add(ILocation loc, T element) {
get(loc).add(element);
}
/**
* Check if a cell contains an element.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param element
* An element to search for.
* @return true if element is at the given location
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
default boolean contains(int x, int y, T element) {
return get(x, y).contains(element);
default boolean contains(ILocation loc, Predicate<T> predicate) {
for (T t : get(loc)) {
if (predicate.test(t))
return true;
}
return false;
}
/**
@ -91,6 +93,24 @@ public interface IMultiGrid<T> extends IGrid<List<T>> {
/**
* Check if a cell contains an element.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param element
* An element to search for.
* @return true if element is at the given location
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
default boolean contains(int x, int y, T element) {
return get(x, y).contains(element);
}
/**
* Get all elements in a cell that match the predicate
*
*
* @param loc
* The (x,y) position of the grid cell
@ -100,30 +120,44 @@ public interface IMultiGrid<T> extends IGrid<List<T>> {
* @throws IndexOutOfBoundsException
* if !isValid(loc)
*/
default boolean contains(ILocation loc, Predicate<T> predicate) {
for (T t : get(loc)) {
if (predicate.test(t))
return true;
}
return false;
default List<T> get(ILocation loc, Predicate<T> predicate) {
return get(loc).stream().filter(predicate).collect(Collectors.toList());
}
/**
* Remove an element from the cell at the given x,y location.
* Check if a cell contains an element.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell
* @param element
* An element to be removed from the cell
* @return Number of elements removed
* The (x,y) position of the grid cell to get the contents of.
* @param predicate
* Search predicate.
* @return true if an element matching the predicate was found
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
default int remove(int x, int y, T element) {
return get(x, y).remove(element) ? 1 : 0;
default List<T> get(int x, int y, Predicate<T> predicate) {
return get(this.getArea().location(x, y), predicate);
}
/**
* Remove an element from the cell at the given location.
*
* @param loc
* The location of the grid cell
* @param predicate
* Predicate which should be true for elements to be removed
* @return Number of elements removed
* @throws IndexOutOfBoundsException
* if !isValid(loc)
*/
default int remove(ILocation loc, Predicate<T> predicate) {
List<T> list = get(loc);
int s = list.size();
get(loc).removeIf(predicate);
return s - list.size();
}
/**
@ -160,55 +194,21 @@ public interface IMultiGrid<T> extends IGrid<List<T>> {
}
/**
* Remove an element from the cell at the given location.
*
* @param loc
* The location of the grid cell
* @param predicate
* Predicate which should be true for elements to be removed
* @return Number of elements removed
* @throws IndexOutOfBoundsException
* if !isValid(loc)
*/
default int remove(ILocation loc, Predicate<T> predicate) {
List<T> list = get(loc);
int s = list.size();
get(loc).removeIf(predicate);
return s - list.size();
}
/**
* Check if a cell contains an element.
* Remove an element from the cell at the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight(). x must be
* greater than or equal to 0 and less than getWidth().
*
* @param pos
* The (x,y) position of the grid cell to get the contents of.
* @param predicate
* Search predicate.
* @return true if an element matching the predicate was found
* The (x,y) position of the grid cell
* @param element
* An element to be removed from the cell
* @return Number of elements removed
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
default List<T> get(int x, int y, Predicate<T> predicate) {
return get(this.getArea().location(x, y), predicate);
}
/**
* Get all elements in a cell that match the predicate
*
*
* @param loc
* The (x,y) position of the grid cell
* @param predicate
* Search predicate.
* @return true if an element matching the predicate was found
* @throws IndexOutOfBoundsException
* if !isValid(loc)
*/
default List<T> get(ILocation loc, Predicate<T> predicate) {
return get(loc).stream().filter(predicate).collect(Collectors.toList());
default int remove(int x, int y, T element) {
return get(x, y).remove(element) ? 1 : 0;
}
}

View File

@ -2,6 +2,42 @@ package inf101.v18.grid;
public interface IPosition {
/**
* @param obj
* Another object
* @return true if obj is also an IPosition, and the x and y coordinates are
* equal
*/
@Override
boolean equals(Object obj);
/**
* Find the Euclidian distance between the midpoint of this position and another
* position.
*
* The distance is computed with the Pythagorean formula, with the assumption
* that the grid cells are square shaped with <em>width</em> = <em>height</em> =
* 1. For example, the distance from (0,0) to (3,5) is (3²+5²) = 5.83.
*
* @param other
* @return Euclidian distance between this and other's midpoints
*/
double geometricDistanceTo(IPosition other);
/**
* Gets the x-coordinate
*
* @return
*/
int getX();
/**
* Gets the y-coordinate
*
* @return
*/
int getY();
/**
* Find the distance in grid cells to another location.
*
@ -19,6 +55,9 @@ public interface IPosition {
*/
int gridDistanceTo(IPosition other);
@Override
int hashCode();
/**
* Find the number of non-diagonal steps needed to go from this location the
* other location.
@ -37,44 +76,8 @@ public interface IPosition {
*/
int stepDistanceTo(IPosition other);
/**
* Find the Euclidian distance between the midpoint of this position and another
* position.
*
* The distance is computed with the Pythagorean formula, with the assumption
* that the grid cells are square shaped with <em>width</em> = <em>height</em> =
* 1. For example, the distance from (0,0) to (3,5) is (3²+5²) = 5.83.
*
* @param other
* @return Euclidian distance between this and other's midpoints
*/
double geometricDistanceTo(IPosition other);
/**
* @param obj
* Another object
* @return true if obj is also an IPosition, and the x and y coordinates are
* equal
*/
boolean equals(Object obj);
/**
* Gets the x-coordinate
*
* @return
*/
int getX();
/**
* Gets the y-coordinate
*
* @return
*/
int getY();
int hashCode();
/** @return Position as a string, "(x,y)" */
@Override
String toString();
}

View File

@ -11,18 +11,6 @@ public class MyGrid<T> implements IGrid<T> {
private final IArea area;
private final List<T> cells;
/**
* Construct a grid with the given dimensions.
*
* @param width
* @param height
* @param initElement
* What the cells should initially hold (possibly null)
*/
public MyGrid(int width, int height, T initElement) {
this(new RectArea(width, height), initElement);
}
/**
* Construct a grid with the given dimensions.
*
@ -40,8 +28,16 @@ public class MyGrid<T> implements IGrid<T> {
* @param initialiser
* The initialiser function
*/
public MyGrid(int width, int height, Function<ILocation, T> initialiser) {
this(new RectArea(width, height), initialiser);
public MyGrid(IArea area, Function<ILocation, T> initialiser) {
if (area == null || initialiser == null) {
throw new IllegalArgumentException();
}
this.area = area;
this.cells = new ArrayList<T>(area.getSize());
for (ILocation loc : area) {
cells.add(initialiser.apply(loc));
}
}
/**
@ -81,16 +77,20 @@ public class MyGrid<T> implements IGrid<T> {
* @param initialiser
* The initialiser function
*/
public MyGrid(IArea area, Function<ILocation, T> initialiser) {
if (area == null || initialiser == null) {
throw new IllegalArgumentException();
}
public MyGrid(int width, int height, Function<ILocation, T> initialiser) {
this(new RectArea(width, height), initialiser);
}
this.area = area;
this.cells = new ArrayList<T>(area.getSize());
for (ILocation loc : area) {
cells.add(initialiser.apply(loc));
}
/**
* Construct a grid with the given dimensions.
*
* @param width
* @param height
* @param initElement
* What the cells should initially hold (possibly null)
*/
public MyGrid(int width, int height, T initElement) {
this(new RectArea(width, height), initElement);
}
@Override
@ -101,79 +101,13 @@ public class MyGrid<T> implements IGrid<T> {
}
@Override
public T get(int x, int y) {
return cells.get(area.toIndex(x, y));
public Stream<T> elementParallelStream() {
return cells.parallelStream();
}
@Override
public int getHeight() {
return area.getHeight();
}
@Override
public int getWidth() {
return area.getWidth();
}
@Override
public void set(int x, int y, T elem) {
cells.set(area.toIndex(x, y), elem);
}
@Override
public Iterator<T> iterator() {
return cells.iterator();
}
@Override
public T get(ILocation loc) {
if (loc.getArea() == area)
return cells.get(loc.getIndex());
else
return cells.get(area.toIndex(loc.getX(), loc.getY()));
}
@Override
public T getOrDefault(int x, int y, T defaultResult) {
T r = null;
if (isValid(x, y))
r = get(x, y);
if (r != null)
return r;
else
return defaultResult;
}
@Override
public T getOrDefault(ILocation loc, T defaultResult) {
if (loc.getArea() == area) {
T r = cells.get(loc.getIndex());
if (r != null)
return r;
else
return defaultResult;
} else {
return getOrDefault(loc.getX(), loc.getY(), defaultResult);
}
}
@Override
public boolean isValid(ILocation loc) {
return loc.getArea() == area || area.contains(loc.getX(), loc.getY());
}
@Override
public boolean isValid(int x, int y) {
return area.contains(x, y);
}
@Override
public void set(ILocation loc, T element) {
if (loc.getArea() == area) {
cells.set(loc.getIndex(), element);
} else {
set(loc.getX(), loc.getY(), element);
}
public Stream<T> elementStream() {
return cells.stream();
}
@Override
@ -193,6 +127,78 @@ public class MyGrid<T> implements IGrid<T> {
}
}
@Override
public T get(ILocation loc) {
if (loc.getArea() == area)
return cells.get(loc.getIndex());
else
return cells.get(area.toIndex(loc.getX(), loc.getY()));
}
@Override
public T get(int x, int y) {
return cells.get(area.toIndex(x, y));
}
@Override
public IArea getArea() {
return area;
}
@Override
public int getHeight() {
return area.getHeight();
}
@Override
public T getOrDefault(ILocation loc, T defaultResult) {
if (loc.getArea() == area) {
T r = cells.get(loc.getIndex());
if (r != null)
return r;
else
return defaultResult;
} else {
return getOrDefault(loc.getX(), loc.getY(), defaultResult);
}
}
@Override
public T getOrDefault(int x, int y, T defaultResult) {
T r = null;
if (isValid(x, y))
r = get(x, y);
if (r != null)
return r;
else
return defaultResult;
}
@Override
public int getWidth() {
return area.getWidth();
}
@Override
public boolean isValid(ILocation loc) {
return loc.getArea() == area || area.contains(loc.getX(), loc.getY());
}
@Override
public boolean isValid(int x, int y) {
return area.contains(x, y);
}
@Override
public Iterator<T> iterator() {
return cells.iterator();
}
@Override
public Stream<ILocation> locationParallelStream() {
return area.parallelStream();
}
@Override
public Iterable<ILocation> locations() {
return area;
@ -204,22 +210,16 @@ public class MyGrid<T> implements IGrid<T> {
}
@Override
public Stream<T> elementStream() {
return cells.stream();
public void set(ILocation loc, T element) {
if (loc.getArea() == area) {
cells.set(loc.getIndex(), element);
} else {
set(loc.getX(), loc.getY(), element);
}
}
@Override
public IArea getArea() {
return area;
}
@Override
public Stream<ILocation> locationParallelStream() {
return area.parallelStream();
}
@Override
public Stream<T> elementParallelStream() {
return cells.parallelStream();
public void set(int x, int y, T elem) {
cells.set(area.toIndex(x, y), elem);
}
}

View File

@ -8,10 +8,164 @@ import java.util.List;
import java.util.stream.Stream;
public class RectArea implements IArea {
/** A class to represent an (x, y)-location on a grid. */
class Location implements ILocation {
/** value of the x-coordinate */
protected final int x;
/** value of the y-coordinate */
protected final int y;
protected final int idx;
protected final int edgeMask;
/**
* Main constructor. Initializes a new {@link #Location} objects with the
* corresponding values of x and y.
*
* @param x
* X coordinate
* @param y
* Y coordinate
* @param idx
* 1-dimensional index
* @param edgeMask
* mask with bits {@link RectArea#N}, {@link RectArea#S},
* {@link RectArea#E}, {@link RectArea#W} set if we're on the
* corresponding edge of the area
*/
Location(int x, int y, int idx, int edgeMask) {
this.x = x;
this.y = y;
this.idx = idx;
this.edgeMask = edgeMask;
}
@Override
public Collection<ILocation> allNeighbours() {
Collection<ILocation> ns = new ArrayList<>(8);
for (GridDirection d : GridDirection.EIGHT_DIRECTIONS) {
if (canGo(d))
ns.add(go(d));
}
return ns;
}
@Override
public boolean canGo(GridDirection dir) {
return (edgeMask & dir.getMask()) == 0;
}
@Override
public Collection<ILocation> cardinalNeighbours() {
Collection<ILocation> ns = new ArrayList<>(4);
for (GridDirection d : GridDirection.FOUR_DIRECTIONS) {
if (canGo(d))
ns.add(go(d));
}
return ns;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof IPosition)) {
return false;
}
IPosition other = (IPosition) obj;
if (x != other.getX()) {
return false;
}
if (y != other.getY()) {
return false;
}
return true;
}
@Override
public double geometricDistanceTo(IPosition other) {
return Math.sqrt(Math.pow(this.x - other.getX(), 2) + Math.pow(this.y - other.getY(), 2));
}
@Override
public IArea getArea() {
return RectArea.this;
}
@Override
public int getIndex() {
return idx;
}
@Override
public int getX() {
return x;
}
@Override
public int getY() {
return y;
}
@Override
public ILocation go(GridDirection dir) {
return location(x + dir.getDx(), y + dir.getDy());
}
@Override
public int gridDistanceTo(IPosition other) {
return Math.max(Math.abs(this.x - other.getX()), Math.abs(this.y - other.getY()));
}
@Override
public List<ILocation> gridLineTo(ILocation other) {
if (!contains(other))
throw new IllegalArgumentException();
int distX = other.getX() - x;
int distY = other.getY() - y;
int length = Math.max(Math.abs(distX), Math.abs(distY));
List<ILocation> line = new ArrayList<>(length);
if (length == 0)
return line;
double dx = (double) distX / (double) length;
double dy = (double) distY / (double) length;
// System.out.printf("dx=%g, dy=%g, length=%d%n", dx, dy, length);
for (int i = 1; i <= length; i++) {
line.add(location(x + (int) Math.round(dx * i), y + (int) Math.round(dy * i)));
}
return line;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public int stepDistanceTo(IPosition other) {
return Math.abs(this.x - other.getX()) + Math.abs(this.y - other.getY());
}
@Override
public String toString() {
return "(x=" + x + ",y=" + y + ")";
}
}
protected final int width;
protected final int height;
protected final int size;
protected final List<ILocation> locs;
protected final boolean hWrap, vWrap;
public RectArea(int width, int height) {
@ -86,15 +240,16 @@ public class RectArea implements IArea {
}
@Override
public int getHeight() {
return height;
public ILocation fromIndex(int i) {
if (i >= 0 && i < size)
return locs.get(i);
else
throw new IndexOutOfBoundsException("" + i);
}
@Override
public int toIndex(int x, int y) {
x = checkX(x);
y = checkY(y);
return y * width + x;
public int getHeight() {
return height;
}
@Override
@ -112,6 +267,41 @@ public class RectArea implements IArea {
return locs.iterator();
}
@Override
public ILocation location(int x, int y) {
if (x < 0 || x >= width || y < 0 || y >= height)
throw new IndexOutOfBoundsException("(" + x + "," + y + ")");
int i = x + y * width;
return locs.get(i);
}
@Override
public List<ILocation> locations() {
return locs; // (OK since locs has been through Collections.unmodifiableList())
}
@Override
public Iterable<ILocation> neighboursOf(ILocation pos) {
return pos.allNeighbours();
}
@Override
public Stream<ILocation> parallelStream() {
return locs.parallelStream();
}
@Override
public Stream<ILocation> stream() {
return locs.stream();
}
@Override
public int toIndex(int x, int y) {
x = checkX(x);
y = checkY(y);
return y * width + x;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
@ -154,193 +344,4 @@ public class RectArea implements IArea {
}
}
/** A class to represent an (x, y)-location on a grid. */
class Location implements ILocation {
/** value of the x-coordinate */
protected final int x;
/** value of the y-coordinate */
protected final int y;
protected final int idx;
protected final int edgeMask;
/**
* Main constructor. Initializes a new {@link #Location} objects with the
* corresponding values of x and y.
*
* @param x
* X coordinate
* @param y
* Y coordinate
* @param idx
* 1-dimensional index
* @param edgeMask
* mask with bits {@link RectArea#N}, {@link RectArea#S},
* {@link RectArea#E}, {@link RectArea#W} set if we're on the
* corresponding edge of the area
*/
Location(int x, int y, int idx, int edgeMask) {
this.x = x;
this.y = y;
this.idx = idx;
this.edgeMask = edgeMask;
}
@Override
public int gridDistanceTo(IPosition other) {
return Math.max(Math.abs(this.x - other.getX()), Math.abs(this.y - other.getY()));
}
@Override
public int stepDistanceTo(IPosition other) {
return Math.abs(this.x - other.getX()) + Math.abs(this.y - other.getY());
}
@Override
public double geometricDistanceTo(IPosition other) {
return Math.sqrt(Math.pow(this.x - other.getX(), 2) + Math.pow(this.y - other.getY(), 2));
}
@Override
public List<ILocation> gridLineTo(ILocation other) {
if (!contains(other))
throw new IllegalArgumentException();
int distX = other.getX() - x;
int distY = other.getY() - y;
int length = Math.max(Math.abs(distX), Math.abs(distY));
List<ILocation> line = new ArrayList<>(length);
if (length == 0)
return line;
double dx = (double) distX / (double) length;
double dy = (double) distY / (double) length;
// System.out.printf("dx=%g, dy=%g, length=%d%n", dx, dy, length);
for (int i = 1; i <= length; i++) {
line.add(location(x + (int) Math.round(dx * i), y + (int) Math.round(dy * i)));
}
return line;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof IPosition)) {
return false;
}
IPosition other = (IPosition) obj;
if (x != other.getX()) {
return false;
}
if (y != other.getY()) {
return false;
}
return true;
}
@Override
public int getX() {
return x;
}
@Override
public int getY() {
return y;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public boolean canGo(GridDirection dir) {
return (edgeMask & dir.getMask()) == 0;
}
@Override
public ILocation go(GridDirection dir) {
return location(x + dir.getDx(), y + dir.getDy());
}
@Override
public String toString() {
return "(x=" + x + ",y=" + y + ")";
}
@Override
public Collection<ILocation> cardinalNeighbours() {
Collection<ILocation> ns = new ArrayList<>(4);
for (GridDirection d : GridDirection.FOUR_DIRECTIONS) {
if (canGo(d))
ns.add(go(d));
}
return ns;
}
@Override
public Collection<ILocation> allNeighbours() {
Collection<ILocation> ns = new ArrayList<>(8);
for (GridDirection d : GridDirection.EIGHT_DIRECTIONS) {
if (canGo(d))
ns.add(go(d));
}
return ns;
}
@Override
public IArea getArea() {
return RectArea.this;
}
@Override
public int getIndex() {
return idx;
}
}
@Override
public ILocation location(int x, int y) {
if (x < 0 || x >= width || y < 0 || y >= height)
throw new IndexOutOfBoundsException("(" + x + "," + y + ")");
int i = x + y * width;
return locs.get(i);
}
@Override
public Stream<ILocation> stream() {
return locs.stream();
}
@Override
public Stream<ILocation> parallelStream() {
return locs.parallelStream();
}
@Override
public Iterable<ILocation> neighboursOf(ILocation pos) {
return pos.allNeighbours();
}
@Override
public ILocation fromIndex(int i) {
if (i >= 0 && i < size)
return locs.get(i);
else
throw new IndexOutOfBoundsException("" + i);
}
@Override
public List<ILocation> locations() {
return locs; // (OK since locs has been through Collections.unmodifiableList())
}
}

View File

@ -23,32 +23,6 @@ public class AreaRetting {
}
public void uniqueLocationsProperty(IArea area) {
Set<ILocation> set = new HashSet<>();
for (ILocation l : area) {
assertTrue("Location should be unique: " + l, set.add(l));
}
}
public void validLocationsProperty(IArea area) {
for (ILocation l : area) {
assertTrue("Location should be in area: " + l, area.contains(l));
assertTrue("Location should be in area: " + l, area.contains(l.getX(), l.getY()));
}
}
public void neighboursDistProperty(ILocation loc) {
for (ILocation l : loc.allNeighbours()) {
assertEquals(1, loc.gridDistanceTo(l));
}
}
public void neighboursSymmetryProperty(ILocation loc) {
for (ILocation l : loc.allNeighbours()) {
assertTrue("My neighbour should have me as a neighbour", l.allNeighbours().contains(loc));
}
}
public void canGoProperty(ILocation l, GridDirection dir) {
int x = l.getX() + dir.getDx();
int y = l.getY() + dir.getDy();
@ -61,33 +35,23 @@ public class AreaRetting {
}
}
@Test
public void uniqueLocations() {
for (int i = 0; i < N / 10; i++) {
IArea area = areaGen.generate();
uniqueLocationsProperty(area);
}
public void distanceProperty(ILocation l1, ILocation l2) {
assertEquals(l1.gridDistanceTo(l2), l2.gridDistanceTo(l1));
assertEquals(l1.stepDistanceTo(l2), l2.stepDistanceTo(l1));
assertTrue(l1.gridDistanceTo(l2) <= l1.stepDistanceTo(l2));
assertTrue(l1.gridDistanceTo(l2) <= l1.geometricDistanceTo(l2));
}
@Test
public void validLocations() {
for (int i = 0; i < N / 10; i++) {
IArea area = areaGen.generate();
validLocationsProperty(area);
}
}
@Test
public void locationsTest() {
for (int i = 0; i < 10; i++) {
IArea area = areaGen.generate();
for (ILocation l : area) {
neighboursDistProperty(l);
neighboursSymmetryProperty(l);
for (GridDirection d : GridDirection.EIGHT_DIRECTIONS)
canGoProperty(l, d);
}
public void gridLineProperty(ILocation l1, ILocation l2) {
// System.out.println(l1.toString() + " .. " + l2.toString());
List<ILocation> line = l1.gridLineTo(l2);
assertEquals(l1.gridDistanceTo(l2), line.size());
ILocation last = l1;
for (ILocation l : line) {
assertEquals(1, last.gridDistanceTo(l));
last = l;
}
assertEquals(l2, last);
}
@Test
@ -104,22 +68,58 @@ public class AreaRetting {
}
}
public void gridLineProperty(ILocation l1, ILocation l2) {
// System.out.println(l1.toString() + " .. " + l2.toString());
List<ILocation> line = l1.gridLineTo(l2);
assertEquals(l1.gridDistanceTo(l2), line.size());
ILocation last = l1;
for (ILocation l : line) {
assertEquals(1, last.gridDistanceTo(l));
last = l;
@Test
public void locationsTest() {
for (int i = 0; i < 10; i++) {
IArea area = areaGen.generate();
for (ILocation l : area) {
neighboursDistProperty(l);
neighboursSymmetryProperty(l);
for (GridDirection d : GridDirection.EIGHT_DIRECTIONS)
canGoProperty(l, d);
}
}
assertEquals(l2, last);
}
public void distanceProperty(ILocation l1, ILocation l2) {
assertEquals(l1.gridDistanceTo(l2), l2.gridDistanceTo(l1));
assertEquals(l1.stepDistanceTo(l2), l2.stepDistanceTo(l1));
assertTrue(l1.gridDistanceTo(l2) <= l1.stepDistanceTo(l2));
assertTrue(l1.gridDistanceTo(l2) <= l1.geometricDistanceTo(l2));
public void neighboursDistProperty(ILocation loc) {
for (ILocation l : loc.allNeighbours()) {
assertEquals(1, loc.gridDistanceTo(l));
}
}
public void neighboursSymmetryProperty(ILocation loc) {
for (ILocation l : loc.allNeighbours()) {
assertTrue("My neighbour should have me as a neighbour", l.allNeighbours().contains(loc));
}
}
@Test
public void uniqueLocations() {
for (int i = 0; i < N / 10; i++) {
IArea area = areaGen.generate();
uniqueLocationsProperty(area);
}
}
public void uniqueLocationsProperty(IArea area) {
Set<ILocation> set = new HashSet<>();
for (ILocation l : area) {
assertTrue("Location should be unique: " + l, set.add(l));
}
}
@Test
public void validLocations() {
for (int i = 0; i < N / 10; i++) {
IArea area = areaGen.generate();
validLocationsProperty(area);
}
}
public void validLocationsProperty(IArea area) {
for (ILocation l : area) {
assertTrue("Location should be in area: " + l, area.contains(l));
assertTrue("Location should be in area: " + l, area.contains(l.getX(), l.getY()));
}
}
}

View File

@ -19,37 +19,6 @@ public class GridRetting {
private IGenerator<String> strGen = new StringGenerator();
private IGenerator<IGrid<String>> gridGen = new GridGenerator<String>(strGen);
/** A set on (x1,y1) doesn't affect a get on a different (x2,y2) */
public <T> void setGetIndependentProperty(IGrid<T> grid, ILocation l1, ILocation l2, T val) {
if (!l1.equals(l2)) {
T s = grid.get(l2);
grid.set(l1, val);
assertEquals(s, grid.get(l2));
}
}
@Test
public void setGetIndependentTest() {
for (int j = 0; j < 10; j++) {
IGrid<String> grid = gridGen.generate();
IGenerator<ILocation> lGen = new LocationGenerator(grid.getArea());
for (int i = 0; i < N; i++) {
ILocation l1 = lGen.generate();
ILocation l2 = lGen.generate();
String s = strGen.generate();
setGetIndependentProperty(grid, l1, l2, s);
}
}
}
/** get(x,y) is val after set(x, y, val) */
public <T> void setGetProperty(IGrid<T> grid, ILocation l, T val) {
grid.set(l, val);
assertEquals(val, grid.get(l));
}
public <T> void fillProperty1(IGrid<T> grid, T val) {
grid.fill(val);
for (ILocation l : grid.locations()) {
@ -83,12 +52,37 @@ public class GridRetting {
}
}
@Test
public void uniqueLocations() {
for (int i = 0; i < N / 10; i++) {
/** A set on (x1,y1) doesn't affect a get on a different (x2,y2) */
public <T> void setGetIndependentProperty(IGrid<T> grid, ILocation l1, ILocation l2, T val) {
if (!l1.equals(l2)) {
T s = grid.get(l2);
grid.set(l1, val);
assertEquals(s, grid.get(l2));
}
}
@Test
public void setGetIndependentTest() {
for (int j = 0; j < 10; j++) {
IGrid<String> grid = gridGen.generate();
IGenerator<ILocation> lGen = new LocationGenerator(grid.getArea());
for (int i = 0; i < N; i++) {
ILocation l1 = lGen.generate();
ILocation l2 = lGen.generate();
String s = strGen.generate();
setGetIndependentProperty(grid, l1, l2, s);
}
}
}
/** get(x,y) is val after set(x, y, val) */
public <T> void setGetProperty(IGrid<T> grid, ILocation l, T val) {
grid.set(l, val);
assertEquals(val, grid.get(l));
}
/** Test that get gives back the same value after set. */
@Test
public void setGetTest() {
@ -105,4 +99,10 @@ public class GridRetting {
}
}
@Test
public void uniqueLocations() {
for (int i = 0; i < N / 10; i++) {
}
}
}

View File

@ -7,14 +7,26 @@ import inf101.v18.gfx.Screen;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.textmode.Printer;
import inf101.v18.gfx.textmode.TextMode;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.game.Game;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
// you might want to tune these options
public static final boolean MAIN_USE_BACKGROUND_GRID = true;
public static final boolean MAP_AUTO_SCALE_ITEM_DRAW = true;
public static final boolean MAP_DRAW_ONLY_DIRTY_CELLS = false;
public static final TextMode MAIN_TEXT_MODE = TextMode.MODE_80X25;
public static final boolean DEBUG_TIME = false;
public static final int LINE_MAP_BOTTOM = 20;
public static final int LINE_STATUS = 21;
public static final int LINE_MSG1 = 22;
@ -23,23 +35,61 @@ public class Main extends Application {
public static final int LINE_DEBUG = 25;
public static final int COLUMN_MAP_END = 40;
public static final int COLUMN_RIGHTSIDE_START = 41;
private Screen screen;
private ITurtle painter;
private Printer printer;
private IGame game;
private boolean grid = true;
public static void main(String[] args) {
launch(args);
}
private Screen screen;
private ITurtle painter;
private Printer printer;
private Game game;
private boolean grid = MAIN_USE_BACKGROUND_GRID;
private boolean autoNextTurn = false;
private Timeline bigStepTimeline;
private Timeline smallStepTimeline;
private void setup() {
//
game = new Game(screen, painter, printer);
game.draw();
//
bigStepTimeline = new Timeline();
bigStepTimeline.setCycleCount(Timeline.INDEFINITE);
KeyFrame kf = new KeyFrame(Duration.millis(1000), (ActionEvent event) -> {
if (autoNextTurn) {
doTurn();
}
});
bigStepTimeline.getKeyFrames().add(kf);
// bigStepTimeline.playFromStart();
//
smallStepTimeline = new Timeline();
smallStepTimeline.setCycleCount(1);
kf = new KeyFrame(Duration.millis(1), (ActionEvent event) -> {
doTurn();
});
smallStepTimeline.getKeyFrames().add(kf);
// finally, start game
doTurn();
}
@Override
public void start(Stage primaryStage) throws Exception {
screen = Screen.startPaintScene(primaryStage, Screen.CONFIG_PIXELS_STEP_SCALED); // Screen.CONFIG_SCREEN_FULLSCREEN_NO_HINT);
printer = screen.createPrinter();
painter = screen.createPainter();
printer.setTextMode(TextMode.MODE_80X25, true);
printer.setTextMode(MAIN_TEXT_MODE, true);
// Font with emojis need separate download
// printer.setFont(Printer.FONT_SYMBOLA);
if (grid)
printer.drawCharCells();
printer.setAutoScroll(false);
@ -52,7 +102,7 @@ public class Main extends Application {
printer.cycleMode(true);
if (grid)
printer.drawCharCells();
// game.draw();
game.draw();
return true;
} else if (code == KeyCode.A) {
screen.cycleAspect();
@ -75,8 +125,7 @@ public class Main extends Application {
}
} else if (code == KeyCode.ENTER) {
try {
//game.doTurn();
// game.draw();
doTurn();
} catch (Exception e) {
printer.printAt(1, 25, "Exception: " + e.getMessage(), Color.RED);
e.printStackTrace();
@ -84,8 +133,8 @@ public class Main extends Application {
return true;
} else {
try {
// game.keyPressed(code);
// game.draw();
game.keyPressed(code);
doTurn();
} catch (Exception e) {
e.printStackTrace();
try {
@ -112,13 +161,24 @@ public class Main extends Application {
*/
setup();
// game = new Game(screen, painter, printer);
// game.draw();
primaryStage.show();
}
private void setup() {
public void doTurn() {
long t = System.currentTimeMillis();
boolean waitForPlayer = game.doTurn();
if (DEBUG_TIME)
System.out.println("doTurn() took " + (System.currentTimeMillis() - t) + "ms");
long t2 = System.currentTimeMillis();
game.draw();
if (DEBUG_TIME) {
System.out.println("draw() took " + (System.currentTimeMillis() - t2) + "ms");
System.out.println("doTurn()+draw() took " + (System.currentTimeMillis() - t) + "ms");
System.out.println("waiting for player? " + waitForPlayer);
}
if (!waitForPlayer)
smallStepTimeline.playFromStart(); // this will kickstart a new turn in a few milliseconds
}
public static String BUILTIN_MAP = "40 20\n" //

View File

@ -1,32 +1,132 @@
package inf101.v18.rogue101.events;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.objects.IItem;
/**
* Example implementation of events could be used to have more complex
* behaviour than just attack/defend/get damaged/pickup/drop.
*
* @author anya
*
* @param <T>
* Relevant extra data for this particular event
*/
public class GameEvent<T> implements IEvent<T> {
public static final String ATTACK_FAILURE = "attackFailure";
public static final String ATTACK_SUCCESS = "attackSuccess";
public static final String DEFEND_FAILURE = "defendFailure";
public static final String DEFEND_SUCCESS = "defendSuccess";
public static final String EATEN = "eaten";
private String name;
private IItem source;
private IItem target;
/**
* Create and send events for an attack.
* <p>
* Both attacker and defender will receive appropriate events (ATTACK_* or
* DEFEND_*), depending on who won. The amount of damage is available through
* {@link #getData()}.
* <p>
* Attacker will be sent the "damage" that was actually done (as returned by
* defender's event handler)
* <p>
* Methods such as these could sensible be placed in IGame/Game.
*
* @param success
* True if attack succeeded (attacker "won")
* @param attacker
* @param defender
* @param damage
*/
public static void triggerAttack(boolean success, IItem attacker, IItem defender, int damage) {
if (success) {
Integer result = defender
.handleEvent(new GameEvent<Integer>(DEFEND_FAILURE, null, attacker, defender, damage));
if (result != null)
damage = result;
attacker.handleEvent(new GameEvent<Integer>(ATTACK_SUCCESS, null, attacker, defender, damage));
} else {
attacker.handleEvent(new GameEvent<Integer>(ATTACK_FAILURE, null, attacker, defender, 0));
defender.handleEvent(new GameEvent<Integer>(DEFEND_SUCCESS, null, attacker, defender, 0));
}
}
private final String name;
private final IItem source;
private final IItem target;
private T value;
private final IGame game;
/**
* Create a new game event
* @param name The name is used when checking which event this is / determine its meaning
* @param source The item that caused the event, or <code>null</code> if unknown/not relevant
* @param target The item that receives the event, or <code>null</code> if unknown/not relevant
* @param value Arbitrary extra data
*
* @param name
* The name is used when checking which event this is / determine its
* meaning
* @param game
* The game, or <code>null</code> if unknown/not relevant
* @param source
* The item that caused the event, or <code>null</code> if
* unknown/not relevant
* @param target
* The item that receives the event, or <code>null</code> if
* unknown/not relevant
* @param value
* Arbitrary extra data
*/
public GameEvent(String name, IItem source, IItem target, T value) {
public GameEvent(String name, IGame game, IItem source, IItem target, T value) {
this.name = name;
this.game = game;
this.source = source;
this.target = target;
this.value = value;
}
/**
* Create a new game event
*
* @param name
* The name is used when checking which event this is / determine its
* meaning
* @param source
* The item that caused the event, or <code>null</code> if
* unknown/not relevant
*/
public GameEvent(String name, IItem source) {
this(name, null, source, null, null);
}
/**
* Create a new game event
*
* @param name
* The name is used when checking which event this is / determine its
* meaning
* @param source
* The item that caused the event, or <code>null</code> if
* unknown/not relevant
* @param value
* Arbitrary extra data
*/
public GameEvent(String name, IItem source, T value) {
this(name, null, source, null, value);
}
@Override
public T getData() {
return value;
}
@Override
public String getEventName() {
return name;
}
@Override
public IGame getGame() {
return game;
}
@Override
public IItem getSource() {
return source;
@ -37,16 +137,6 @@ public class GameEvent<T> implements IEvent<T> {
return target;
}
@Override
public String getEventName() {
return name;
}
@Override
public T getData() {
return value;
}
@Override
public void setData(T value) {
this.value = value;

View File

@ -1,7 +1,30 @@
package inf101.v18.rogue101.events;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.objects.IItem;
/**
* An event is something that happens in the game, typically due to an actor
* taking some action (nut could also be used to transmit user input)
* <p>
* Storing the particular action and it's associated data in an object means
* that we can extend our system with many different kinds of actions and have
* many different reactions to those actions without having to all of the
* "game rule specific" stuff to all our interfaces and classes.
* <p>
* Our event objects let you store an extra (arbitrary) piece of data, giving
* more information about what happened. An event handler can also update this
* information, which is a possible way to report back to whomever caused the
* event in the first place. The event objects also contain an event name, and
* source/targets of the event (where relevant).
* <p>
* This system is fairly simplistic, and you're not expected to make use of it.
*
* @author anya
*
* @param <T>
* Type of the extra data
*/
public interface IEvent<T> {
/**
* @return Extra data stored in this event
@ -13,6 +36,16 @@ public interface IEvent<T> {
*/
String getEventName();
/**
* Not all events need to be connected to the game, but you can use this if you
* need it (e.g., for showing a message, or adding something to the map).
* <p>
* The result might be null.
*
* @return The game associated with this event, or null.
*/
IGame getGame();
/**
* The source is the item that caused the event
* <p>
@ -32,7 +65,8 @@ public interface IEvent<T> {
IItem getTarget();
/**
* @param value Extra data to store in this event
* @param value
* Extra data to store in this event
*/
void setData(T value);
}

View File

@ -1,5 +0,0 @@
package inf101.v18.rogue101.events;
public interface IPickedUpEvent extends IEvent {
}

View File

@ -9,21 +9,53 @@ public class Carrot implements IItem {
private int hp = getMaxHealth();
public void doTurn(IGame game) {
hp = Math.min(hp+1, getMaxHealth());
hp = Math.min(hp + 1, getMaxHealth());
}
@Override
public boolean draw(ITurtle painter, double w, double h) {
painter.save();
painter.turn(75);
double size = ((double) hp + getMaxHealth()) / (2.0 * getMaxHealth());
double carrotLength = size * h * .6;
double carrotWidth = size * h * .4;
painter.jump(-carrotLength / 6);
painter.shape().ellipse().width(carrotLength).height(carrotWidth).fillPaint(Color.ORANGE).fill();
painter.jump(carrotLength / 2);
painter.setInk(Color.FORESTGREEN);
for (int i = -1; i < 2; i++) {
painter.save();
painter.turn(20 * i);
painter.draw(carrotLength / 3);
painter.restore();
}
painter.restore();
return true;
}
@Override
public int getCurrentHealth() {
return hp;
}
@Override
public int getDefence() {
return 0;
}
@Override
public double getHealthStatus() {
return getCurrentHealth() / getMaxHealth();
}
@Override
public int getMaxHealth() {
return 10;
}
@Override
public int getCurrentHealth() {
return hp;
public String getName() {
return "carrot";
}
@Override
@ -36,45 +68,15 @@ public class Carrot implements IItem {
return "C";
}
@Override
public double getHealthStatus() {
return getCurrentHealth() / getMaxHealth();
}
@Override
public int handleDamage(IGame game, IItem source, int amount) {
hp -= amount;
if(hp < 0) {
if (hp < 0) {
// we're all eaten!
hp = -1;
}
return amount;
}
@Override
public boolean draw(ITurtle painter, double w, double h) {
painter.save();
painter.turn(75);
double size = ((double)hp+getMaxHealth())/(2.0*getMaxHealth());
double carrotLength = size*h*.6;
double carrotWidth = size*h*.2;
painter.jump(-carrotLength/6);
painter.shape().ellipse().width(carrotLength).height(carrotWidth).fillPaint(Color.ORANGE).fill();
painter.jump(carrotLength/2);
painter.setInk(Color.FORESTGREEN);
for(int i = -1; i < 2; i++) {
painter.save();
painter.turn(20*i);
painter.draw(carrotLength/3);
painter.restore();
}
painter.restore();
return true;
}
@Override
public String getName() {
return "carrot";
}
}

View File

@ -1,14 +1,22 @@
package inf101.v18.rogue101.examples;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.rogue101.events.GameEvent;
import inf101.v18.rogue101.events.IEvent;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.objects.IItem;
public class ExampleItem implements IItem {
private int hp = getMaxHealth();
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public int getCurrentHealth() {
return hp;
}
@Override
public int getDefence() {
return 0;
@ -20,8 +28,8 @@ public class ExampleItem implements IItem {
}
@Override
public int getCurrentHealth() {
return hp;
public String getName() {
return "strange model of an item";
}
@Override
@ -33,22 +41,11 @@ public class ExampleItem implements IItem {
public String getSymbol() {
return "X";
}
@Override
public int handleDamage(IGame game, IItem source, int amount) {
hp -= amount;
return amount;
}
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public String getName() {
return "strange model of an item";
}
}

View File

@ -6,8 +6,6 @@ import java.util.List;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.grid.GridDirection;
import inf101.v18.rogue101.events.GameEvent;
import inf101.v18.rogue101.events.IEvent;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.objects.IItem;
import inf101.v18.rogue101.objects.INonPlayer;
@ -32,13 +30,13 @@ public class Rabbit implements INonPlayer {
if (eaten > 0) {
System.out.println("ate carrot worth " + eaten + "!");
food += eaten;
game.displayMessage("You hear a faint crunching (" + getName() + " eats " + item.getArticle() + " " + item.getName() + ")");
game.displayMessage("You hear a faint crunching (" + getName() + " eats " + item.getArticle() + " "
+ item.getName() + ")");
return;
}
}
}
// TODO:
// TODO: prøv forskjellige varianter her
List<GridDirection> possibleMoves = new ArrayList<>();
for (GridDirection dir : GridDirection.FOUR_DIRECTIONS) {
if (game.canGo(dir))
@ -50,11 +48,21 @@ public class Rabbit implements INonPlayer {
}
}
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public int getAttack() {
return 1000;
}
@Override
public int getCurrentHealth() {
return hp;
}
@Override
public int getDamage() {
return 1000;
@ -71,8 +79,8 @@ public class Rabbit implements INonPlayer {
}
@Override
public int getCurrentHealth() {
return hp;
public String getName() {
return "rabbit";
}
@Override
@ -91,14 +99,4 @@ public class Rabbit implements INonPlayer {
return amount;
}
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public String getName() {
return "rabbit";
}
}

View File

@ -0,0 +1,489 @@
package inf101.v18.rogue101.game;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.Supplier;
import inf101.v18.gfx.Screen;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.gfxmode.TurtlePainter;
import inf101.v18.gfx.textmode.Printer;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.IGrid;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.Main;
import inf101.v18.rogue101.examples.Carrot;
import inf101.v18.rogue101.examples.Rabbit;
import inf101.v18.rogue101.map.GameMap;
import inf101.v18.rogue101.map.IGameMap;
import inf101.v18.rogue101.map.IMapView;
import inf101.v18.rogue101.map.MapReader;
import inf101.v18.rogue101.objects.Dust;
import inf101.v18.rogue101.objects.IActor;
import inf101.v18.rogue101.objects.IItem;
import inf101.v18.rogue101.objects.INonPlayer;
import inf101.v18.rogue101.objects.IPlayer;
import inf101.v18.rogue101.objects.Wall;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.KeyCode;
import javafx.scene.paint.Color;
public class Game implements IGame {
/**
* All the IActors that have things left to do this turn
*/
private List<IActor> actors = Collections.synchronizedList(new ArrayList<>());
/**
* For fancy solution to factory problem
*/
private Map<String, Supplier<IItem>> itemFactories = new HashMap<>();
/**
* Useful random generator
*/
private Random random = new Random();
/**
* The game map. {@link IGameMap} gives us a few more details than
* {@link IMapView} (write access to item lists); the game needs this but
* individual items don't.
*/
private IGameMap map;
private IActor currentActor;
private ILocation currentLocation;
private int movePoints = 0;
private final ITurtle painter;
private final Printer printer;
private int numPlayers = 0;
public Game(Screen screen, ITurtle painter, Printer printer) {
this.painter = painter;
this.printer = printer;
// TODO: (*very* optional) for advanced factory technique, use
// something like "itemFactories.put("R", () -> new Rabbit());"
// must be done *before* you read the map
// NOTE: in a more realistic situation, we will have multiple levels (one map
// per level), and (at least for a Roguelike game) the levels should be
// generated
//
// inputGrid will be filled with single-character strings indicating what (if
// anything)
// should be placed at that map square
IGrid<String> inputGrid = MapReader.readFile("maps/level1.txt");
if (inputGrid == null) {
System.err.println("Map not found falling back to builtin map");
inputGrid = MapReader.readString(Main.BUILTIN_MAP);
}
this.map = new GameMap(inputGrid.getArea());
for (ILocation loc : inputGrid.locations()) {
IItem item = createItem(inputGrid.get(loc));
if (item != null) {
map.add(loc, item);
}
}
}
public Game(String mapString) {
printer = new Printer(1280, 720);
painter = new TurtlePainter(1280, 720, null);
IGrid<String> inputGrid = MapReader.readString(mapString);
this.map = new GameMap(inputGrid.getArea());
for (ILocation loc : inputGrid.locations()) {
IItem item = createItem(inputGrid.get(loc));
if (item != null) {
map.add(loc, item);
}
}
}
@Override
public void addItem(IItem item) {
map.add(currentLocation, item);
}
@Override
public void addItem(String sym) {
IItem item = createItem(sym);
if (item != null)
map.add(currentLocation, item);
}
@Override
public ILocation attack(GridDirection dir, IItem target) {
ILocation loc = map.go(currentLocation, dir);
if (map.has(loc, target))
throw new IllegalMoveException("Target isn't there!");
// TODO: implement attack
map.clean(loc);
if (target.isDestroyed()) {
return move(dir);
} else {
movePoints--;
return currentLocation;
}
}
/**
* Begin a new game turn, or continue to the previous turn
*
* @return True if the game should wait for more user input
*/
public boolean doTurn() {
do {
if (actors.isEmpty()) {
// System.err.println("new turn!");
// no one in the queue, we're starting a new turn!
// first collect all the actors:
beginTurn();
}
// process actors one by one; for the IPlayer, we return and wait for keypresses
// Possible TODO: for INonPlayer, we could also return early (returning
// *false*), and then insert a little timer delay between each non-player move
// (the timer
// is already set up in Main)
while (!actors.isEmpty()) {
// get the next player or non-player in the queue
currentActor = actors.remove(0);
if (currentActor.isDestroyed()) // skip if it's dead
continue;
currentLocation = map.getLocation(currentActor);
if (currentLocation == null) {
displayDebug("doTurn(): Whoops! Actor has disappeared from the map: " + currentActor);
}
movePoints = 1; // everyone gets to do one thing
if (currentActor instanceof INonPlayer) {
// computer-controlled players do their stuff right away
((INonPlayer) currentActor).doTurn(this);
// remove any dead items from current location
map.clean(currentLocation);
} else if (currentActor instanceof IPlayer) {
if (currentActor.isDestroyed()) {
// a dead human player gets removed from the game
// TODO: you might want to be more clever here
displayMessage("YOU DIE!!!");
map.remove(currentLocation, currentActor);
currentActor = null;
currentLocation = null;
} else {
// For the human player, we need to wait for input, so we just return.
// Further keypresses will cause keyPressed() to be called, and once the human
// makes a move, it'll lose its movement point and doTurn() will be called again
//
// NOTE: currentActor and currentLocation are set to the IPlayer (above),
// so the game remembers who the player is whenever new keypresses occur. This
// is also how e.g., getLocalItems() work the game always keeps track of
// whose turn it is.
return true;
}
} else {
displayDebug("doTurn(): Hmm, this is a very strange actor: " + currentActor);
}
}
} while (numPlayers > 0); // we can safely repeat if we have players, since we'll return (and break out of
// the loop) once we hit the player
return true;
}
/**
* Go through the map and collect all the actors.
*/
private void beginTurn() {
numPlayers = 0;
// this extra fancy iteration over each map location runs *in parallel* on
// multicore systems!
// that makes some things more tricky, hence the "synchronized" block and
// "Collections.synchronizedList()" in the initialization of "actors".
// NOTE: If you want to modify this yourself, it might be a good idea to replace
// "parallelStream()" by "stream()", because weird things can happen when many
// things happen
// at the same time! (or do INF214 or DAT103 to learn about locks and threading)
map.getArea().parallelStream().forEach((loc) -> { // will do this for each location in map
List<IItem> list = map.getAllModifiable(loc); // all items at loc
Iterator<IItem> li = list.iterator(); // manual iterator lets us remove() items
while (li.hasNext()) { // this is what "for(IItem item : list)" looks like on the inside
IItem item = li.next();
if (item.getCurrentHealth() < 0) {
// normally, we expect these things to be removed when they are destroyed, so
// this shouldn't happen
synchronized (this) {
formatDebug("beginTurn(): found and removed leftover destroyed item %s '%s' at %s%n",
item.getName(), item.getSymbol(), loc);
}
li.remove();
map.remove(loc, item); // need to do this too, to update item map
} else if (item instanceof IPlayer) {
actors.add(0, (IActor) item); // we let the human player go first
synchronized (this) {
numPlayers++;
}
} else if (item instanceof IActor) {
actors.add((IActor) item); // add other actors to the end of the list
}
}
});
}
@Override
public boolean canGo(GridDirection dir) {
return map.canGo(currentLocation, dir);
}
@Override
public IItem createItem(String sym) {
switch (sym) {
case "#":
return new Wall();
case ".":
// TODO: add Dust
return null;
case "R":
return new Rabbit();
case "C":
return new Carrot();
case "@":
// TODO: add Player
case " ":
return null;
default:
// alternative/advanced method
Supplier<IItem> factory = itemFactories.get(sym);
if (factory != null) {
return factory.get();
} else {
System.err.println("createItem: Don't know how to create a '" + sym + "'");
return null;
}
}
}
@Override
public void displayDebug(String s) {
printer.clearLine(Main.LINE_DEBUG);
printer.printAt(1, Main.LINE_DEBUG, s, Color.DARKRED);
System.err.println(s);
}
@Override
public void displayMessage(String s) {
// it should be safe to print to lines Main.LINE_MSG1, Main.LINE_MSG2,
// Main.LINE_MSG3
// TODO: you can save the last three lines, and display/scroll them
printer.clearLine(Main.LINE_MSG1);
printer.printAt(1, Main.LINE_MSG1, s);
System.out.println("Message: «" + s + "»");
}
@Override
public void displayStatus(String s) {
printer.clearLine(Main.LINE_STATUS);
printer.printAt(1, Main.LINE_STATUS, s);
System.out.println("Status: «" + s + "»");
}
public void draw() {
map.draw(painter, printer);
}
@Override
public boolean drop(IItem item) {
if (item != null) {
map.add(currentLocation, item);
return true;
} else
return false;
}
@Override
public void formatDebug(String s, Object... args) {
displayDebug(String.format(s, args));
}
@Override
public void formatMessage(String s, Object... args) {
displayMessage(String.format(s, args));
}
@Override
public void formatStatus(String s, Object... args) {
displayStatus(String.format(s, args));
}
@Override
public int getHeight() {
return map.getHeight();
}
@Override
public List<IItem> getLocalItems() {
return map.getItems(currentLocation);
}
@Override
public ILocation getLocation() {
return currentLocation;
}
@Override
public ILocation getLocation(GridDirection dir) {
if (currentLocation.canGo(dir))
return currentLocation.go(dir);
else
return null;
}
/**
* Return the game map. {@link IGameMap} gives us a few more details than
* {@link IMapView} (write access to item lists); the game needs this but
* individual items don't.
*/
@Override
public IMapView getMap() {
return map;
}
@Override
public List<GridDirection> getPossibleMoves() {
// TODO
throw new UnsupportedOperationException();
}
@Override
public List<ILocation> getVisible() {
// TODO: maybe replace 3 by some sort of visibility range obtained from
// currentActor?
return map.getNeighbourhood(currentLocation, 3);
}
@Override
public int getWidth() {
return map.getWidth();
}
public void keyPressed(KeyCode code) {
// only an IPlayer/human can handle keypresses, and only if it's the human's
// turn
if (currentActor instanceof IPlayer) {
((IPlayer) currentActor).keyPressed(this, code); // do your thing
if (movePoints <= 0)
doTurn(); // proceed with turn if we're out of moves
}
}
@Override
public ILocation move(GridDirection dir) {
if (movePoints < 1)
throw new IllegalMoveException("You're out of moves!");
ILocation newLoc = map.go(currentLocation, dir);
map.remove(currentLocation, currentActor);
map.add(newLoc, currentActor);
currentLocation = newLoc;
movePoints--;
return currentLocation;
}
@Override
public IItem pickUp(IItem item) {
if (item != null && map.has(currentLocation, item)) {
// TODO: bruk getAttack()/getDefence() til å avgjøre om man får til å plukke opp
// tingen
// evt.: en IActor kan bare plukkes opp hvis den har /ingen helsepoeng igjen
map.remove(currentLocation, item);
return item;
} else {
return null;
}
}
@Override
public ILocation rangedAttack(GridDirection dir, IItem target) {
return currentLocation;
}
@Override
public ITurtle getPainter() {
return painter;
}
@Override
public Printer getPrinter() {
return printer;
}
@Override
public int[] getFreeTextAreaBounds() {
int[] area = new int[4];
area[0] = getWidth() + 1;
area[1] = 1;
area[2] = printer.getLineWidth();
area[3] = printer.getPageHeight() - 5;
return area;
}
@Override
public void clearFreeTextArea() {
printer.clearRegion(getWidth() + 1, 1, printer.getLineWidth() - getWidth(), printer.getPageHeight() - 5);
}
@Override
public void clearFreeGraphicsArea() {
painter.as(GraphicsContext.class).clearRect(getWidth() * printer.getCharWidth(), 0,
painter.getWidth() - getWidth() * printer.getCharWidth(),
(printer.getPageHeight() - 5) * printer.getCharHeight());
}
@Override
public double[] getFreeGraphicsAreaBounds() {
double[] area = new double[4];
area[0] = getWidth() * printer.getCharWidth();
area[1] = 0;
area[2] = painter.getWidth();
area[3] = getHeight() * printer.getCharHeight();
return area;
}
@Override
public IActor getActor() {
return currentActor;
}
public ILocation setCurrent(IActor actor) {
currentLocation = map.getLocation(actor);
if (currentLocation != null) {
currentActor = actor;
movePoints = 1;
}
return currentLocation;
}
public IActor setCurrent(ILocation loc) {
List<IActor> list = map.getActors(loc);
if (!list.isEmpty()) {
currentActor = list.get(0);
currentLocation = loc;
movePoints = 1;
}
return currentActor;
}
public IActor setCurrent(int x, int y) {
return setCurrent(map.getLocation(x, y));
}
@Override
public Random getRandom() {
return random;
}
}

View File

@ -1,23 +1,235 @@
package inf101.v18.rogue101.game;
import java.util.Collection;
import java.util.List;
import java.util.Random;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.textmode.Printer;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.map.IMapView;
import inf101.v18.rogue101.objects.IItem;
import inf101.v18.rogue101.objects.IActor;
import inf101.v18.rogue101.objects.INonPlayer;
import inf101.v18.rogue101.objects.IPlayer;
/**
* Game interface
* <p>
* The game has a map and a current {@link IActor} (the player or non-player
* whose turn it currently is). The game also knows the current location of the
* actor. Most methods that deal with the map will use this current location
* they are meant to be used by the current actor for exploring or interacting
* with its surroundings.
* <p>
* In other words, you should avoid calling most of these methods if you're not
* the current actor. You know you're the current actor when you're inside your
* {@link IPlayer#keyPressed()} or {@link INonPlayer#doTurn()} method.
*
* @author anya
*
*/
public interface IGame {
IMapView getMap();
/**
* Add an item to the current location
* <p>
* If the item is an actor, it won't move until the next turn.
*
* @param item
*/
void addItem(IItem item);
ILocation move(GridDirection dir);
/**
* Add a new item (identified by symbol) to the current location
* <p>
* If the item is an actor, it won't move until the next turn.
*
* @param sym
*/
void addItem(String sym);
/**
* Perform an attack on the target.
* <p>
* Will use your {@link IActor#getAttack()} against the target's
* {@link IItem#getDefence()}, and then possiblyu find the damage you're dealing
* with {@link IActor#getDamage()} and inform the target using
* {@link IItem#handleDamage(IGame, IItem, int)}
*
* @param dir
* Direction
* @param target
* A target item, which should be in the neighbouring square in the
* given direction
* @return Your new location if the attack resulted in you moving
*/
ILocation attack(GridDirection dir, IItem target);
ILocation rangedAttack(GridDirection dir, IItem target);
/**
* @param dir
* @return True if it's possible to move in the given direction
*/
boolean canGo(GridDirection dir);
Collection<IItem> getLocalItems();
/**
* Create a new item based on a text symbol
* <p>
* The item won't be added to the map unless you call {@link #addItem(IItem)}.
*
* @param symbol
* @return The new item
*/
IItem createItem(String symbol);
/**
* Displays a message in the debug area on the screen (bottom line)
*
* @param s
* A message
*/
void displayDebug(String s);
/**
* Displays a message in the message area on the screen (below the map and the
* status line)
*
* @param s
* A message
*/
void displayMessage(String s);
/**
* Displays a status message in the status area on the screen (right below the
* map)
*
* @param s
* A message
*/
void displayStatus(String s);
/**
* Displays a message in the message area on the screen (below the map and the
* status line)
*
* @param s
* A message
* @see String#format(String, Object...)
*/
void formatDebug(String s, Object... args);
/**
* Displays a formatted message in the message area on the screen (below the map
* and the status line)
*
* @param s
* A message
* @see String#format(String, Object...)
*/
void formatMessage(String s, Object... args);
/**
* Displays a formatted status message in the status area on the screen (right
* below the map)
*
* @param s
* A message
* @see String#format(String, Object...)
*/
void formatStatus(String s, Object... args);
/**
* Pick up an item
* <p>
* This should be used by IActors who want to pick up an item and carry it. The
* item will be returned if picking it succeeded (the actor <em>might</em> also
* make a mistake and pick up the wrong item!).
*
* @param item
* An item, should be in the current map location
* @return The item that was picked up (normally <code>item</code>), or
* <code>null</code> if it failed
*/
IItem pickUp(IItem item);
/**
* Drop an item
* <p>
* This should be used by IActors who are carrying an item and want to put it on
* the ground. Check the return value to see if it succeeded.
*
* @param item
* An item, should be carried by the current actor and not already be
* on the map
* @return True if the item was placed on the map, false means you're still
* holding it
*/
boolean drop(IItem item);
/**
* Clear the unused graphics area (you can fill it with whatever you want!)
*/
void clearFreeGraphicsArea();
/**
* Clear the unused text area (you can fill it with whatever you want!)
*/
void clearFreeTextArea();
/**
* Get the bounds of the free graphics area.
* <p>
* You can fill this with whatever you want, using {@link #getPainter()} and
* {@link #clearFreeGraphicsArea()}.
*
* @return Array of coordinates; ([0],[1]) are the top-left corner, and
* ([2],[3]) are the bottom-right corner (exclusive).
*/
double[] getFreeGraphicsAreaBounds();
/**
* Get the bounds of the free texxt area.
* <p>
* You can fill this with whatever you want, using {@link #getPrinter()} and
* {@link #clearFreeTextArea()}.
* <p>
* You'll probably want to use something like:
*
* <pre>
* int[] bounds = getFreeTextArea();
* int x = bounds[0];
* int y = bounds[1];
* game.getPrinter().printAt(x, y++, "Hello");
* game.getPrinter().printAt(x, y++, "Do you have any carrot cake?", Color.ORANGE);
* </pre>
*
* @return Array of column/line numbers; ([0],[1]) is the top-left corner, and
* ([2],[3]) is the bottom-right corner (inclusive).
*/
int[] getFreeTextAreaBounds();
/**
* See {@link #getFreeGraphicsAreaBounds()}, {@link #clearFreeGraphicsArea()}.
*
* @return A Turtle, for painting graphics
*/
ITurtle getPainter();
/**
* See {@link #getFreeTextAreaBounds()}, {@link #clearFreeTextArea()}.
*
* @return A printer, for printing text
*/
Printer getPrinter();
/**
* @return The height of the map
*/
int getHeight();
/**
* @return A list of the non-actor items at the current map location
*/
List<IItem> getLocalItems();
/**
* Get the current actor's location.
@ -27,22 +239,38 @@ public interface IGame {
* @return Location of the current actor
*/
ILocation getLocation();
/**
* Get the current actor
* <p>
* You can check if it's your move by doing game.getActor()==this.
*
* @return The current actor (i.e., the (IPlayer/INonPlayer) player who's turn it currently is)
*/
IActor getActor();
/**
* Get the neighbouring map location in direction <code>dir</code>
* <p>
* Same as <code>getLocation().go(dir)</code>
*
* @param dir A direction
* @return A location, or <code>null</code> if the location would be outside the map
* @param dir
* A direction
* @return A location, or <code>null</code> if the location would be outside the
* map
*/
ILocation getLocation(GridDirection dir);
IItem pickUp(IItem item);
/**
* @return The map
*/
IMapView getMap();
boolean drop(IItem item);
boolean canGo(GridDirection dir);
/**
* @return A list of directions we can move in, for use with
* {@link #move(GridDirection)}
*/
List<GridDirection> getPossibleMoves();
/**
* Get a list of all locations that are visible from the current location.
@ -54,26 +282,39 @@ public interface IGame {
* @return A list of grid cells visible from the {@link #getLocation()}
*/
List<ILocation> getVisible();
/**
* @return Width of the map
*/
int getWidth();
int getHeight();
IItem createItem(String symbol);
/**
* Move the current actor in the given direction.
* <p>
* The new location will be returned.
*
* @param dir
* @return A new location
* @throws IllegalMoveException
* if moving in that direction is illegal
*/
ILocation move(GridDirection dir);
void addItem(IItem item);
/**
* Perform a ranged attack on the target.
* <p>
* Rules for this are up to you!
*
* @param dir
* Direction
* @param target
* A target item, which should in some square in the given direction
* @return Your new location if the attack resulted in you moving (unlikely)
*/
ILocation rangedAttack(GridDirection dir, IItem target);
void addItem(String sym);
void displayMessage(String s);
void formatMessage(String s, Object... args);
void displayStatus(String s);
void formatStatus(String s, Object...args);
void displayDebug(String s);
void formatDebug(String s, Object... args);
/**
* @return A random generator
*/
Random getRandom();
}

View File

@ -0,0 +1,254 @@
package inf101.v18.rogue101.map;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.textmode.Printer;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.IArea;
import inf101.v18.grid.ILocation;
import inf101.v18.grid.IMultiGrid;
import inf101.v18.grid.MultiGrid;
import inf101.v18.rogue101.Main;
import inf101.v18.rogue101.game.IllegalMoveException;
import inf101.v18.rogue101.objects.IActor;
import inf101.v18.rogue101.objects.IItem;
import inf101.v18.rogue101.objects.Wall;
import javafx.scene.canvas.GraphicsContext;
public class GameMap implements IGameMap {
/**
* The grid that makes up our map
*/
private final IMultiGrid<IItem> grid;
/**
* These locations have changed, and need to be redrawn
*/
private final Set<ILocation> dirtyLocs = new HashSet<>();
/**
* An index of all the items in the map and their locations.
*/
// an IdentityHashMap uses object identity as a lookup key, so items are
// considered equal if they are the same object (a == b)
private final Map<IItem, ILocation> items = new IdentityHashMap<>();
public GameMap(IArea area) {
grid = new MultiGrid<>(area);
}
public GameMap(int width, int height) {
grid = new MultiGrid<>(width, height);
}
@Override
public void add(ILocation loc, IItem item) {
// keep track of location of all items
items.put(item, loc);
// also keep track of whether we need to redraw this cell
dirtyLocs.add(loc);
// do the actual adding
List<IItem> list = grid.get(loc);
list.add(item);
// TODO: should be sorted!
}
@Override
public boolean canGo(ILocation to) {
return !grid.contains(to, (i) -> (i instanceof Wall || i instanceof IActor));
}
@Override
public boolean hasNeighbour(ILocation from, GridDirection dir) {
return from.canGo(dir);
}
@Override
public boolean canGo(ILocation from, GridDirection dir) {
if (!from.canGo(dir))
return false;
ILocation loc = from.go(dir);
return canGo(loc);
}
@Override
public void draw(ITurtle painter, Printer printer) {
Iterable<ILocation> cells;
if (Main.MAP_DRAW_ONLY_DIRTY_CELLS) {
if (dirtyLocs.isEmpty())
return;
else
cells = dirtyLocs;
} else {
cells = grid.locations();
painter.as(GraphicsContext.class).clearRect(0, 0, getWidth() * printer.getCharWidth(),
getHeight() * printer.getCharHeight());
printer.clearRegion(1, 1, getWidth(), getHeight());
}
GraphicsContext ctx = painter.as(GraphicsContext.class);
double h = printer.getCharHeight();
double w = printer.getCharWidth();
if (Main.MAP_AUTO_SCALE_ITEM_DRAW) {
ctx.save();
ctx.scale(w / h, 1.0);
w = h;
}
try {
for (ILocation loc : cells) {
List<IItem> list = grid.get(loc);
String sym = " ";
if (!list.isEmpty()) {
if (Main.MAP_DRAW_ONLY_DIRTY_CELLS) {
ctx.clearRect(loc.getX() * w, loc.getY() * h, w, h);
// ctx.fillRect(loc.getX() * w, loc.getY() * h, w, h);
}
painter.save();
painter.jumpTo((loc.getX() + 0.5) * w, (loc.getY() + 0.5) * h);
boolean dontPrint = list.get(0).draw(painter, w, h);
painter.restore();
if (!dontPrint) {
sym = list.get(0).getPrintSymbol();
}
}
printer.printAt(loc.getX() + 1, loc.getY() + 1, sym);
}
} finally {
if (Main.MAP_AUTO_SCALE_ITEM_DRAW) {
ctx.restore();
}
}
dirtyLocs.clear();
}
@Override
public List<IActor> getActors(ILocation loc) {
List<IActor> items = new ArrayList<>();
for (IItem item : grid.get(loc)) {
if (item instanceof IActor)
items.add((IActor) item);
}
return items;
}
@Override
public List<IItem> getAll(ILocation loc) {
return Collections.unmodifiableList(grid.get(loc));
}
@Override
public List<IItem> getAllModifiable(ILocation loc) {
dirtyLocs.add(loc);
return grid.get(loc);
}
@Override
public void clean(ILocation loc) {
// remove any items that have health < 0:
if (grid.get(loc).removeIf((item) -> {
if (item.isDestroyed()) {
items.remove(item);
return true;
} else {
return false;
}
})) {
dirtyLocs.add(loc);
}
}
@Override
public IArea getArea() {
return grid.getArea();
}
@Override
public int getHeight() {
return grid.getHeight();
}
@Override
public List<IItem> getItems(ILocation loc) {
List<IItem> items = new ArrayList<>(grid.get(loc));
items.removeIf((i) -> i instanceof IActor);
return items;
}
@Override
public ILocation getLocation(IItem item) {
return items.get(item);
}
@Override
public ILocation getLocation(int x, int y) {
return grid.getArea().location(x, y);
}
@Override
public ILocation getNeighbour(ILocation from, GridDirection dir) {
if (!hasNeighbour(from, dir))
return null;
else
return from.go(dir);
}
@Override
public int getWidth() {
return grid.getWidth();
}
@Override
public ILocation go(ILocation from, GridDirection dir) throws IllegalMoveException {
if (!from.canGo(dir))
throw new IllegalMoveException("Cannot move outside map!");
ILocation loc = from.go(dir);
if (!canGo(loc))
throw new IllegalMoveException("Occupied!");
return loc;
}
@Override
public boolean has(ILocation loc, IItem target) {
return grid.contains(loc, target);
}
@Override
public boolean hasActors(ILocation loc) {
return grid.contains(loc, (i) -> i instanceof IActor);
}
@Override
public boolean hasItems(ILocation loc) {
// true if grid cell contains an item which is not an IActor
return grid.contains(loc, (i) -> !(i instanceof IActor));
}
@Override
public boolean hasWall(ILocation loc) {
return grid.contains(loc, (i) -> i instanceof Wall);
}
@Override
public void remove(ILocation loc, IItem item) {
grid.remove(loc, item);
items.remove(item);
dirtyLocs.add(loc);
}
@Override
public List<ILocation> getNeighbourhood(ILocation loc, int dist) {
if (dist < 0 || loc == null)
throw new IllegalArgumentException();
else if (dist == 0)
return new ArrayList<>(); // empty!
// TODO: implement this!
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,48 @@
package inf101.v18.rogue101.map;
import java.util.List;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.textmode.Printer;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.objects.IItem;
/**
* Extra map methods that are for the game class only!
*
* @author anya
*
*/
public interface IGameMap extends IMapView {
/**
* Draw the map
*
* @param painter
* @param printer
*/
void draw(ITurtle painter, Printer printer);
/**
* Get a modifiable list of items
*
* @param loc
* @return
*/
List<IItem> getAllModifiable(ILocation loc);
/**
* Remove any destroyed items at the given location (items where {@link IItem#isDestroyed()} is true)
*
* @param loc
*/
void clean(ILocation loc);
/**
* Remove an item
* @param loc
* @param item
*/
void remove(ILocation loc, IItem item);
}

View File

@ -2,8 +2,6 @@ package inf101.v18.rogue101.map;
import java.util.List;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.textmode.Printer;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.IArea;
import inf101.v18.grid.ILocation;
@ -12,37 +10,181 @@ import inf101.v18.rogue101.objects.IActor;
import inf101.v18.rogue101.objects.IItem;
public interface IMapView {
boolean canGo(ILocation from, GridDirection dir);
boolean canGo(ILocation to);
ILocation go(ILocation from, GridDirection dir) throws IllegalMoveException;
boolean hasActors(ILocation loc);
List<IActor> getActors(ILocation loc);
boolean hasItems(ILocation loc);
List<IItem> getItems(ILocation loc);
List<IItem> getAll(ILocation loc);
boolean hasWall(ILocation loc);
void remove(ILocation loc, IItem item);
/**
* Add an item to the map.
*
* @param loc
* A location
* @param item
* the item
*/
void add(ILocation loc, IItem item);
boolean has(ILocation loc, IItem target);
ILocation getLocation(int x, int y);
ILocation getLocation(IItem item);
/**
* Check if it's legal for an IActor to go into the given location
*
* @param to
* A location
* @return True if the location isn't already occupied
*/
boolean canGo(ILocation to);
/**
* Check if it's legal for an IActor to go in the given direction from the given
* location
*
* @param from
* Current location
* @param dir
* Direction we want to move in
* @return True if the next location exists and isn't occupied
*/
boolean canGo(ILocation from, GridDirection dir);
/**
* Get all IActors at the given location
* <p>
* The returned list either can't be modified, or modifying it won't affect the
* map.
*
* @param loc
* @return A list of actors
*/
List<IActor> getActors(ILocation loc);
/**
* Get all items (both IActors and other IItems) at the given location
* <p>
* The returned list either can't be modified, or modifying it won't affect the
* map.
*
* @param loc
* @return A list of items
*/
List<IItem> getAll(ILocation loc);
/**
* Get all non-IActor items at the given location
* <p>
* The returned list either can't be modified, or modifying it won't affect the
* map.
*
* @param loc
* @return A list of items, non of which are instanceof IActor
*/
List<IItem> getItems(ILocation loc);
/**
* @return A 2D-area defining the legal map locations
*/
IArea getArea();
int getWidth();
/**
* @return Height of the map, same as
* {@link #getArea()}.{@link IArea#getHeight()}
*/
int getHeight();
/**
* @return Width of the map, same as {@link #getArea()}.{@link IArea#getWidth()}
*/
int getWidth();
/**
* Find location of an item
*
* @param item
* The item
* @return It's location, or <code>null</code> if it's not on the map
*/
ILocation getLocation(IItem item);
/**
* Translate (x,y)-coordinates to ILocation
*
* @param x
* @param y
* @return an ILocation
* @throws IndexOutOfBoundsException
* if (x,y) is outside {@link #getArea()}
*/
ILocation getLocation(int x, int y);
/**
* Get the neighbouring location in the given direction
*
* @param from
* A location
* @param dir
* the Direction
* @return from's neighbour in direction dir, or null, if this would be outside
* the map
*/
ILocation getNeighbour(ILocation from, GridDirection dir);
/**
* Compute new location of an IActor moving the given direction
*
* @param from
* Original location
* @param dir
* Direction we're moving in
* @return The new location
* @throws IllegalMoveException
* if !{@link #canGo(ILocation, GridDirection)}
*/
ILocation go(ILocation from, GridDirection dir) throws IllegalMoveException;
/**
* Check if an item exists at a location
*
* @param loc
* The location
* @param target
* The item we're interested in
* @return True if target would appear in {@link #getAll(loc)}
*/
boolean has(ILocation loc, IItem target);
/**
* Check for actors.
*
* @param loc
* @return True if {@link #getActors(loc)} would be non-empty
*/
boolean hasActors(ILocation loc);
/**
* Check for non-actors
*
* @param loc
* @return True if {@link #getItem(loc)} would be non-empty
*/
boolean hasItems(ILocation loc);
/**
* Check for walls
*
* @param loc
* @return True if there is a wall at the given location ({@link #getAll(loc)}
* would contain an instance of {@link Wall})
*/
boolean hasWall(ILocation loc);
/**
* Check if a neighbour exists on the map
*
* @param from A location
* @param dir A direction
* @return True if {@link #getNeighbour(from, dir)} would return non-null
*/
boolean hasNeighbour(ILocation from, GridDirection dir);
/**
* Get all locations within i steps from the given centre
* @param centre
* @param dist
* @return A list of locations, all at most i grid cells away from centre
*/
List<ILocation> getNeighbourhood(ILocation centre, int dist);
}

View File

@ -1,7 +1,5 @@
package inf101.v18.rogue101.map;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
@ -36,50 +34,6 @@ import inf101.v18.grid.MyGrid;
* @author anya (Rogue101 update, 2018)
*/
public class MapReader {
/**
* Load map from file.
* <p>
* Files are search for relative to the folder containing the MapReader class.
*
* @return the dungeon map as a grid of characters read from the file, or null
* if it failed
*/
public static IGrid<String> readFile(String path) {
IGrid<String> symbolMap = null;
InputStream stream = MapReader.class.getResourceAsStream(path);
if(stream == null)
return null;
try (Scanner in = new Scanner(stream, "UTF-8")) {
int width = in.nextInt();
int height = in.nextInt();
// System.out.println(width + " " + height);
symbolMap = new MyGrid<String>(width, height, " ");
in.nextLine();
fillMap(symbolMap, in);
}
try {
stream.close();
} catch (IOException e) {
}
return symbolMap;
}
/**
* @return the dungeon map as a grid of characters read from the input string, or null
* if it failed
*/
public static IGrid<String> readString(String input) {
IGrid<String> symbolMap = null;
try (Scanner in = new Scanner(input)) {
int width = in.nextInt();
int height = in.nextInt();
symbolMap = new MyGrid<String>(width, height, " ");
in.nextLine();
fillMap(symbolMap, in);
}
return symbolMap;
}
/**
* This method fills the previously initialized {@link #symbolMap} with the
* characters read from the file.
@ -98,4 +52,48 @@ public class MapReader {
xy[1]++;
}
}
/**
* Load map from file.
* <p>
* Files are search for relative to the folder containing the MapReader class.
*
* @return the dungeon map as a grid of characters read from the file, or null
* if it failed
*/
public static IGrid<String> readFile(String path) {
IGrid<String> symbolMap = null;
InputStream stream = MapReader.class.getResourceAsStream(path);
if (stream == null)
return null;
try (Scanner in = new Scanner(stream, "UTF-8")) {
int width = in.nextInt();
int height = in.nextInt();
// System.out.println(width + " " + height);
symbolMap = new MyGrid<String>(width, height, " ");
in.nextLine();
fillMap(symbolMap, in);
}
try {
stream.close();
} catch (IOException e) {
}
return symbolMap;
}
/**
* @return the dungeon map as a grid of characters read from the input string,
* or null if it failed
*/
public static IGrid<String> readString(String input) {
IGrid<String> symbolMap = null;
try (Scanner in = new Scanner(input)) {
int width = in.nextInt();
int height = in.nextInt();
symbolMap = new MyGrid<String>(width, height, " ");
in.nextLine();
fillMap(symbolMap, in);
}
return symbolMap;
}
}

View File

@ -2,11 +2,20 @@ package inf101.v18.rogue101.objects;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.textmode.BlocksAndBoxes;
import inf101.v18.rogue101.events.IEvent;
import inf101.v18.rogue101.game.IGame;
public class Dust implements IItem {
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public int getCurrentHealth() {
return 0;
}
@Override
public int getDefence() {
return 0;
@ -18,8 +27,8 @@ public class Dust implements IItem {
}
@Override
public int getCurrentHealth() {
return 0;
public String getName() {
return "thick layer of dust";
}
@Override
@ -32,21 +41,9 @@ public class Dust implements IItem {
return BlocksAndBoxes.BLOCK_HALF;
}
@Override
public int handleDamage(IGame game, IItem source, int amount) {
return 0;
}
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public String getName() {
return "thick layer of dust";
}
}

View File

@ -1,7 +1,24 @@
package inf101.v18.rogue101.objects;
/**
* An actor is an IItem that can also do something, either controlled by the
* computer (INonPlayer) or the user (IPlayer).
*
* @author anya
*
*/
public interface IActor extends IItem {
/**
* @return This actor's attack score (used against an item's
* {@link #getDefence()} score to see if an attack is successful)
*/
int getAttack();
/**
* @return The damage this actor deals on a successful attack (used together
* with
* {@link #handleDamage(inf101.v18.rogue101.game.IGame, IItem, int)} on
* the target)
*/
int getDamage();
}

View File

@ -18,131 +18,6 @@ import inf101.v18.rogue101.game.IGame;
* @author anya
*/
public interface IItem extends Comparable<IItem> {
/**
* The defence score determines how hard an object/actor is to hit or grab.
*
* @return Defence score of this object
*/
int getDefence();
/**
* Get maximum health points.
*
* An object's <em>health points</em> determines how much damage it can take
* before it is destroyed / broken / killed.
*
* @return Max health points for this item
*/
int getMaxHealth();
/**
* Get current remaining health points.
* <p>
* An object's <em>health points</em> determines how much damage it can take
* before it is destroyed / broken / killed.
*
* @return Current health points for this item
*/
int getCurrentHealth();
/**
* Get the size of the object.
* <p>
* The size determines how much space an item will use if put into a container.
*
* @return Size of the item
*/
int getSize();
/**
* Get the map symbol of this item.
* <p>
* The symbol can be used on a text-only map, or when loading a map from text.
* <p>
* The symbol should be a single Unicode codepoint (i.e.,
* <code>getSymbol().codePointCount(0, getSymbol().length()) == 1</code>). In
* most cases this means that the symbol should be a single character (i.e.,
* getSymbol().length() == 1); but there are a few Unicode characters (such as
* many emojis and special symbols) that require two Java <code>char</code>s.
*
* @return A single-codepoint string with the item's symbol
*/
String getSymbol();
/**
* Get a (user-friendly) name for the item
* <p>
* Used for things like <code>"You see " + getArticle() + " " + getName()</code>
*
* @return Item's name
*/
String getName();
/**
* @return "a" or "an", depending on the name
*/
default String getArticle() {
return "a";
}
/**
* Get a map symbol used for printing this item on the screen.
* <p>
* This is usually the same as {@link #getSymbol()}, but could also include
* special control characters for changing the text colour, for example.
*
*
* @return A string to be displayed for this item on the screen (should be only
* one column wide when printed)
* @see <a href="https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">ANSI
* escape code (on Wikipedia)</a>
*/
default String getPrintSymbol() {
return getSymbol();
}
/**
* Get item health as a 0.0..1.0 proportion.
*
* <li><code>getHealth() >= 1.0</code> means perfect condition
* <li><code>getHealth() <= 0.0</code> means broken or dead
* <li><code>0.0 < getHealth() < 1.0</code> means partially damaged
*
* @return Health, in the range 0.0 to 1.0
*/
default double getHealthStatus() {
return getMaxHealth() > 0 ? getCurrentHealth() / getMaxHealth() : 0;
}
/**
* Inform the item that it has been damaged
*
* @param game
* The game
* @param source
* The item (usually an IActor) that caused the damage
* @param amount
* How much damage the item should take
* @return Amount of damage actually taken (could be less than
* <code>amount</code> due to armour/protection effects)
*/
int handleDamage(IGame game, IItem source, int amount);
/**
* Inform the item that something has happened.
*
* @param event
* An object describing the event.
* @return
*/
default <T> T handleEvent(IEvent<T> event) {
return event.getData();
}
default boolean isDestroyed() {
return getCurrentHealth() < 0;
}
@Override
default int compareTo(IItem other) {
return Integer.compare(getSize(), other.getSize());
@ -175,4 +50,132 @@ public interface IItem extends Comparable<IItem> {
default boolean draw(ITurtle painter, double w, double h) {
return false;
}
/**
* @return "a" or "an", depending on the name
*/
default String getArticle() {
return "a";
}
/**
* Get current remaining health points.
* <p>
* An object's <em>health points</em> determines how much damage it can take
* before it is destroyed / broken / killed.
*
* @return Current health points for this item
*/
int getCurrentHealth();
/**
* The defence score determines how hard an object/actor is to hit or grab.
*
* @return Defence score of this object
*/
int getDefence();
/**
* Get item health as a 0.0..1.0 proportion.
*
* <li><code>getHealth() >= 1.0</code> means perfect condition
* <li><code>getHealth() <= 0.0</code> means broken or dead
* <li><code>0.0 < getHealth() < 1.0</code> means partially damaged
*
* @return Health, in the range 0.0 to 1.0
*/
default double getHealthStatus() {
return getMaxHealth() > 0 ? getCurrentHealth() / getMaxHealth() : 0;
}
/**
* Get maximum health points.
*
* An object's <em>health points</em> determines how much damage it can take
* before it is destroyed / broken / killed.
*
* @return Max health points for this item
*/
int getMaxHealth();
/**
* Get a (user-friendly) name for the item
* <p>
* Used for things like <code>"You see " + getArticle() + " " + getName()</code>
*
* @return Item's name
*/
String getName();
/**
* Get a map symbol used for printing this item on the screen.
* <p>
* This is usually the same as {@link #getSymbol()}, but could also include
* special control characters for changing the text colour, for example.
*
*
* @return A string to be displayed for this item on the screen (should be only
* one column wide when printed)
* @see <a href="https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">ANSI
* escape code (on Wikipedia)</a>
*/
default String getPrintSymbol() {
return getSymbol();
}
/**
* Get the size of the object.
* <p>
* The size determines how much space an item will use if put into a container.
*
* @return Size of the item
*/
int getSize();
/**
* Get the map symbol of this item.
* <p>
* The symbol can be used on a text-only map, or when loading a map from text.
* <p>
* The symbol should be a single Unicode codepoint (i.e.,
* <code>getSymbol().codePointCount(0, getSymbol().length()) == 1</code>). In
* most cases this means that the symbol should be a single character (i.e.,
* getSymbol().length() == 1); but there are a few Unicode characters (such as
* many emojis and special symbols) that require two Java <code>char</code>s.
*
* @return A single-codepoint string with the item's symbol
*/
String getSymbol();
/**
* Inform the item that it has been damaged
*
* @param game
* The game
* @param source
* The item (usually an IActor) that caused the damage
* @param amount
* How much damage the item should take
* @return Amount of damage actually taken (could be less than
* <code>amount</code> due to armour/protection effects)
*/
int handleDamage(IGame game, IItem source, int amount);
/**
* Inform the item that something has happened.
*
* @param event
* An object describing the event.
* @return
*/
default <T> T handleEvent(IEvent<T> event) {
return event.getData();
}
/**
* @return True if this item has been destroyed, and should be removed from the map
*/
default boolean isDestroyed() {
return getCurrentHealth() < 0;
}
}

View File

@ -2,6 +2,21 @@ package inf101.v18.rogue101.objects;
import inf101.v18.rogue101.game.IGame;
/**
* An actor controlled by the computer
*
* @author anya
*
*/
public interface INonPlayer extends IActor {
/**
* Do one turn for this non-player
* <p>
* This INonPlayer will be the game's current actor ({@link IGame#getActor()})
* for the duration of this method call.
*
* @param game
* Game, for interacting with the world
*/
void doTurn(IGame game);
}

View File

@ -4,5 +4,22 @@ import inf101.v18.rogue101.game.IGame;
import javafx.scene.input.KeyCode;
public interface IPlayer extends IActor {
/**
* Send key presses from the human player to the player object.
* <p>
* The player object should interpret the key presses, and then perform its
* moves or whatever, according to the game's rules and the player's
* instructions.
* <p>
* This IPlayer will be the game's current actor ({@link IGame#getActor()}) and
* be at {@link IGame#getLocation()}, when this method is called.
* <p>
* This method may be called many times in a single turn; the turn ends
* {@link #keyPressed(IGame, KeyCode)} returns and the player has used its
* movement points (e.g., by calling {@link IGame#move(inf101.v18.grid.GridDirection)}).
*
* @param game
* Game, for interacting with the world
*/
void keyPressed(IGame game, KeyCode key);
}

View File

@ -2,13 +2,21 @@ package inf101.v18.rogue101.objects;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.gfx.textmode.BlocksAndBoxes;
import inf101.v18.rogue101.events.GameEvent;
import inf101.v18.rogue101.events.IEvent;
import inf101.v18.rogue101.game.IGame;
public class Wall implements IItem {
private int hp = getMaxHealth();
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public int getCurrentHealth() {
return hp;
}
@Override
public int getDefence() {
return 10;
@ -20,8 +28,8 @@ public class Wall implements IItem {
}
@Override
public int getCurrentHealth() {
return hp;
public String getName() {
return "wall";
}
@Override
@ -33,20 +41,10 @@ public class Wall implements IItem {
public String getSymbol() {
return BlocksAndBoxes.BLOCK_FULL;
}
@Override
public int handleDamage(IGame game, IItem source, int amount) {
hp -= amount;
return amount;
}
@Override
public boolean draw(ITurtle painter, double w, double h) {
return false;
}
@Override
public String getName() {
return "wall";
}
}

View File

@ -1,15 +1,19 @@
package inf101.v18.rogue101.tests;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.Assert.*;
import org.junit.jupiter.api.Test;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.map.GameMap;
class GameMapTest {
@Test
void testSortedAdd() {
// TODO
GameMap gameMap = new GameMap(20, 20);
ILocation location = gameMap.getLocation(10, 10);
// TODO:
fail("Not yet implemented");
}

View File

@ -0,0 +1,42 @@
package inf101.v18.rogue101.tests;
import static org.junit.Assert.*;
import org.junit.jupiter.api.Test;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.game.Game;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.map.GameMap;
import inf101.v18.rogue101.objects.IItem;
import inf101.v18.rogue101.objects.IPlayer;
import javafx.scene.input.KeyCode;
class PlayerTest {
public static String TEST_MAP = "40 5\n" //
+ "########################################\n" //
+ "#...... ..C.R ......R.R......... ..R...#\n" //
+ "#.R@R...... ..........RC..R...... ... .#\n" //
+ "#... ..R........R......R. R........R.RR#\n" //
+ "########################################\n" //
;
@Test
void testPlayer1() {
// new game with our test map
Game game = new Game(TEST_MAP);
// pick (3,2) as the "current" position; this is where the player is on the
// test map, so it'll set up the player and return it
IPlayer player = (IPlayer) game.setCurrent(3, 2);
// find players location
ILocation loc = game.getLocation();
// press "UP" key
player.keyPressed(game, KeyCode.UP);
// see that we moved north
assertEquals(loc.go(GridDirection.NORTH), game.getLocation());
}
}

View File

@ -8,17 +8,6 @@ import java.util.Random;
public class ElementGenerator<T> extends AbstractGenerator<T> {
private List<T> elts;
/**
* New ElementGenerator, will pick a random element from a list.
*
* @requires list must not be empty
*/
public ElementGenerator(List<T> elts) {
if (elts.size() == 0)
throw new IllegalArgumentException();
this.elts = elts;
}
/**
* New ElementGenerator, will pick a random element from a collection.
*
@ -30,6 +19,17 @@ public class ElementGenerator<T> extends AbstractGenerator<T> {
this.elts = new ArrayList<>(elts);
}
/**
* New ElementGenerator, will pick a random element from a list.
*
* @requires list must not be empty
*/
public ElementGenerator(List<T> elts) {
if (elts.size() == 0)
throw new IllegalArgumentException();
this.elts = elts;
}
@Override
public T generate(Random r) {
return elts.get(r.nextInt(elts.size()));

View File

@ -4,7 +4,6 @@ import java.util.Random;
import inf101.v18.grid.IArea;
import inf101.v18.grid.ILocation;
import inf101.v18.grid.IPosition;
public class LocationGenerator extends AbstractGenerator<ILocation> {
private final IArea area;