initial import

This commit is contained in:
Anya Helene Bagge 2018-02-28 10:22:33 +01:00
commit 6c2103c9ca
79 changed files with 8843 additions and 0 deletions

11
.classpath Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<accessrules>
<accessrule kind="accessible" pattern="javafx/**"/>
</accessrules>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
<classpathentry kind="output" path="bin"/>
</classpath>

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/bin/
.DS_Store

17
.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>inf101.v18.sem1</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@ -0,0 +1,12 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.8
org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter

View File

@ -0,0 +1,3 @@
cleanup_settings_version=2
eclipse.preferences.version=1
formatter_settings_version=13

6
AUTHORS.md Normal file
View File

@ -0,0 +1,6 @@
# Authors
* Anya Helene Bagge, UiB (graphics, maintenance)
* Lars Jaffke, UiB (Langton's Ant)
* *you*, UiB (the rest!)

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2015-2018 University of Bergen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

11
README.md Normal file
View File

@ -0,0 +1,11 @@
# [Semesteroppgave 1: “Rogue One-Oh-One”](https://retting.ii.uib.no/inf101.v18.sem1/blob/master/SEM-1.md)
**OBS: koden / oppgaveteksen er ikke helt ferdig ennå du kan lese den og se på ting online men ikke laste den ned**
**Innleveringsfrist: Fredag 9. mars kl 2400** (TODO: info om forlengelse)
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).
# Fyll inn egne svar/beskrivelse/kommentarer til prosjektet under
* Levert av: *NAVN* (*BRUKERNAVN*)

100
SEM-1.md Normal file
View File

@ -0,0 +1,100 @@
# [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)
# Praktisk informasjon
## Sjekkliste:
* [ ] Thing to do
## Om semesteroppgaven
Semesteroppgaven er *obligatorisk*, og er ment å gi innsikt i og erfaring med
teknikkene vi har lært hittil i semesteret, og å teste en del praktiske
ferdigheter som ikke er så lette å teste på eksamen. Se under angående
karakter.
*Les helst all den kjedelige teksten før du begynner. Si ifra om noe er veldig uklart.*
### Læringsmål
Målene for denne semesteroppgaven er:
* Å kunne sette seg inn i et eksisterende program/rammeverk, og utvide det ut ifra spesifikasjoner (beskrivelsen i oppgavene).
* Å bruke grensesnitt (interface) til å kommunisere mellom objekter.
* Å lage programmer hvor objektene selv styrer sin egen oppførsel, basert på *hendelser* i systemet.
* Å beskrive komplekse objekt-orienterte systemer på en forståelig måte.
### Retting og poeng
Semesteroppgaven blir rettet av en gruppeleder, som gir tilbakemeldinger på
innleveringen. For semesteroppgavene blir det satt poeng, og til sammen teller
semesteroppgavene 30 % på karakteren i faget. Ved poengsetting legger vi vekt
på følgende:
* At du har fungerende løsninger på de forskjellige deloppgavene
* At koden din er ryddig og at eventuelle deler som er vanskelig å forstå er forklart i kommentarer
* At du har laget tester for koden din
* Kreativitet, og at du gjør mer enn minimum for å fullføre oppgaven
Du kan regne med en godt gjennomført innlevering som oppfyller minimumskravene gir en
poengsum ca. tilsvarende C. For høyere poengsum må man ha gjort en del av bonusoppgavene. Manglende /
svært mangelfull innlevering gir 0 poeng.
### Samarbeid
Innleveringen er *individuell* og kan ikke løses i grupper. Dere står likevel fri
til å samarbeide om utarbeiding av ideer, diskutere løsninger og å hjelpe
hverandre med å finne og løse problemer (vi oppfordrer faktisk til det!) men programmeringen må du gjøre selv, og du er selv ansvarlig for din egen kode og at du vet og kan forklare hvordan den virker.
Hvis du har diskutert ideer eller løsninger med noen, gi en kort redegjørelse for det i `README.md` og evt. i commit-meldingen hvis det er relatert til en konkret commit. F.eks. *“Kaniner-og-aliens-konseptet er tenkt ut sammen med Helene Harepus, men vi har kodet det hver for oss”; “Implementert telljeløkkjer til regnbuepromp for einhjørningar (takk til Dag H. for tips om dette)”; “Sorter elementene i riktig rekkefølge (fixes #23, takk til bestemor som la merke til feilen)”.*
### Fusk og opphavsrett
Forøvrig gjelder [UiBs regler om fusk og plagiat](http://www.uib.no/studiekvalitet/77864/fusk-hva-er-det-og-hvilke-konsekvenser-f%C3%A5r-det-deg-som-student). Akademisk uredelighet og (forsøk på) fusk reguleres av Universitetsloven, og mulige konsekvenser er blant annet annullering av eksamen og utestenging (evt. tilbaketrekking av vitnemålet om ting blir oppdaget i ettertid).
(*Men:* Så lenge det er klart og tydelig hvem som har skrevet hva, hva kilden er og hvem som evt. har hjulpet til med hva, er det *ikke* fusk eller plagiat men du får selvfølgelig bare poeng for ting du har gjort selv.)
Opphavsrett er et separat spørsmål du kan generelt ikke klippe kode eller bruke bilder/lyd/media fra nettet [uten at du har tillatelse](https://retting.ii.uib.no/inf101/inf101.v18/wikis/opphavsrett-lisenser). Hvis du bruker ting du har funnet på nettet e.l. må du opplyse i `README.md` om hva det er, hvem som har laget det og hvor du har funnet det. For grafikk/lyd som du har rett til å gjenbruke, se gjerne etter ting med [Creative Commons lisens](https://creativecommons.org/licenses/). Vi har en liste med greie kilder på slutten av oppgaven. (Og om du er nysgjerrig, finner du lisensen for koden du har fått utlevert i filen [LICENSE](LICENSE).)
### Innlevering
Du finner koden din i repositoriet med URIen:
https://retting.ii.uib.no/<brukernavn>/inf101.v18.sem1.git
Oppgaven leveres inn ved å pushe til retting.ii.uib.no, [slik du har gjort med alle tidligere INF101-oppgaver](https://retting.ii.uib.no/inf101/inf101.v18/wikis/hente-levere-oppgaver). Husk å få med eventuelle nye filer du har opprettet (hvis testene virker hos deg, men ikke i innleveringssystemet, er det gjerne det som er feil).
**VIKTIG:** *Sjekk kvitteringssiden som kommer opp når du pusher, i tilfelle det skjer feil!* Du må evt. gjøre Pull før Push, slik du så i Lab 3.
Vi anbefaler at du gjør commit hver dag, eller hver gang du er ferdig med en
større endring. Da går det alltid an å lete seg tilbake i historien til en
tidligere versjon hvis du har havnet på villspor.
* Du kan levere inn så mye og ofte du vil. Versjonen som teller er den siste du
pushet før innleveringsfristen.
* *VIKTIG:* Hvis du ikke allerede har prøvd ut GitLab / https://retting.ii.uib.no/ og pushing av
innleveringer, må du gjøre det *med en gang* (gjør labbene!). Du kan ikke regne med å få hjelp til
dette på innleveringsdagen, men på gruppetimene vil du få rikelig med hjelp
til dette.
* Alle testene bør passere (være grønne). Det blir i tillegg lagt betydelig
vekt på kodekvalitet og dokumentasjon. Dvs. koden din skal ikke bare *virke*,
den være lett å forstå og å endre.
* Du kan selv sjekke status i
[innleveringssystemet](http://retting.ii.uib.no:81/) det vil gi rask
tilbakemelding hver gang du pusher til Gitlab, også før innleveringsfristen.
Alt skal være *grønt* der. Hvis du ser feil der som du ikke finner ut av, er det bare å spørre om hjelp.
### Tips
* Selv om det kanskje bare er litt mer å gjøre enn i en vanlig ukeoppgave, er
det *veldig mye* å sette seg inn i. Du bør begynne tidlig og jobbe jevnt. Du må
også regne med å jobbe utenom labtimene.

174
SEM-1_DEL-A.md Normal file
View File

@ -0,0 +1,174 @@
# [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
* [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)
## Kunnskap/konsepter du har bruk for til denne delen
* *Abstraksjon* å se bort fra uvesentlige detaljer og konsentrere seg om de tingene som er viktig (for det vi holder på med / fra vårt synspunkt)
* *Modellering* en modell er en (som regel forenklet) representasjon av noe. Vi bruker modeller for å
* prøve ut / leke med / teste ting når vi ikke kan/vil gjøre direkte med det vi modellerer (f.eks.: en klimamodell lar oss forstå hvordan klimaet kan utvikle seg i forskjellige scenarier uten at vi trenger å teste det i virkeligheten; en prototype lar deg se hvordan at produkt kan bli; en modell lar deg se hvordan klær ser ut uten å måtte prøve dem på deg selv; et dataspill lar deg gjøre ting du ikke kunne gjort (eller hatt lov til) i virkeligheten; lek lar barn bygge “romskip” og utføre medisinsk behandling på “spedbarn” uten å svi av plenen eller skade lillebror).
* ha et eksempel som vi kan bruke når vi skal lage flere ting (f.eks.: [kilogramprototypen](https://en.wikipedia.org/wiki/Kilogram#International_prototype_kilogram), arkitekttegninger, 3D designmodeller (CAD/CAM))
* *Objekter* (`new C()`) med oppførsel (metoder) og innkapslet tilstand (feltvariabler), og relasjoner til og interaksjon med andre objekter.
* *Interface* (`interface I`, `class C implements I`) hvordan du kan bruke objekter som du har (og hvilke metoder objektenes klasse må implementere)
Ting som (kanskje) er nytt:
* `interface I extends J` både `I` og `J` må være grensesnitt
* Alle metodene i `J` er også med i `I` (som om du hadde listet dem opp i `I`)
* `I` blir en *subtype* av `J` hvis noe er en `I` (er av en klasse som implementerer `I`) så kan det også brukes som en `J` (i en variabel av type `J`, f.eks.). (Det motsatte er *ikke* tilfelle ikke alle `J`-er er `I`-er.)
* `default`-metoder i `interface`/grensesnitt disse gir deg kode for en metode så du slipper å implementere den selv i klassen din.
* Default-metodene har ikke tilgang til feltvariabler, og de kan bare bruke metodene i grensesnittet.
* Typiske default-metoder er ekstrametoder som bygger på de andre metodene (metoder som er praktisk for bruk uten å være helt nødvendige). Det er også praktisk når man senere skal legge til flere metoder i et grensesnitt, for å slippe å måtte oppdatere en hel haug med klasser.
## Rogue 101
Semesteroppgaven er en naturlig videreutvikling av Labyrint-oppgaven i stedet for å bare gå rundt i en kjedelig labyrint, skal vi nå kunne gå rundt, plukke opp ting og interagere med ting.
### Bakgrunn
Et eksempel på et slikt labyrint-spill er [Rogue](https://en.wikipedia.org/wiki/Rogue_(video_game)), som er et gammel [dungeon crawl](https://en.wikipedia.org/wiki/Dungeon_crawl)-spill med “tekst-grafikk”, der du går rundt i en labyrint, plukker opp ting og dreper monstre. De første slike spillene dukket opp på slutten av 1970-tallet; *Rogue* (1980) var et av de første, og har gitt navn til hele sjangeren ([“Roguelike”](https://en.wikipedia.org/wiki/Roguelike)). Dungeon crawls og roguelike spill har vært veldig populære opp gjennom tidene (ikke minst på grunn av fancy 2D-grafikk med tekst-symboler, som var et betydelig steg opp fra [tidligere adventure/fantasy spill](https://en.wikipedia.org/wiki/Colossal_Cave_Adventure) som hvor alt var bare tekst), inkludert gamle klassikere som [NetHack](TODO), [Moria](TODO) og [Larn](TODO). Ofte har disse spillene vært laget som hobbyprosjekter av programmører (informatikk-studenter, for eksempel) og vært mer kompliserte (og mye vanskeligere) enn spill fra kommersielle spillprodusenter, selv om de gjerne ikke har hatt spesielt fancy grafikk.
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
* 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...)
![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
* a) først tenke oss hvilke elementer som inngår i spill-verdenen;
* b) finne ut hvordan vi representerer og implementerer disse på datamaskinen.
Dette er likende til prosessen vi snakket om på de første forelesningene:
1. Vi har et problemdomene, *dungeon crawling*
2. Vi analyserer domenet, og lager en abstrakt beskrivelse av det, f.eks. spillreglene skrevet på papir.
3. Vi designer programmet, og prøver å lage en konkret implementasjon.
4. Vi tester programmet vårt mot forventingene, og justerer på ting og gjentar prosessen.
Mesteparten av denne jobben er allerede gjort for deg. Din jobb er nå å sette deg inn i koden og vår dungeon crawling modell. Vi har gjort analyse og design og en god del implementasjon, så vi er ca. på slutten av punkt 3. Du må bli kjent med koden og komfortabel med å gjøre endringer, og så gå videre med implementasjonen. I siste del av oppgaven er det du som er “sjefen”, og kan gjøre (nesten) som du vil med design og regler.
### Analyse hva er de essensielle bitene av Rogue-101?
TODO: make links
Basert på tidligere erfaringer med slike spill, samt grunding tenking og lesing av de relevante Wikipedia-sidene, ser vi for oss følgende:
* Alt foregår på et ruteformet kart (2D grid)
* Hver celle/rute på kartet kan inneholde enten:
* en vegg
* 0 eller flere ting
* En “ting” i denne sammenhengen er:
* et objekt som spilleren/noen kan plukke opp og bruke til noe f.eks. en gulrot, et papirark, et sverd, en brunostskive, osv.
* *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
* 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.)
* merk deg at alle ting har helse og forsvar, selv ting som er ikke er levende dette kan vi f.eks. bruke til å gjøre det mulig å ødelegge ting eller gjøre enkelte ting vanskelige å plukke opp
### Design hvordan lager vi dette i Java?
Basert på dette tenker vi oss følgende typer objekter:
* IGameMap spillkartet
* IItem 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 en IItem som ikke kan dele plass med noe annet
* IActor en IItem som bevege seg og ikke kan dele plass med en annen IActor
* IPlayer en IActor som styres ved at brukeren trykker på tastene
* INonPlayer 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 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.
* GridDirection en retning (NORTH, SOUTH, ...), til bruk sammen med ILocation
* IArea et rektangulært sett med ILocations. Brukes f.eks. av spillkartet for å lettvint gå gjennom alle cellene/rutene i kartet.
* (IGrid<T> og IMultiGrid<T> 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~~ (TODO, 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.
Les gjennom grensesnittene vi har nevnt over ([IGameMap](TODO), [IItem](TODO), [IActor](TODO), [INonPlayer](TODO), [IPlayer](TODO) 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):
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.
* **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)*
* **2. Hva ser ut til å være sammenhengen mellom grensesnittene?** Flere av dem er f.eks. laget slik at de utvider (extends) andre grensesnitt.
* **3. Det er to grensesnitt for kart, både [IGameMap](TODO) og [IMapView](TODO). Hvorfor har vi gjort det slik?**
* **4. Hvorfor tror du [INonPlayer](TODO) og [IPlayer](TODO) er forskjellige? Ville du gjort det annerledes?**
### Deloppgave A2: Eksempler på IItem og IActor
Til denne deloppgaven kan du se først på [Carrot](TODO) og [Rabbit](TODO). 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?**
Se på [Game](TODO) og [GameMap](TODO) 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å?**
* **7. Hvordan vet `Game` hvor `Rabbit` er når den spør / hvordan vet `Game` *hvilken* `Rabbit` som kaller `getLocation()`?**
### 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
* F11 eller Ctrl/Cmd-F bytt til/fra fullskjerm
* Ctrl-R / Cmd-R bytt mellom forskjellige varianter av 40- og 80-tegn tekstskjerm
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:
* **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.
* **c)** En annen mulighet er å alltid gå i første tilgjengelige retning; dvs. samme som den opprinnelige implementasjonen, men plukk første retning i stedet for en tilfeldig en. Prøv dette. Ser det ut som de blir bedre eller dårligere til å finne gulrøtter?
* **d)** En enda bedre strategi er å sjekke alle nabofeltene, se om det ligger en gulrot der, og såfall gå dit (hvis den ikke ser en gulrot kan den f.eks. gå tilfeldig). Implementer dette. Tips:
* når du har funnet ut hvilke retninger (GridDirection) du kan gå i, kan du finne nabofeltene med `ILocation loc = game.getLocation(dir)` (evt `game.getLocation().go(dir)`).
* 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
#### Bedre gulrøtter
Prøv også å justere gulrøttene litt ([Carrot](TODO)):
* **a)** Gulrøttene får helsen satt til -1 når de blir spist etter helsereglene våre vil de derfor bli fjernet fra kartet. Prøv å sette helsen til 0 i stedet. Hvorfor går det ikke bedre med kaninene selv om gulrøttene nå blir værende på kartet?
* **b)** Det hadde kanskje vært praktisk (ihvertfall for kaninene) om gulrøttene vokste seg store og fine igjen etter en stund; for eksempel ved at de “helbreder” ett helsepoeng for hver runde som går men `Carrot` har ingen `doTurn()` metode slik `Rabbit` har, så den får ikke med seg at rundene går eller at kaninene hopper rundt (rent bortsett fra at det går an å “jukse” ved å regne med / håpe på at `draw()` blir kalt en gang per runde).
* Lag en `doTurn()`-metode i `Carrot` som øker `hp` med 1 for hver runde (opp til `getMaxHealth()`).
* Hva skjer, ser det ut til å virke?
* Hvis det ikke virker, hva må du eventuelt gjøre for å få `Carrot` til å gjøre noe hver runde?
* **c)** Du kan også prøve å la `Game` legge til nye, tilfeldig plasserte gulrøtter av og til:
* `random.nextInt(100) < 20` er en grei test hvis du f.eks. vil gjøre noe med 20% sannsynlighet
* 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
**Tegn en liten oversikt over det du tenker er de viktigste grensesnittene/klassene i programmet.**
* Hvis noe `implements`/`extends` noe, tegn en pil til det det implementerer/utvider.
* Du kan skrive ned f.eks. de viktigste metodenavnene eller noen stikkord om rollen til den aktuelle typen.
* Tegn også en liten oversikt over objekter i forskjellige situasjoner (f.eks. Rabbit, Carrot, Game, GameMap og hvordan de (evt) kommuniserer når kaninen holder på å gjøre noe).
(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)
* 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` (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-1_DEL-B](SEM-1DEL-B.md)

90
SEM-1_DEL-B.md Normal file
View File

@ -0,0 +1,90 @@
# [Semesteroppgave 1: “Rogue One oh one”](https://retting.ii.uib.no/inf101.v18.sem1/blob/master/SEM-1_DEL-B.md) Del B: Implementasjonsarbeid
**OBS: koden / oppgaveteksen er ikke helt ferdig ennå du kan lese den og se på ting online men ikke laste den ned**
* [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)
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.
* **b)** Lag deg en ny klasse som implementerer IItem. 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.
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
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`. 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`).
* **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.)
* **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.
* Hvis det ikke er lov å flytte (det er en vegg der eller en kanin), kan du f.eks. gi en tilbakemelding til brukeren med `game.displayMessage("Ouch!")`. Hvis du er ekstra streng, kan du også la spilleren tape litt helse på å stange hodet i veggen.
* **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
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).
En grei løsning er å si at vi vil se den *største* tingen alle IItems har en `getSize()` metode som forteller hvor “stor” den er (uten at vi har brukt dette til noe hittil). IItems er også `Comparable` det følger med en `default` `compareTo()`-metode i `IItem` som sammenlikner størrelse (`getSize()`).
For å fikse dette vil vi at kartet skal ha tingene liggende i sortert rekkefølge i listene for hver celle. Vi kunne sortert listen hver gang vi hentet den (f.eks. hver gang kaninen kaller `game.getLocalItems()`, slik at den største gulroten kommer først), men det er både raskere og ca. like lettvint å passe på at vi legger ting inn i sortert rekkefølge: Hvis vi skal legge et element *e* inn i listen *l*, så vil vi ha det på den første posisjonen *i* slik at *e.compareTo(l.get(i)) >= 0* (altså, sett inn *e* foran det første elementet *e* er større enn).
* **a)** Legg til alternativet for `"."` i `Game.createItem()` den skal produsere et `Dust`-objekt.
* Hvis du kjører programmet nå, vil du antakelig se at mange av kaninenen “gjemmer seg” de ligger altså under støvet (Dust-objektene; sees som litt mørkere felter).
* **b)** Endre `add`-metoden i `GameMap` slik at nye items blir lagt til i sortert rekkefølge (største først).
* Husk at `a.compareTo(b)` er `< 0` hvis `a` er mindre `b`, `== 0` hvis `a` er lik `b` og `> 0` hvis `a` er større `b`.
* Du kan legge element foran det nåværende elementet på posisjon `i` med `list.add(i, e)` (det er også OK om `i == list.size()`, da legger du til et nytt element på slutten av listen)
* Det holder å kalle `add` på listen, siden `list` refererer til samme objektet som ligger inne i kart-cellen.
* `Dust`-objektene er ganske små (størrelse 1), så hvis du har gjort det riktig, vil kaninene (og spilleren) bli tegnet i stedet for støvet.
* **c)** Test `add`-metoden.
* Finn testklassen [GameMapTest](TODO) og testmetoden vi har begynt på (`testSortedAdd`).
* Du må sette opp et test-scenario:
* du trenger et nytt GameMap (det trenger ikke være fylt med noe)
* du trenger også en ILocation, siden de fleste kart-metodene bruker det (kall `getLocation(x,y)` med en kjent, lovlig posisjon)
* 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
Vi vil gjerne la brukeren kunne plukke opp og legge fra seg ting på kartet halvspiste gulrøtter, for eksempel.
* **a)** For å få til dette må du legge til flere tastetrykk til `keyPressed`-metoden i `Player` du kan f.eks. bruke `KeyCode.P` og `KeyCode.D`. Lag gjerne egne metoder for å plukke opp/droppe ting i spilleren.
* *Legg merke til:* andre metoder i `Player` er laget slik at de tar et `IGame`-argument. Det trenger du nok her også fordi du må snakke med spill-objektet for å interagere med kartet og andre ting. Du *kan* lagre `game` som en feltvariabel, så du slipper å sende den rundt som argument. Vi har latt være å gjøre det fordi: a) det er lettere å opprette og teste Player (og andre IItems/IActors) hvis man ikke trenger å lage gi dem et game-objekt (f.eks. kunne du teste GameMap/sortert add uten å bruke `Game`/`IGame` i det hele tatt); og b) det kan være litt forvirrende med sykliske avhengigheter mellom objekter (ikke minst når forholdet endrer seg f.eks. når ting fjernes fra spillet).
* **b)** `Game` har tilsvarende `pickUp()` og `drop()` metoder du kan bruke for å plukke et objekt fra kartet og for å etterlate et objekt på kartet. Metoden `pickUp()` returnerer `null` om du av en eller annen grunn ikke kunne plukke opp tingen. Implementer “plukke opp”-funksjonaliteten. Hint:
* `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. Implementer “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)
###
###

20
SEM-1_DEL-C.md Normal file
View File

@ -0,0 +1,20 @@
# [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
* [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)
# Åpne kilder til grafikk / lyd / media
* 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.
* [Kevin MacLeod](https://incompetech.com/music/) er komponist og har masse musikk tilgjengelig som egner seg for spill og småfilmer; du trenger bare å kreditere ham (han bruker vanlig [CC-BY-3.0](http://creativecommons.org/licenses/by/3.0/) lisens).
* Tidligere INF101-student [Øyvind Aandalen](https://soundcloud.com/user-616269685) har litt musikk han har laget på [SoundCloud](https://soundcloud.com/user-616269685) som han sier dere kan som dere vil.

View File

@ -0,0 +1,27 @@
package inf101.v18.gfx;
public interface IPaintLayer {
/**
* Clear the layer.
*
* <p>
* Everything on the layer is removed, leaving only transparency.
*/
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.
*
* <p>
* There will still be background behind this layer. You may clear it or draw to
* it using {@link Screen#clearBackground()},
* {@link Screen#setBackground(javafx.scene.paint.Color)} and
* {@link Screen#getBackgroundContext()}.
*/
void layerToBack();
}

View File

@ -0,0 +1,667 @@
package inf101.v18.gfx;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import inf101.v18.gfx.gfxmode.TurtlePainter;
import inf101.v18.gfx.textmode.Printer;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Bounds;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SubScene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;
public class Screen {
private static final double STD_CANVAS_WIDTH = 1280;
private static final List<Double> STD_ASPECTS = Arrays.asList(16.0 / 9.0, 16.0 / 10.0, 4.0 / 3.0);
/** 16:9 */
public static final int ASPECT_WIDE = 0;
/** 16:10 */
public static final int ASPECT_MEDIUM = 1;
/** 4:3 */
public static final int ASPECT_CLASSIC = 2;
public static final int ASPECT_NATIVE = 2;
private static final int CONFIG_ASPECT_SHIFT = 0;
/** Screen's initial aspect ratio should be 16:9 */
public static final int CONFIG_ASPECT_WIDE = 0 << CONFIG_ASPECT_SHIFT;
/** Screen's initial aspect ratio should be 16:10 */
public static final int CONFIG_ASPECT_MEDIUM = 1 << CONFIG_ASPECT_SHIFT;
/** Screen's initial aspect ratio should be 4:3 */
public static final int CONFIG_ASPECT_CLASSIC = 2 << CONFIG_ASPECT_SHIFT;
/** Screen's initial aspect ratio should be the same as the device display. */
public static final int CONFIG_ASPECT_DEVICE = 3 << CONFIG_ASPECT_SHIFT;
private static final int CONFIG_ASPECT_MASK = 3 << CONFIG_ASPECT_SHIFT;
private static final int CONFIG_SCREEN_SHIFT = 2;
/** Screen should start in a window. */
public static final int CONFIG_SCREEN_WINDOWED = 0 << CONFIG_SCREEN_SHIFT;
/** Screen should start in a borderless window. */
public static final int CONFIG_SCREEN_BORDERLESS = 1 << CONFIG_SCREEN_SHIFT;
/** Screen should start in a transparent window. */
public static final int CONFIG_SCREEN_TRANSPARENT = 2 << CONFIG_SCREEN_SHIFT;
/** Screen should start fullscreen. */
public static final int CONFIG_SCREEN_FULLSCREEN = 3 << CONFIG_SCREEN_SHIFT;
/**
* Screen should start fullscreen, without showing a "Press ESC to exit
* fullscreen" hint.
*/
public static final int CONFIG_SCREEN_FULLSCREEN_NO_HINT = 4 << CONFIG_SCREEN_SHIFT;
private static final int CONFIG_SCREEN_MASK = 7 << CONFIG_SCREEN_SHIFT;
private static final int CONFIG_PIXELS_SHIFT = 5;
/**
* Canvas size / number of pixels should be determined the default way.
*
* The default is {@link #CONFIG_PIXELS_DEVICE} for
* {@link #CONFIG_SCREEN_FULLSCREEN} and {@link #CONFIG_COORDS_DEVICE}, and
* {@link #CONFIG_PIXELS_STEP_SCALED} otherwise.
*/
public static final int CONFIG_PIXELS_DEFAULT = 0 << CONFIG_PIXELS_SHIFT;
/**
* Canvas size / number of pixels will be an integer multiple or fraction of the
* logical canvas size that fits the native display size.
*
* Scaling by whole integers makes it less likely that we get artifacts from
* rounding errors or JavaFX's antialiasing (e.g., fuzzy lines).
*/
public static final int CONFIG_PIXELS_STEP_SCALED = 1 << CONFIG_PIXELS_SHIFT;
/** Canvas size / number of pixels will the same as the native display size. */
public static final int CONFIG_PIXELS_DEVICE = 2 << CONFIG_PIXELS_SHIFT;
/**
* Canvas size / number of pixels will the same as the logical canvas size
* (typically 1280x960).
*/
public static final int CONFIG_PIXELS_LOGICAL = 3 << CONFIG_PIXELS_SHIFT;
/**
* Canvas size / number of pixels will be scaled to fit the native display size.
*/
public static final int CONFIG_PIXELS_SCALED = 4 << CONFIG_PIXELS_SHIFT;
private static final int CONFIG_PIXELS_MASK = 7 << CONFIG_PIXELS_SHIFT;
private static final int CONFIG_COORDS_SHIFT = 8;
/**
* The logical canvas coordinate system will be in logical units (i.e., 1280
* pixels wide regardless of how many pixels wide the screen actually is)
*/
public static final int CONFIG_COORDS_LOGICAL = 0 << CONFIG_COORDS_SHIFT;
/** The logical canvas coordinate system will match the display. */
public static final int CONFIG_COORDS_DEVICE = 1 << CONFIG_COORDS_SHIFT;
private static final int CONFIG_COORDS_MASK = 1 << CONFIG_COORDS_SHIFT;
private static final int CONFIG_FLAG_SHIFT = 9;
public static final int CONFIG_FLAG_HIDE_MOUSE = 1 << CONFIG_FLAG_SHIFT;
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.
*
* <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();
}
/**
* 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();
}
/**
* Get the height 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 Height of the display
* @see javafx.stage.Screen#getVisualBounds()
*/
public static double getDisplayHeight() {
return javafx.stage.Screen.getPrimary().getVisualBounds().getHeight();
}
/**
* Get the resolution of this screen, in DPI (pixels per inch).
*
* @return The primary display's DPI
* @see javafx.stage.Screen#getDpi()
*/
public static double getDisplayDpi() {
return javafx.stage.Screen.getPrimary().getDpi();
}
/**
* Start the paint display system.
*
* This will open a window on the screen, and set up background, text and paint
* layers, and listener to handle keyboard input.
*
* @param stage
* A JavaFX {@link javafx.stage.Stage}, typically obtained from the
* {@link javafx.application.Application#start(Stage)} method
* @return A screen for drawing on
*/
public static Screen startPaintScene(Stage stage) {
return startPaintScene(stage, CONFIG_SCREEN_FULLSCREEN_NO_HINT);
}
/**
* Start the paint display system.
*
* This will open a window on the screen, and set up background, text and paint
* layers, and listener to handle keyboard input.
*
* @param stage
* A JavaFX {@link javafx.stage.Stage}, typically obtained from the
* {@link javafx.application.Application#start(Stage)} method
* @return A screen for drawing on
*/
public static Screen startPaintScene(Stage stage, int configuration) {
int configAspect = (configuration & CONFIG_ASPECT_MASK);
int configScreen = (configuration & CONFIG_SCREEN_MASK);
int configPixels = (configuration & CONFIG_PIXELS_MASK);
int configCoords = (configuration & CONFIG_COORDS_MASK);
int configFlags = (configuration & CONFIG_FLAG_MASK);
boolean debug = (configFlags & CONFIG_FLAG_DEBUG) != 0;
if (configPixels == CONFIG_PIXELS_DEFAULT) {
if (configCoords == CONFIG_COORDS_DEVICE || configScreen == CONFIG_SCREEN_FULLSCREEN)
configPixels = CONFIG_PIXELS_DEVICE;
else
configPixels = CONFIG_PIXELS_STEP_SCALED;
}
double rawWidth = getRawDisplayWidth();
double rawHeight = getRawDisplayHeight();
double width = getDisplayWidth() - 40;
double height = getDisplayHeight() - 100;
double canvasAspect = configAspect == CONFIG_ASPECT_DEVICE ? rawWidth / rawHeight
: STD_ASPECTS.get(configAspect);
double xScale = (height * canvasAspect) / Screen.STD_CANVAS_WIDTH;
double yScale = (width / canvasAspect) / (Screen.STD_CANVAS_WIDTH / canvasAspect);
double scale = Math.min(xScale, yScale);
if (configPixels == CONFIG_PIXELS_STEP_SCALED) {
if (scale > 1.0)
scale = Math.max(1, Math.floor(scale));
else if (scale < 1.0)
scale = 1 / Math.max(1, Math.floor(1 / scale));
}
double winWidth = Math.floor(Screen.STD_CANVAS_WIDTH * scale);
double winHeight = Math.floor((Screen.STD_CANVAS_WIDTH / canvasAspect) * scale);
double canvasWidth = Screen.STD_CANVAS_WIDTH;
double canvasHeight = Math.floor(3 * Screen.STD_CANVAS_WIDTH / 4);
double pixWidth = canvasWidth;
double pixHeight = canvasHeight;
if (configPixels == CONFIG_PIXELS_SCALED || configPixels == CONFIG_PIXELS_STEP_SCALED) {
pixWidth *= scale;
pixHeight *= scale;
} else if (configPixels == CONFIG_PIXELS_DEVICE) {
pixWidth = rawWidth;
pixHeight = rawHeight;
}
if (configCoords == CONFIG_COORDS_DEVICE) {
canvasWidth = pixWidth;
canvasHeight = pixHeight;
}
if (debug) {
System.out.printf("Screen setup:%n");
System.out.printf(" Display: %.0fx%.0f (raw %.0fx%.0f)%n", width, height, rawWidth, rawHeight);
System.out.printf(" Window: %.0fx%.0f%n", winWidth, winHeight);
System.out.printf(" Canvas: physical %.0fx%.0f, logical %.0fx%.0f%n", pixWidth, pixHeight, canvasWidth,
canvasHeight);
System.out.printf(" Aspect: %.5f Scale: %.5f%n", canvasAspect, scale);
}
Group root = new Group();
Scene scene = new Scene(root, winWidth, winHeight, Color.BLACK);
stage.setScene(scene);
if ((configFlags & CONFIG_FLAG_HIDE_MOUSE) != 0) {
scene.setCursor(Cursor.NONE);
}
Screen pScene = new Screen(scene.getWidth(), scene.getHeight(), //
pixWidth, pixHeight, //
canvasWidth, canvasHeight);
pScene.subScene.widthProperty().bind(scene.widthProperty());
pScene.subScene.heightProperty().bind(scene.heightProperty());
pScene.debug = debug;
pScene.hideFullScreenMouseCursor = (configFlags & CONFIG_FLAG_NO_AUTOHIDE_MOUSE) == 0;
root.getChildren().add(pScene.subScene);
boolean[] suppressKeyTyped = { false };
switch (configScreen) {
case CONFIG_SCREEN_WINDOWED:
break;
case CONFIG_SCREEN_BORDERLESS:
stage.initStyle(StageStyle.UNDECORATED);
break;
case CONFIG_SCREEN_TRANSPARENT:
stage.initStyle(StageStyle.TRANSPARENT);
break;
case CONFIG_SCREEN_FULLSCREEN_NO_HINT:
stage.setFullScreenExitHint("");
// fall-through
case CONFIG_SCREEN_FULLSCREEN:
stage.setFullScreen(true);
break;
}
scene.setOnKeyPressed((KeyEvent event) -> {
if (!event.isConsumed() && pScene.keyOverride != null && pScene.keyOverride.test(event)) {
event.consume();
}
if (!event.isConsumed() && pScene.minimalKeyHandler(event)) {
event.consume();
}
if (!event.isConsumed() && pScene.keyPressedHandler != null && pScene.keyPressedHandler.test(event)) {
event.consume();
}
if (pScene.logKeyEvents)
System.err.println(event);
suppressKeyTyped[0] = event.isConsumed();
});
scene.setOnKeyTyped((KeyEvent event) -> {
if (suppressKeyTyped[0]) {
suppressKeyTyped[0] = false;
event.consume();
}
if (!event.isConsumed() && pScene.keyTypedHandler != null && pScene.keyTypedHandler.test(event)) {
event.consume();
}
if (pScene.logKeyEvents)
System.err.println(event);
});
scene.setOnKeyReleased((KeyEvent event) -> {
suppressKeyTyped[0] = false;
if (!event.isConsumed() && pScene.keyReleasedHandler != null && pScene.keyReleasedHandler.test(event)) {
event.consume();
}
if (pScene.logKeyEvents)
System.err.println(event);
});
return pScene;
}
public double getRawWidth() {
return rawCanvasWidth;
}
public double getRawHeight() {
return Math.floor(rawCanvasWidth / aspects.get(aspect));
}
public double getWidth() {
return Math.floor(getRawWidth() / resolutionScale);
}
public double getHeight() {
return Math.floor(getRawHeight() / resolutionScale);
}
public void moveToFront(IPaintLayer layer) {
Canvas canvas = layerCanvases.get(layer);
if (canvas != null) {
canvas.toFront();
}
}
public void moveToBack(IPaintLayer layer) {
Canvas canvas = layerCanvases.get(layer);
if (canvas != null) {
canvas.toBack();
background.toBack();
}
}
public void hideMouseCursor() {
subScene.getScene().setCursor(Cursor.NONE);
}
public void showMouseCursor() {
subScene.getScene().setCursor(Cursor.DEFAULT);
}
public void setMouseCursor(Cursor cursor) {
subScene.getScene().setCursor(cursor);
}
public void setHideFullScreenMouseCursor(boolean hideIt) {
if (hideIt != hideFullScreenMouseCursor && isFullScreen()) {
if (hideIt) {
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);
}
}
hideFullScreenMouseCursor = hideIt;
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,39 @@
# ZX Spectrum 7 Font
By Sizenko Alexander, [Style-7](http://www.styleseven.com), December 2012. From http://www.styleseven.com/php/get_product.php?product=ZX-Spectrum-7
Minor metrics adjustments by Anya Helene Bagge, October 2017.
* ZXSpectrum-7.otf monospaced regular
# True Type Font: ZX Spectrum-7 version 1.0
## EULA
The font ZX Spectrum-7 is freeware.
## DESCRIPTION
This font has been created to mark the 30th anniversary of the great computer ZX-Spectrum. Font based on native ZX-Spectrum symbols. Latin and Cyrillic code pages are supported.
Files in zx_spectrum-7.zip:
* readme.txt this file;
* zx_spectrum-7.ttf regular style;
* zx_spectrum-7_bold.ttf bold style;
* zx_spectrum-7_screen.png preview image.
Please visit http://www.styleseven.com/ for download our other products as freeware as shareware.
We will welcome any useful suggestions and comments; please send them to ms-7@styleseven.com
## WHAT'S NEW?
* Version 1.01 (December 27 2012):
* Fixed bug for Cyrillic code page.
## AUTHOR
Sizenko Alexander
Style-7
http://www.styleseven.com
Created: December 24 2012

View File

@ -0,0 +1,171 @@
This package was debianized by Florent Rougon <f.rougon@free.fr> on
Tue, 27 Jan 2004 20:12:21 +0100.
The following TeX Live packages were downloaded from
http://www.ctan.org/tex-archive/systems/texlive/tlnet/archive/
and merged into one orig.tar.gz file:
lm.tar.xz
lm.doc.tar.xz
lm-math.tar.xz
lm-math.doc.tar.xz
Upstream work
-------------
The upstream README-Latin-Modern.txt file says:
Font: The Latin Modern Family of Fonts
Designer (Computer Modern Family of Fonts): Donald E. Knuth
Author: Bogus\l{}aw Jackowski and Janusz M. Nowacki
Version: 2.003
Date: 16 IX 2009
Downloads: http://www.gust.org.pl/projects/e-foundry/latin-modern/
License:
% Copyright 2003--2009 by B. Jackowski and J.M. Nowacki
% (on behalf of TeX Users Groups).
% This work is released under the GUST Font License
% -- see GUST-FONT-LICENSE.txt.
% This work has the LPPL maintenance status "maintained".
% The Current Maintainer of this work is Bogus\l{}aw Jackowski
% and Janusz M. Nowacki.
% This work consists of the files listed in the MANIFEST.txt file.
[...]
The current LM package contains the most recent version
of the Latin Modern family of fonts in the PostScript Type 1 and
OpenType format. The fonts are based on Donald E. Knuth's Computer Modern
fonts in the PostScript Type 1 format, released into public domain by the
American Mathematical Society (for the history of the outline version of
the CM fonts see, e.g., http://www.math.utah.edu/~beebe/fonts/bluesky.html ).
The project is supported by TeX users groups: CSTUG, DANTE eV, GUST,
GUTenberg, NTG, and TUG.
The current README-Latin-Modern-Math.txt says
License:
% Copyright 2012--2014 for the Latin Modern math extensions by B. Jackowski,
% P. Strzelczyk and P. Pianowski (on behalf of TeX Users Groups).
%
% This work can be freely used and distributed under
% the GUST Font License (GFL -- see GUST-FONT-LICENSE.txt)
% which is actually an instance of the LaTeX Project Public License
% (LPPL -- see http://www.latex-project.org/lppl.txt).
%
% This work has the maintenance status "maintained". The Current Maintainer
% of this work is Bogus\l{}aw Jackowski, Piotr Strzelczyk and Piotr Pianowski.
%
% This work consists of the files listed
% in the MANIFEST-Latin-Modern-Math.txt file.
See the appendix B for the GUST Font License.
Please read the appendix A below if you want to examine the licensing terms
for the Computer Modern fonts in Type 1 format on which the Latin Modern fonts
are based.
Debian packaging
----------------
Copyright (c) 2004-2007 Florent Rougon
Copyright (c) 2005-2015 Norbert Preining
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 dated June, 1991.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; see the file COPYING. If not, write to the
Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
Boston, MA 02110-1301 USA.
On Debian systems, the complete text of the GNU General Public License version
2 can be found in `/usr/share/common-licenses/GPL-2'.
Appendix A -- Licensing terms for the Computer Modern fonts in Type 1 format
on which the Latin Modern fonts are based.
-----------------------------------------------------------------------------
Running t1disasm (from the t1utils package) on
/usr/share/texmf/fonts/type1/bluesky/cm/*.pfb and
/usr/share/texmf/fonts/type1/bluesky/cmextra/*.pfb yields:
Copyright (C) 1997 American Mathematical Society. All Rights Reserved
[ Florent Rougon's note: well, for
/usr/share/texmf/fonts/type1/bluesky/cm/cmb{,sy}10.pfb, you will only get
one space between "Society." and "All" (with tetex-extra 2.0.2-5.1). ;-) ]
The precise distribution conditions for these fonts can be found in
/usr/share/doc/texmf/fonts/bluesky/README from the tetex-doc package. I will
duplicate here the relevant excerpt for your convenience:
The PostScript Type 1 implementation of the Computer Modern fonts produced
by and previously distributed by Blue Sky Research and Y&Y, Inc. are now
freely available for general use. This has been accomplished through the
cooperation of a consortium of scientific publishers with Blue Sky Research
and Y&Y. Members of this consortium include:
Elsevier Science
IBM Corporation
Society for Industrial and Applied Mathematics (SIAM)
Springer-Verlag
American Mathematical Society (AMS)
In order to assure the authenticity of these fonts, copyright will be held
by the American Mathematical Society. This is not meant to restrict in any
way the legitimate use of the fonts, such as (but not limited to) electronic
distribution of documents containing these fonts, inclusion of these fonts
into other public domain or commercial font collections or computer
applications, use of the outline data to create derivative fonts and/or
faces, etc. However, the AMS does require that the AMS copyright notice be
removed from any derivative versions of the fonts which have been altered in
any way. In addition, to ensure the fidelity of TeX documents using Computer
Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces,
has requested that any alterations which yield different font metrics be
given a different name.
Appendix B -- GUST Font License
-------------------------------
What follows is the exact contents of GUST-FONT-LICENSE.txt from the
upstream distribution of the Latin Modern fonts.
% This is version 1.0, dated 22 June 2009, of the GUST Font License.
% (GUST is the Polish TeX Users Group, http://www.gust.org.pl)
%
% For the most recent version of this license see
% http://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt
% or
% http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt
%
% This work may be distributed and/or modified under the conditions
% of the LaTeX Project Public License, either version 1.3c of this
% license or (at your option) any later version.
%
% Please also observe the following clause:
% 1) it is requested, but not legally required, that derived works be
% distributed only after changing the names of the fonts comprising this
% work and given in an accompanying "manifest", and that the
% files comprising the Work, as listed in the manifest, also be given
% new names. Any exceptions to this request are also given in the
% manifest.
%
% We recommend the manifest be given in a separate file named
% MANIFEST-<fontid>.txt, where <fontid> is some unique identification
% of the font family. If a separate "readme" file accompanies the Work,
% we recommend a name of the form README-<fontid>.txt.
%
% The latest version of the LaTeX Project Public License is in
% http://www.latex-project.org/lppl.txt and version 1.3c or later
% is part of all distributions of LaTeX version 2006/05/20 or later.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,204 @@
package inf101.v18.gfx.gfxmode;
/**
* @author anya
*
*/
public class Direction {
/**
* Construct direction from an angle
*
* @param degrees
* Angle in degrees, where 0 is (1,0)
*/
public static Direction fromDegrees(double degrees) {
return new Direction(degrees);
}
/**
* Construct direction from a vector
*
* @param x
* X direction
* @param y
* Y direction
*/
public static Direction fromVector(double x, double y) {
return new Direction(x, y);
}
private double xDir;
private double yDir;
/**
* Create a new direction.
*
* The direction vector will be normalised to a vector of length 1.
*
* @param degrees
* Angle of direction in degrees
*/
public Direction(double degrees) {
double radians = Math.toRadians(degrees);
this.xDir = Math.cos(radians);
this.yDir = Math.sin(radians);
normalize();
}
/**
* Create a new direction.
*
* The direction vector will be normalised to a vector of length 1.
*
* @param xDir
* X-component of direction vector
* @param yDir
* Y-component of direction vector
*/
public Direction(double xDir, double yDir) {
this.xDir = xDir;
this.yDir = yDir;
normalize();
}
/**
* Multiply direction by distance
*
* @param distance
* @return Position delta
*/
public Point getMovement(double distance) {
return new Point(xDir * distance, -yDir * distance);
}
/**
* @return X-component of direction vector
*
* Same as the Math.cos(toRadians())
*/
public double getX() {
return xDir;
}
/**
* @return Y-component of direction vector
*
* Same as the Math.sin(toRadians())
*/
public double getY() {
return yDir;
}
private void normalize() {
double l = Math.sqrt(xDir * xDir + yDir * yDir);
if (l >= 0.00001) {
xDir = xDir / l;
yDir = yDir / l;
} else if (xDir > 0) {
xDir = 1;
yDir = 0;
} else if (xDir < 0) {
xDir = -1;
yDir = 0;
} else if (yDir > 0) {
xDir = 0;
yDir = 1;
} else if (yDir < 0) {
xDir = 0;
yDir = -1;
} else {
xDir = 1;
yDir = 0;
}
}
/**
* Translate to angle (in degrees)
*
* @return Angle in degrees, -180 .. 180
*/
public double toDegrees() {
return Math.toDegrees(Math.atan2(yDir, xDir));
}
/**
* Translate to angle (in radians)
*
* @return Angle in radians, - ..
*/
public double toRadians() {
return Math.atan2(yDir, xDir);
}
public String toString() {
return String.format("%.2f", toDegrees());
}
/**
* Turn (relative)
*
* @param deltaDir
*/
public Direction turn(Direction deltaDir) {
return new Direction(xDir + deltaDir.xDir, yDir + deltaDir.yDir);
}
/**
* Turn angle degrees
*
* @param angle
*/
public Direction turn(double angle) {
return turnTo(toDegrees() + angle);
}
/**
* Turn around 180 degrees
*/
public Direction turnBack() {
return turn(180.0);
}
/**
* Turn left 90 degrees
*/
public Direction turnLeft() {
return turn(90.0);
}
/**
* Turn right 90 degrees
*/
public Direction turnRight() {
return turn(-90.0);
}
/**
* Absolute turn
*
* @param degrees
* Angle in degrees, where 0 is (1,0)
*/
public Direction turnTo(double degrees) {
return new Direction(degrees);
}
/**
* Turn slightly towards a directions
*
* @param dir
* A direction
* @param percent
* How much to turn (100.0 is the same as turnTo())
*/
public Direction turnTowards(Direction dir, double percent) {
return new Direction(xDir * (1.00 - percent / 100.0) + dir.xDir * (percent / 100.0),
yDir * (1.00 - percent / 100.0) + dir.yDir * (percent / 100.0));
// double thisAngle = toAngle();
// double otherAngle = dir.toAngle();
// turnTo(thisAngle*(1.00 - percent/100.0) +
// otherAngle*(percent/100.0));
}
}

View File

@ -0,0 +1,5 @@
package inf101.v18.gfx.gfxmode;
public enum Gravity {
NORTH, NORTHWEST, WEST, SOUTHWEST, SOUTH, SOUTHEAST, EAST, NORTHEAST, CENTER
}

View File

@ -0,0 +1,17 @@
package inf101.v18.gfx.gfxmode;
import inf101.v18.gfx.IPaintLayer;
import javafx.scene.paint.Paint;
public interface IPainter extends IPaintLayer {
IShape shape();
ITurtle turtle();
IPainter restore();
IPainter save();
IPainter setInk(Paint ink);
}

View File

@ -0,0 +1,239 @@
package inf101.v18.gfx.gfxmode;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Shape;
public interface IShape {
void draw();
void draw(GraphicsContext context);
Shape toFXShape();
String toSvg();
/**
* Set the (x,y)-coordinates of the next draw command
*
* @param p
* Coordinates
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape at(Point p);
/**
* Set the x-coordinate of the next draw command
*
* @param x
* Coordinate
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
IShape x(double x);
/**
* Set the y-coordinate of the next draw command
*
* @param y
* Coordinate
* @return <code>this</code>, for adding more drawing parameters or issuing the
* draw command
*/
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

@ -0,0 +1,240 @@
package inf101.v18.gfx.gfxmode;
import javafx.scene.canvas.GraphicsContext;
public interface ITurtle extends IPainter {
void debugTurtle();
/**
* 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
*/
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
* @return {@code this}, for sending more draw commands
*/
ITurtle line(Point to);
/**
* @return The current angle of the turtle, with 0° pointing to the right and
* 90° pointing up. Same as {@link #getDirection()}.getAngle()
*/
double getAngle();
/**
* @return The current direction of the turtle. Same as calling
* <code>new Direction(getAngle())</code>
*/
Direction getDirection();
/**
* @return The current position of the turtle.
*/
Point getPos();
/**
* Move a distance without drawing.
*
* @param dist
* Distance to move
* @return {@code this}, for sending more draw commands
*/
ITurtle jump(double dist);
/**
* Move a position without drawing.
*
* @param x
* X position to move to
* @param y
* Y position to move to
* @return {@code this}, for sending more draw commands
*/
ITurtle jumpTo(double x, double y);
/**
* Move a position without drawing.
*
* @param to
* X,Y position to move to
* @return {@code this}, for sending more draw commands
*/
ITurtle jumpTo(Point to);
/**
* Move forward the given distance while drawing a line
*
* @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
*
* @param to
* Position to move to
* @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);
/**
* Set the size of the turtle's pen
*
* @param pixels
* Line width, in pixels
* @return {@code this}, for sending more draw commands
* @requires pixels >= 0
*/
ITurtle setPenSize(double pixels);
/**
* Change direction the given number of degrees (relative to the current
* direction).
*
* <p>
* Positive degrees turn <em>left</em> while negative degrees turn
* <em>right</em>.
*
* @param degrees
* @return {@code this}, for sending more draw commands
*/
ITurtle turn(double degrees);
/**
* Turn 180°.
*
* <p>
* Same as <code>turn(180)</code> and <code>turn(-180)</code>.
*
* @return {@code this}, for sending more draw commands
*/
ITurtle turnAround();
/**
* Turn left 90°.
*
* <p>
* Same as <code>turn(90)</code>.
*
* @return {@code this}, for sending more draw commands
*/
ITurtle turnLeft();
/**
* Turn left.
*
* <p>
* Same as <code>turn(degrees)</code>.
*
* @return {@code this}, for sending more draw commands
* @requires degrees >= 0
*/
ITurtle turnLeft(double degrees);
/**
* Turn right 90°.
*
* <p>
* Same as <code>turn(-90)</code>.
*
* @return {@code this}, for sending more draw commands
*/
ITurtle turnRight();
/**
* Turn left.
*
* <p>
* Same as <code>turn(-degrees)</code>.
*
* @return {@code this}, for sending more draw commands
* @requires degrees >= 0
*/
ITurtle turnRight(double degrees);
/**
* Turn to the given bearing.
*
* <p>
* 0° is due right, 90° is up.
*
* @param degrees
* Bearing, in degrees
* @return {@code this}, for sending more draw commands
*/
ITurtle turnTo(double degrees);
/**
* Turn towards the given bearing.
*
* <p>
* Use this method to turn slightly towards something.
*
* <p>
* 0° is due right, 90° is up.
*
* @param degrees
* Bearing, in degrees
* @param percent
* How far to turn, in degrees.
* @return {@code this}, for sending more draw commands
*/
ITurtle turnTowards(double degrees, double percent);
<T> T as(Class<T> class1);
}

View File

@ -0,0 +1,94 @@
package inf101.v18.gfx.gfxmode;
public class Point {
private final double x;
private final double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
/**
* Calculate direction towards other position
*
* @param otherPos
* @return
*/
public Direction directionTo(Point otherPos) {
return new Direction(otherPos.x - x, otherPos.y - y);
}
/**
* Calculate distance to other position
*
* @param otherPos
* @return
*/
public double distanceTo(Point otherPos) {
return Math.sqrt(Math.pow(x - otherPos.x, 2) + Math.pow(y - otherPos.y, 2));
}
/**
* @return The X coordinate
*/
public double getX() {
return x;
}
/**
* @return The Y coordinate
*/
public double getY() {
return y;
}
/**
* Relative move
*
* @param dir
* Direction
* @param distance
* Distance to move
*/
public Point move(Direction dir, double distance) {
return new Point(x + dir.getX() * distance, y - dir.getY() * distance);
}
/**
* Relative move
*
* @param deltaX
* @param deltaY
* @return A new point at x+deltaX, y+deltaY
*/
public Point move(double deltaX, double deltaY) {
return new Point(x + deltaX, y + deltaY);
}
/**
* Relative move
*
* @param deltaPos
*/
public Point move(Point deltaPos) {
return new Point(x + deltaPos.x, y + deltaPos.y);
}
/**
* Change position
*
* @param newX
* the new X coordinate
* @param newY
* the new Y coordinate
* @return A new point at newX, newY
*/
public Point moveTo(double newX, double newY) {
return new Point(newX, newY);
}
public String toString() {
return String.format("(%.2f,%.2f)", x, y);
}
}

View File

@ -0,0 +1,301 @@
package inf101.v18.gfx.gfxmode;
import java.util.List;
import javafx.scene.canvas.GraphicsContext;
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:
case CENTER:
return w / 2;
case EAST:
return w;
case NORTH:
return w / 2;
case NORTHEAST:
return w;
case NORTHWEST:
return 0;
case SOUTH:
return w / 2;
case SOUTHEAST:
return w;
case SOUTHWEST:
return 0;
case WEST:
return 0;
}
}
protected double calcY(Gravity g, double h) {
switch (g) {
default:
case CENTER:
return h / 2;
case EAST:
return h / 2;
case NORTH:
return 0;
case NORTHEAST:
return 0;
case NORTHWEST:
return 0;
case SOUTH:
return h;
case SOUTHEAST:
return h;
case SOUTHWEST:
return h;
case WEST:
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 fillIt(GraphicsContext ctx, ShapePainter p) {
ctx.fillRect(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
}
}
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);
}
public void fillIt(GraphicsContext ctx, ShapePainter p) {
ctx.fillOval(-calcX(p.gravity, p.w), -calcY(p.gravity, p.h), p.w, p.h);
}
}
private static class DrawLine extends DrawCommand {
public void strokeIt(GraphicsContext ctx, ShapePainter p) {
if (p.lineSegments == null) {
double x = -calcX(p.gravity, p.w);
double y = -calcY(p.gravity, p.h);
ctx.strokeLine(x, y, x + p.w, y + p.h);
} else {
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;
}
if (p.closed)
ctx.strokePolygon(xs, ys, nPoints);
else
ctx.strokePolyline(xs, ys, nPoints);
}
}
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);
}
}
}
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 addPoint(Point xy) {
lineSegments.add(xy.getX());
lineSegments.add(xy.getY());
return this;
}
@Override
public IShape addPoint(double x, double y) {
lineSegments.add(x);
lineSegments.add(y);
return this;
}
@Override
public IShape close() {
closed = true;
return this;
}
}

View File

@ -0,0 +1,273 @@
package inf101.v18.gfx.gfxmode;
import java.util.ArrayList;
import java.util.List;
import inf101.v18.gfx.IPaintLayer;
import inf101.v18.gfx.Screen;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
public class TurtlePainter implements IPaintLayer, ITurtle {
static class TurtleState {
protected Point pos;
protected Direction dir;
protected Direction inDir;
protected double penSize = 1.0;
protected Paint ink = Color.BLACK;
public TurtleState() {
}
public TurtleState(TurtleState s) {
pos = s.pos;
dir = s.dir;
inDir = s.inDir;
penSize = s.penSize;
ink = s.ink;
}
}
private final Screen screen;
private final GraphicsContext context;
private final List<TurtleState> stateStack = new ArrayList<>();
private TurtleState state = new TurtleState();
private final Canvas canvas;
private boolean path = false;
public TurtlePainter(Screen screen, Canvas canvas) {
this.screen = screen;
this.canvas = canvas;
this.context = canvas.getGraphicsContext2D();
stateStack.add(new TurtleState());
state.dir = new Direction(1.0, 0.0);
state.pos = new Point(screen.getWidth() / 2, screen.getHeight() / 2);
}
@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);
if (!path) {
// context.save();
context.setStroke(state.ink);
context.setLineWidth(state.penSize);
context.beginPath();
context.moveTo(state.pos.getX(), state.pos.getY());
}
context.bezierCurveTo(c1.getX(), c1.getY(), c2.getX(), c2.getY(), to.getX(), to.getY());
state.inDir = state.dir;
state.pos = to;
state.dir = Direction.fromDegrees(endAngle);
if (!path) {
context.stroke();
// context.restore();
}
return this;
}
@Override
public ITurtle jump(double dist) {
state.inDir = state.dir;
state.pos = state.pos.move(state.dir, dist);
return this;
}
@Override
public ITurtle jumpTo(double x, double y) {
state.inDir = state.dir;
state.pos = new Point(x, y);
return this;
}
@Override
public ITurtle jumpTo(Point to) {
state.inDir = state.dir;
state.pos = to;
return this;
}
@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 IPainter restore() {
if (stateStack.size() > 0) {
state = stateStack.remove(stateStack.size() - 1);
}
return this;
}
@Override
public IPainter save() {
stateStack.add(new TurtleState(state));
return this;
}
@Override
public IPainter setInk(Paint ink) {
state.ink = ink;
return this;
}
@Override
public ITurtle setPenSize(double pixels) {
if (pixels < 0)
throw new IllegalArgumentException("Negative: " + pixels);
state.penSize = pixels;
return this;
}
@Override
public ITurtle turn(double degrees) {
state.dir = state.dir.turn(degrees);
return this;
}
@Override
public ITurtle turnAround() {
return turn(180);
}
@Override
public ITurtle turnLeft() {
return turn(90);
}
@Override
public ITurtle turnLeft(double degrees) {
if (degrees < 0)
throw new IllegalArgumentException("Negative: " + degrees + " (use turn())");
state.dir = state.dir.turn(degrees);
return this;
}
@Override
public ITurtle turnRight() {
return turn(-90);
}
@Override
public ITurtle turnRight(double degrees) {
if (degrees < 0)
throw new IllegalArgumentException("Negative: " + degrees + " (use turn())");
state.dir = state.dir.turn(-degrees);
return this;
}
@Override
public ITurtle turnTo(double degrees) {
state.dir = state.dir.turnTo(degrees);
return this;
}
@Override
public ITurtle turnTowards(double degrees, double percent) {
state.dir = state.dir.turnTowards(new Direction(degrees), percent);
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);
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

@ -0,0 +1,200 @@
package inf101.v18.gfx.textmode;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.function.BiFunction;
public class BlocksAndBoxes {
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 String unicodeBlocksString = String.join("", unicodeBlocks);
public static final String BLOCK_EMPTY = " ";
public static final String BLOCK_BOTTOM_RIGHT = "";
public static final String BLOCK_BOTTOM_LEFT = "";
public static final String BLOCK_BOTTOM = "";
public static final String BLOCK_TOP_RIGHT = "";
public static final String BLOCK_RIGHT = "";
public static final String BLOCK_DIAG_FORWARD = "";
public static final String BLOCK_REVERSE_TOP_LEFT = "";
public static final String BLOCK_TOP_LEFT = "";
public static final String BLOCK_DIAG_BACKWARD = "";
public static final String BLOCK_LEFT = "";
public static final String BLOCK_REVERSE_TOP_RIGHT = "";
public static final String BLOCK_TOP = "";
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 = "";
/**
* Convert a string into a Unicode block graphics character.
*
* <p>
* The block characters corresponds to 2x2-pixel images, and can be used, e.g.,
* to draw a 80x44 pixel image on a 40x22 character text screen.
*
* <p>
* The blocks are specified by four-character strings of spaces and asterisks,
* with space indicating an open space and asterisk indicating a filled "pixel",
* with the pixels arranged in a left-to-right, top-to-bottom order:
*
* <pre>
* 01
* 23
* </pre>
*
* <p>
* So <code>"* **"</code> corresponds to the block character <code>""</code>,
* with this layout:
*
* <pre>
* *
* *
* </pre>
*
* <p>
* The special codes <code>"++++"</code> and <code>"+"</code> corresponds to the
* "grey" block <code>""</code>, and <code>"*"</code> corresponds to the
* "black" block <code>""</code>.
*
* @param s
* A four character string, indicating which block character to
* select.
* @return A Unicode block character
* @throws IllegalArgumentException
* if the string isn't of the expected form
*/
public static String blockChar(String s) {
switch (s.replaceAll("\n", s)) {
case " ":
return unicodeBlocks[0];
case " *":
return unicodeBlocks[1];
case " * ":
return unicodeBlocks[2];
case " **":
return unicodeBlocks[3];
case " * ":
return unicodeBlocks[4];
case " * *":
return unicodeBlocks[5];
case " ** ":
return unicodeBlocks[6];
case " ***":
return unicodeBlocks[7];
case "* ":
return unicodeBlocks[8];
case "* *":
return unicodeBlocks[9];
case "* * ":
return unicodeBlocks[10];
case "* **":
return unicodeBlocks[11];
case "** ":
return unicodeBlocks[12];
case "** *":
return unicodeBlocks[13];
case "*** ":
return unicodeBlocks[14];
case "****":
return unicodeBlocks[15];
case "++++":
return unicodeBlocks[16];
case ".":
return BLOCK_BOTTOM_LEFT;
case "_":
return BLOCK_BOTTOM;
case "/":
return BLOCK_DIAG_FORWARD;
case "\\":
return BLOCK_DIAG_BACKWARD;
case "|":
return BLOCK_LEFT;
case "#":
return BLOCK_FULL;
case "`":
return BLOCK_TOP_LEFT;
case "'":
return BLOCK_TOP_RIGHT;
}
throw new IllegalArgumentException(
"Expected length 4 string of \" \" and \"*\", or \"++++\", got \"" + s + "\"");
};
public static String blockCompose(String b1, String b2, BiFunction<Integer, Integer, Integer> op) {
int i1 = unicodeBlocksString.indexOf(b1);
if (i1 < 0)
throw new IllegalArgumentException("Not a block character: " + b1);
int i2 = unicodeBlocksString.indexOf(b2);
if (i2 < 0)
throw new IllegalArgumentException("Not a block character: " + b1);
if (i1 == 16 || i2 == 16)
return b2;
else
return unicodeBlocks[op.apply(i1, i2)];
}
public static String blockComposeOrOverwrite(String b1, String b2, BiFunction<Integer, Integer, Integer> op) {
int i1 = unicodeBlocksString.indexOf(b1);
int i2 = unicodeBlocksString.indexOf(b2);
if (i1 < 0 || i2 < 0 || i1 == 16 || i2 == 16)
return b2;
else
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) {
for (int bit : order) {
if ((i & bit) != 0)
return unicodeBlocks[i & ~bit];
}
}
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

@ -0,0 +1,271 @@
package inf101.v18.gfx.textmode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javafx.scene.paint.Color;
public class ControlSequences {
public static class CsiPattern {
public static CsiPattern compile0(String pat, String desc, Consumer<Printer> handler) {
CsiPattern csiPattern = new CsiPattern(pat, 0, 0, desc, handler, null, null);
patterns.put(csiPattern.getCommandLetter(), csiPattern);
return csiPattern;
}
public static CsiPattern compile1(String pat, int defaultArg, String desc,
BiConsumer<Printer, Integer> handler) {
CsiPattern csiPattern = new CsiPattern(pat, defaultArg, 1, desc, null, handler, null);
patterns.put(csiPattern.getCommandLetter(), csiPattern);
return csiPattern;
}
public static CsiPattern compileN(String pat, int defaultArg, int numArgs, String desc,
BiConsumer<Printer, List<Integer>> handler) {
CsiPattern csiPattern = new CsiPattern(pat, defaultArg, numArgs, desc, null, null, handler);
patterns.put(csiPattern.getCommandLetter(), csiPattern);
return csiPattern;
}
private String patStr;
private Pattern pattern;
private int defaultArg = 0;
private String desc;
private Consumer<Printer> handler0;
private BiConsumer<Printer, Integer> handler1;
private BiConsumer<Printer, List<Integer>> handlerN;
private int numArgs;
public CsiPattern(String pat, int defaultArg, int numArgs, String desc, Consumer<Printer> handler0,
BiConsumer<Printer, Integer> handler1, BiConsumer<Printer, List<Integer>> handlerN) {
this.patStr = pat;
this.pattern = Pattern.compile(pat);
this.defaultArg = defaultArg;
this.numArgs = numArgs;
this.desc = desc;
this.handler0 = handler0;
this.handler1 = handler1;
this.handlerN = handlerN;
}
public String getCommandLetter() {
return patStr.substring(patStr.length() - 1);
}
public String getDescription() {
return desc;
}
public boolean match(Printer printer, String input) {
Matcher matcher = pattern.matcher(input);
if (matcher.matches()) {
String argStr = matcher.groupCount() > 0 ? matcher.group(1) : "";
String[] args = argStr.split(";");
if (handler0 != null) {
System.out.println("Handling " + getDescription() + ".");
handler0.accept(printer);
} else if (handler1 != null) {
int arg = args.length > 0 && !args[0].equals("") ? Integer.valueOf(args[0]) : defaultArg;
System.out.println("Handling " + getDescription() + ": " + arg);
handler1.accept(printer, arg);
} else if (handlerN != null) {
List<Integer> argList = new ArrayList<>();
for (String s : args) {
if (s.equals(""))
argList.add(defaultArg);
else
argList.add(Integer.valueOf(s));
}
while (argList.size() < numArgs) {
argList.add(defaultArg);
}
System.out.println("Handling " + getDescription() + ": " + argList);
handlerN.accept(printer, argList);
}
return true;
}
return false;
}
}
public static final Map<String, CsiPattern> patterns = new HashMap<>();
public static final CsiPattern CUU = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)A", 1, "cursor up",
(Printer p, Integer i) -> {
p.move(0, -i);
});
public static final CsiPattern CUD = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)B", 1, "cursor down",
(Printer p, Integer i) -> {
p.move(0, i);
});
public static final CsiPattern CUF = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)C", 1, "cursor forward",
(Printer p, Integer i) -> {
p.move(i, 0);
});
public static final CsiPattern CUB = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)D", 1, "cursor back",
(Printer p, Integer i) -> {
p.move(-i, 0);
});
public static final CsiPattern CNL = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)E", 1, "cursor next line",
(Printer p, Integer i) -> {
p.move(0, i);
p.beginningOfLine();
});
public static final CsiPattern CPL = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)F", 1, "cursor previous line",
(Printer p, Integer i) -> {
p.move(0, -i);
p.beginningOfLine();
});
public static final CsiPattern CHA = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)G", 1,
"cursor horizontal absolute", (Printer p, Integer i) -> {
p.moveTo(i, p.getY());
});
public static final CsiPattern CUP = CsiPattern.compileN("\u001b\\\u005b([0-9;]*)H", 1, 2, "cursor position",
(Printer p, List<Integer> i) -> {
p.moveTo(i.get(1), i.get(0));
});
public static final CsiPattern ED = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)J", 0, "erase in display",
(Printer p, Integer i) -> {
if (i == 2 || i == 3)
p.clear();
else
System.err.println("Unimplemented: ED");
});
public static final CsiPattern EK = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)K", 0, "erase in line",
(Printer p, Integer i) -> {
System.err.println("Unimplemented: EK");
});
public static final CsiPattern SU = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)S", 1, "scroll up",
(Printer p, Integer i) -> {
p.scroll(i);
});
public static final CsiPattern SD = CsiPattern.compile1("\u001b\\\u005b([0-9;]*)T", 1, "scroll down",
(Printer p, Integer i) -> {
p.scroll(-i);
});
public static final CsiPattern HVP = CsiPattern.compileN("\u001b\\\u005b([0-9;]*)f", 1, 2,
"horizontal vertical position", (Printer p, List<Integer> l) -> {
p.moveTo(l.get(1), l.get(0));
});
public static final CsiPattern AUX_ON = CsiPattern.compile0("\u001b\\\u005b5i", "aux port on", (Printer p) -> {
System.err.println("Unimplemented: AUX on");
});
public static final CsiPattern AUX_OFF = CsiPattern.compile0("\u001b\\\u005b4i", "aux port off", (Printer p) -> {
System.err.println("Unimplemented: AUX off");
});
public static final CsiPattern DSR = CsiPattern.compile0("\u001b\\\u005b6n", "device status report",
(Printer p) -> {
System.out.println("ESC[" + p.getY() + ";" + p.getX() + "R");
});
public static final CsiPattern SCP = CsiPattern.compile0("\u001b\\\u005bs", "save cursor position", (Printer p) -> {
p.saveCursor();
});
public static final CsiPattern RCP = CsiPattern.compile0("\u001b\\\u005bu", "restore cursor position",
(Printer p) -> {
p.restoreCursor();
});
public static final int F = 0xFF, H = 0xAA, L = 0x55, OFF = 0x00;
public static final Color[] PALETTE_CGA = { //
Color.rgb(0, 0, 0), Color.rgb(0, 0, H), Color.rgb(0, H, 0), Color.rgb(0, H, H), //
Color.rgb(H, 0, 0), Color.rgb(H, 0, H), Color.rgb(H, L, 0), Color.rgb(H, H, H), //
Color.rgb(L, L, L), Color.rgb(L, L, F), Color.rgb(L, F, L), Color.rgb(L, F, F), //
Color.rgb(F, L, L), Color.rgb(F, L, F), Color.rgb(F, F, L), Color.rgb(F, F, F), };
public static final Color[] PALETTE_VGA = { //
Color.rgb(0, 0, 0), Color.rgb(H, 0, 0), Color.rgb(0, H, 0), Color.rgb(H, H, 0), //
Color.rgb(0, 0, H), Color.rgb(H, 0, H), Color.rgb(0, H, H), Color.rgb(H, H, H), //
Color.rgb(L, L, L), Color.rgb(F, L, L), Color.rgb(L, F, L), Color.rgb(F, F, L), //
Color.rgb(L, L, F), Color.rgb(F, L, F), Color.rgb(L, F, F), Color.rgb(F, F, F), };
public static final CsiPattern SGR = CsiPattern.compileN("\u001b\\\u005b([0-9;]*)m", 0, -1,
"select graphics rendition", (Printer p, List<Integer> l) -> {
if (l.size() == 0) {
l.add(0);
}
int[] attrs = { 0, TextFont.ATTR_BRIGHT, TextFont.ATTR_FAINT, TextFont.ATTR_ITALIC,
TextFont.ATTR_UNDERLINE, TextFont.ATTR_BLINK, TextFont.ATTR_BLINK, TextFont.ATTR_INVERSE, 0,
TextFont.ATTR_LINE_THROUGH };
Iterator<Integer> it = l.iterator();
while (it.hasNext()) {
int i = it.next();
if (i == 0) {
p.setVideoAttrs(0);
p.setInk(PALETTE_VGA[7]);
p.setBackground(PALETTE_VGA[0]);
} else if (i < 10) {
p.setVideoAttrEnabled(attrs[i]);
} else if (i >= 20 && i < 30) {
p.setVideoAttrDisabled(attrs[i] - 20);
} else if (i >= 30 && i < 38) {
p.setInk(PALETTE_VGA[i - 30]);
} else if (i == 38) {
p.setInk(decode256(it));
} else if (i == 29) {
p.setInk(Color.WHITE);
} else if (i >= 40 && i < 48) {
p.setBackground(PALETTE_VGA[i - 40]);
} else if (i == 48) {
p.setInk(decode256(it));
} else if (i == 49) {
p.setBackground(Color.BLACK);
} else if (i >= 90 && i < 98) {
p.setInk(PALETTE_VGA[8 + i - 90]);
} else if (i >= 100 && i < 108) {
p.setBackground(PALETTE_VGA[8 + i - 100]);
} else if (i == 53) {
p.setVideoAttrEnabled(TextFont.ATTR_OVERLINE);
} else if (i == 55) {
p.setVideoAttrEnabled(TextFont.ATTR_OVERLINE);
}
}
});
public static boolean applyCsi(Printer printer, String csi) {
CsiPattern csiPattern = patterns.get(csi.substring(csi.length() - 1));
// System.out.println("Applying CSI: " + csi.replaceAll("\u001b", "ESC"));
if (csiPattern != null) {
if (csiPattern.match(printer, csi))
return true;
else
System.err.println("Handler failed for escape sequence: " + csi.replaceAll("\u001b", "ESC"));
} else {
System.err.println("No handler for escape sequence: " + csi.replaceAll("\u001b", "ESC"));
}
return false;
}
private static Color decode256(Iterator<Integer> it) {
int i;
try {
i = it.next();
if (i == 5) {
i = it.next();
if (i < 16)
return PALETTE_VGA[i];
else if (i < 232)
return Color.rgb(i / 36, (i / 6) % 6, i % 6);
else
return Color.gray((i - 232) / 23.0);
} else if (i == 2) {
int r = it.next();
int g = it.next();
int b = it.next();
return Color.rgb(r, g, b);
}
} catch (NoSuchElementException e) {
}
return null;
}
}

View File

@ -0,0 +1,154 @@
package inf101.v18.gfx.textmode;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import javafx.scene.paint.Color;
public class DemoPages {
public static void printAnsiArt(Printer printer) {
printer.moveTo(1, 1);
printer.setAutoScroll(false);
printer.clear();
try (InputStream stream = DemoPages.class.getResourceAsStream("flower.txt")) {
BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
for (String s = reader.readLine(); s != null; s = reader.readLine()) {
printer.println(s);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void printBlockPlotting(Printer printer) {
printer.clear();
printer.setAutoScroll(false);
printer.setVideoAttrs(0);
int topLine = 8;
for (int x = 0; x < 16; x += 1) {
if ((x & 1) > 0)
printer.plot(4 * x + 1, 1 + (topLine - 1) * 2);
if ((x & 2) > 0)
printer.plot(4 * x, 1 + (topLine - 1) * 2);
if ((x & 4) > 0)
printer.plot(4 * x + 1, (topLine - 1) * 2);
if ((x & 8) > 0)
printer.plot(4 * x, (topLine - 1) * 2);
printer.printAt(1 + 2 * x, topLine + 2, BlocksAndBoxes.unicodeBlocks[x]);
printer.printAt(1 + 2 * x, topLine + 4, BlocksAndBoxes.unicodeBlocks[15]);
printer.printAt(1 + 2 * x, topLine + 6, BlocksAndBoxes.unicodeBlocks[~x & +0xf]);
printer.printAt(1 + 2 * x, topLine + 7, String.format("%X", x));
if ((x & 1) > 0)
printer.unplot(4 * x + 1, 1 + (4 + topLine - 1) * 2);
if ((x & 2) > 0)
printer.unplot(4 * x, 1 + (4 + topLine - 1) * 2);
if ((x & 4) > 0)
printer.unplot(4 * x + 1, (4 + topLine - 1) * 2);
if ((x & 8) > 0)
printer.unplot(4 * x, (4 + topLine - 1) * 2);
}
printer.printAt(1, 1,
"Plotting with Unicode Block Elements\n(ZX81-like Graphics)\n\nThe plot/print and unplot/inverse\nlines should be equal:");
printer.printAt(33, topLine, "plot");
printer.printAt(33, topLine + 2, "print");
printer.printAt(33, topLine + 4, "unplot");
printer.printAt(33, topLine + 6, "inverse");
printer.printAt(0, topLine + 9, String.format("Full blocks:\n Clear[%s] Shaded[%s] Opaque[%s]",
BlocksAndBoxes.unicodeBlocks[0], BlocksAndBoxes.unicodeBlocks[16], BlocksAndBoxes.unicodeBlocks[15]));
printer.printAt(41, topLine + 9, "(ZX81 inverted shade and half block");
printer.printAt(41, topLine + 10, "shades are missing in Unicode and");
printer.printAt(41, topLine + 11, "therefore not supported)");
printer.println();
}
public static void printBoxDrawing(Printer printer) {
printer.clear();
printer.setAutoScroll(false);
printer.println(" Latin-1 Boxes & Blocks");
printer.println(" U+0000..00FF U+2500..257F..259F");
printer.println(" ");
printer.println(" 0123456789ABCDEF 0123456789ABCDEF");
for (int y = 0; y < 16; y++) {
printer.print(String.format(" %X", y));
int c = 0x00 + y * 0x010;
for (int x = 0; x < 16; x++) {
printer.print(c >= 0x20 ? Character.toString((char) (c + x)) : " ");
}
printer.print(" ");
if (y < 10) {
printer.print(String.format("%X", y));
c = 0x2500 + y * 0x010;
for (int x = 0; x < 16; x++) {
printer.print(Character.toString((char) (c + x)));
}
}
printer.println();
}
}
public static void printVideoAttributes(Printer printer) {
printer.clear();
printer.setAutoScroll(false);
printer.setVideoAttrs(0);
printer.setInk(Color.BLACK);
printer.setStroke(Color.WHITE);
String demoLine = "Lorem=ipsum-dolor$sit.ametÆØÅå*,|▞&Jumps Over\\the?fLat Dog{}()#\"!";
printer.println("RIBU|" + demoLine);
for (int i = 1; i < 16; i++) {
printer.setVideoAttrs(i);
String s = (i & 1) != 0 ? "X" : " ";
s += (i & 2) != 0 ? "X" : " ";
s += (i & 4) != 0 ? "X" : " ";
s += (i & 8) != 0 ? "X" : " ";
printer.println(s + "|" + demoLine);
}
printer.setVideoAttrs(0);
printer.println();
printer.println("Lines: under, through, over");
printer.setVideoAttrs(TextFont.ATTR_UNDERLINE);
printer.println(" " + demoLine + " ");
printer.setVideoAttrs(TextFont.ATTR_LINE_THROUGH);
printer.println(" " + demoLine + " ");
printer.setVideoAttrs(TextFont.ATTR_OVERLINE);
printer.println(" " + demoLine + " ");
printer.setVideoAttrs(0);
}
public static void printZX(Printer printer) {
printer.moveTo(1, 1);
printer.setAutoScroll(false);
printer.clear();
printer.println(" ▄▄▄ ▄ ▄ ▄");
printer.println(" █ █ █ █ █ █ █");
printer.println(" █ █ █ █ █ █ █");
printer.println(" █ █ █ █ █ █ █");
printer.println(" ▀ ▀ ▀ ▀ ▀▀▀");
printer.println(" ▄▄▄ ▄▄");
printer.println(" █ █");
printer.println(" █ █");
printer.println(" █ █");
printer.println(" ▀▀▀ ▀▀");
printer.println(" ▄▄ ▄ ▄ ▄");
printer.println(" █ █ █ █ █ █");
printer.println(" █ █ █ █ █ █");
printer.println(" █ █ █ █ █ █");
printer.println(" ▀▀ ▀ ▀ ▀▀▀");
printer.println("ON █████ █ █ ███ █");
printer.println("THE █ █ █ █ █ ██");
printer.println("SINCLAIR █ ███ █");
printer.println(" █ █ █ █ █ WITH");
printer.println(" █ █ █ █ █ █ 16K");
printer.println(" █████ █ █ ███ ███ RAM");
printer.moveTo(1, 1);
printer.setAutoScroll(true);
}
}

View File

@ -0,0 +1,680 @@
package inf101.v18.gfx.textmode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;
import inf101.v18.gfx.IPaintLayer;
import inf101.v18.gfx.Screen;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.effect.BlendMode;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
public class Printer implements IPaintLayer {
private static class Char {
public int mode;
public String s;
public Color fill;
public Color stroke;
public Paint bg;
public Char(String s, Color fill, Color stroke, Paint bg, int mode) {
this.s = s;
this.fill = fill;
this.stroke = stroke;
this.bg = bg;
this.mode = mode;
}
}
public static final TextFont FONT_MONOSPACED = new TextFont("Monospaced", 27.00, TextMode.CHAR_BOX_SIZE, 3.4000,
-6.7000, 1.5000, 1.0000, true);
public static final TextFont FONT_LMMONO = new TextFont("lmmono10-regular.otf", 30.00, TextMode.CHAR_BOX_SIZE,
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://www.kreativekorp.com/software/fonts/c64.shtml
*/
public static final TextFont FONT_GIANA = new TextFont("Giana.ttf", 25.00, TextMode.CHAR_BOX_SIZE, 4.6000, -5.0000,
1.0000, 1.0000, true);
/**
* TTF file can be found here:
* http://www.kreativekorp.com/software/fonts/c64.shtml
*/
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;
public static String center(String s, int width) {
for (; s.length() < width; s = " " + s + " ")
;
return s;
}
public static String repeat(String s, int width) {
String r = s;
for (; r.length() < width; r += s)
;
return r;
}
private TextMode textMode;
private Color fill;
private Color stroke;
private Paint background;
private Screen screen;
private List<Char[]> lineBuffer = new ArrayList<>();
private boolean autoscroll = true;
private final Canvas textPage;
private int x = 1, y = 1, savedX = 1, savedY = 1;
// private int pageWidth = LINE_WIDTHS[resMode], pageHeight =
// PAGE_HEIGHTS[resMode];
private int leftMargin = 1, topMargin = 1;
private TextFont font = FONT_LMMONO;
private int videoAttrs = 0;
private String csiSeq = null;
private boolean csiEnabled = true;
private int csiMode = 0;
public Printer(Screen screen, Canvas page) {
this.screen = screen;
this.textPage = page;
for (int i = 0; i < TextMode.PAGE_HEIGHT_MAX; i++) {
lineBuffer.add(new Char[TextMode.LINE_WIDTH_MAX]);
}
resetFull();
}
public void addToCharBuffer(String string) {
string.codePoints().mapToObj((int i) -> String.valueOf(Character.toChars(i))).forEach((String s) -> {
if (csiMode != 0) {
s = addToCsiBuffer(s);
}
switch (s) {
case "\r":
moveTo(leftMargin, y);
break;
case "\n":
moveTo(leftMargin, y + 1);
break;
case "\f":
GraphicsContext context = textPage.getGraphicsContext2D();
moveTo(leftMargin, topMargin);
for (Char[] line : lineBuffer)
Arrays.fill(line, null);
if (background != null && background != Color.TRANSPARENT) {
context.setFill(background);
context.fillRect(0.0, 0.0, textPage.getWidth(), textPage.getHeight());
} else
context.clearRect(0.0, 0.0, textPage.getWidth(), textPage.getHeight());
break;
case "\b":
moveHoriz(-1);
break;
case "\t":
moveTo((x + 8) % 8, y);
break;
case "\u001b":
if (csiEnabled) {
csiSeq = s;
csiMode = 1;
}
break;
default:
if (s.length() > 0 && s.codePointAt(0) >= 0x20) {
drawChar(x, y, setChar(x, y, s));
moveHoriz(1);
}
break;
}
});
}
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) {
case "[":
csiMode = 2;
csiSeq += s;
break;
case "c":
csiMode = 0;
resetFull();
break;
default:
csiReset();
return s;
}
} else if (csiMode == 2) {
int c = s.codePointAt(0);
if (c >= 0x30 && c <= 0x3f) {
csiSeq += s;
} else if (c >= 0x20 && c <= 0x2f) {
csiMode = 3;
csiSeq += s;
} else if (c >= 0x40 && c <= 0x7e) {
csiSeq += s;
csiFinish();
} else {
csiReset();
return s;
}
} else if (csiMode == 3) {
int c = s.codePointAt(0);
if (c >= 0x20 && c <= 0x2f) {
csiSeq += s;
} else if (c >= 0x40 && c <= 0x7e) {
csiSeq += s;
csiFinish();
} else {
csiReset();
return s;
}
}
return "";
}
public void beginningOfLine() {
x = leftMargin;
}
public void beginningOfPage() {
x = leftMargin;
y = topMargin;
}
public void clear() {
print("\f");
}
public void clearAt(int x, int y) {
printAt(x, y, " ");
}
private int constrainX(int x) {
return x; // Math.min(LINE_WIDTH_HIRES, Math.max(1, x));
}
public int constrainY(int y) {
return y; // Math.min(pageHeight, Math.max(1, y));
}
public int constrainYOrScroll(int y) {
if (autoscroll) {
if (y < 1) {
scroll(y - 1);
return 1;
} else if (y > getPageHeight()) {
scroll(y - getPageHeight());
return getPageHeight();
}
}
return y;// Math.min(pageHeight, Math.max(1, y));
}
private void csiFinish() {
ControlSequences.applyCsi(this, csiSeq);
csiReset();
}
private void csiReset() {
csiMode = 0;
csiSeq = null;
}
public void cycleMode(boolean adjustDisplayAspect) {
textMode = textMode.nextMode();
if (adjustDisplayAspect)
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);
}
}
context.restore();
}
public boolean getBold() {
return (videoAttrs & TextFont.ATTR_BRIGHT) != 0;
}
public String getChar(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];
}
if (c != null)
return c.s;
else
return " ";
}
public double getCharHeight() {
return textMode.getCharHeight();
}
public double getCharWidth() {
return textMode.getCharWidth();
}
public Color getColor(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];
}
if (c != null)
return c.fill;
else
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;
}
public boolean getItalics() {
return (videoAttrs & TextFont.ATTR_ITALIC) != 0;
}
/**
* @return the leftMargin
*/
public int getLeftMargin() {
return leftMargin;
}
public int getLineWidth() {
return textMode.getLineWidth();
}
public int getPageHeight() {
return textMode.getPageHeight();
}
public boolean getReverseVideo() {
return (videoAttrs & TextFont.ATTR_INVERSE) != 0;
}
/**
* @return the topMargin
*/
public int getTopMargin() {
return topMargin;
}
public int getVideoMode() {
return videoAttrs;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public boolean isFilled(int x, int y) {
return !getChar(x, y).equals(" ");
}
public void move(int deltaX, int deltaY) {
x = constrainX(x + deltaX);
y = constrainYOrScroll(y + deltaY);
}
public void moveHoriz(int dist) {
x = constrainX(x + dist);
}
public void moveTo(int newX, int newY) {
x = constrainX(newX);
y = constrainYOrScroll(newY);
}
public void moveVert(int dist) {
y = constrainYOrScroll(y + dist);
}
public void plot(int x, int y) {
plot(x, y, (a, b) -> a | b);
}
public void plot(int x, int y, BiFunction<Integer, Integer, Integer> op) {
int textX = (x) / 2 + 1;
int textY = (y) / 2 + 1;
int bitPos = (x + 1) % 2 + ((y + 1) % 2) * 2;
String blockChar = BlocksAndBoxes.unicodeBlocks[1 << bitPos];
// System.out.println(blockChar + ", " + bitPos + ", ("+ (x) + ", " + (y) + ")"+
// ", (" + (textX) + ", " + (textY) + ")");
String s = BlocksAndBoxes.blockComposeOrOverwrite(getChar(textX, textY), blockChar, op);
// System.out.println("Merge '" + getChar(textX, textY) + "' + '" + blockChar +
// "' = '" + s + "'");
printAt(textX, textY, s);
}
public void print(String s) {
addToCharBuffer(s);
}
public void print(String s, Color paint) {
Color tmp = fill;
fill = paint;
addToCharBuffer(s);
fill = tmp;
}
public void printAt(int atX, int atY, String s) {
moveTo(atX, atY);
print(s);
}
public void printAt(int atX, int atY, String s, Color ink) {
moveTo(atX, atY);
print(s, ink);
}
public void println() {
print("\n");
}
public void println(String s) {
print(s);
print("\n");
}
public void redrawTextPage() {
/*
* System.out.printf("redrawTextPage benchmark");
* System.out.printf(" %5s %5s %7s %4s %5s %5s %5s%n", "ms", "chars",
* "ms/char", "mode", "indir", "inv", "fake"); for (int m = -1; m < 8; m++) {
* long t0 = System.currentTimeMillis(); int n = 0;
*/
GraphicsContext context = textPage.getGraphicsContext2D();
if (background != null && background != Color.TRANSPARENT) {
context.setFill(background);
context.fillRect(0.0, 0.0, textPage.getWidth(), textPage.getHeight());
} else
context.clearRect(0.0, 0.0, textPage.getWidth(), textPage.getHeight());
for (int tmpY = 1; tmpY <= getPageHeight(); tmpY++) {
Char[] line = lineBuffer.get(tmpY - 1);
for (int tmpX = 1; tmpX <= getLineWidth(); tmpX++) {
Char c = line[tmpX - 1];
if (c != null) {
context.save();
context.setFill(c.fill);
context.setStroke(c.stroke);
font.drawTextAt(context, (tmpX - 1) * getCharWidth(), tmpY * getCharHeight(), c.s,
textMode.getCharWidth() / textMode.getCharBoxSize(), c.mode/* m */, c.bg);
context.restore();
// n++;
}
}
}
/*
* long t = System.currentTimeMillis() - t0; if (m >= 0)
* System.out.printf(" %5d %5d %7.4f %4d %5b %5b %5b%n", t, n, ((double) t) /
* n, m, (m & 3) != 0, (m & 1) != 0, (m & 4) != 0); } System.out.println();
*/
}
public void resetAttrs() {
this.fill = DEFAULT_FILL;
this.stroke = DEFAULT_STROKE;
this.background = DEFAULT_BACKGROUND;
this.videoAttrs = 0;
this.csiSeq = null;
this.csiMode = 0;
}
public void resetFull() {
resetAttrs();
beginningOfPage();
this.autoscroll = true;
this.textMode = DEFAULT_MODE;
redrawTextPage();
}
public void restoreCursor() {
x = savedX;
y = savedY;
}
public void saveCursor() {
savedX = x;
savedY = y;
}
void scroll(int i) {
while (i < 0) {
scrollDown();
i++;
}
while (i > 0) {
scrollUp();
i--;
}
}
public void scrollDown() {
Char[] remove = lineBuffer.remove(lineBuffer.size() - 1);
Arrays.fill(remove, null);
lineBuffer.add(0, remove);
redrawTextPage();
}
public void scrollUp() {
Char[] remove = lineBuffer.remove(0);
Arrays.fill(remove, null);
lineBuffer.add(remove);
redrawTextPage();
}
public boolean setAutoScroll(boolean autoScroll) {
boolean old = autoscroll;
autoscroll = autoScroll;
return old;
}
public void setBackground(Paint bgColor) {
this.background = bgColor != null ? bgColor : DEFAULT_BACKGROUND;
}
public void setBold(boolean enabled) {
if (enabled)
videoAttrs |= TextFont.ATTR_BRIGHT;
else
videoAttrs &= ~TextFont.ATTR_BRIGHT;
}
public Char setChar(int x, int y, String s) {
if (x > 0 && x <= TextMode.LINE_WIDTH_MAX && y > 0 && y <= TextMode.PAGE_HEIGHT_MAX) {
Char c = new Char(s, fill, stroke, background, videoAttrs);
lineBuffer.get(y - 1)[x - 1] = c;
return c;
}
return null;
}
public void setFill(Color fill) {
this.fill = fill != null ? fill : DEFAULT_FILL;
}
public void setFont(TextFont font) {
this.font = font;
}
public void setInk(Color ink) {
fill = ink != null ? ink : DEFAULT_FILL;
stroke = ink != null ? ink : DEFAULT_STROKE;
}
public void setItalics(boolean enabled) {
if (enabled)
videoAttrs |= TextFont.ATTR_ITALIC;
else
videoAttrs &= ~TextFont.ATTR_ITALIC;
}
/**
*/
public void setLeftMargin() {
this.leftMargin = x;
}
/**
* @param leftMargin
* the leftMargin to set
*/
public void setLeftMargin(int leftMargin) {
this.leftMargin = constrainX(leftMargin);
}
public void setReverseVideo(boolean enabled) {
if (enabled)
videoAttrs |= TextFont.ATTR_INVERSE;
else
videoAttrs &= ~TextFont.ATTR_INVERSE;
}
public void setStroke(Color stroke) {
this.stroke = stroke != null ? stroke : DEFAULT_STROKE;
}
public void setTopMargin() {
this.topMargin = y;
}
/**
* @param topMargin
* the topMargin to set
*/
public void setTopMargin(int topMargin) {
this.topMargin = constrainY(topMargin);
}
public void setVideoAttrs(int attr) {
videoAttrs = attr;
}
public void setVideoAttrDisabled(int attr) {
videoAttrs &= ~attr;
}
public void setVideoAttrEnabled(int attr) {
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 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

@ -0,0 +1,980 @@
package inf101.v18.gfx.textmode;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import javafx.geometry.Point2D;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.text.Font;
import javafx.scene.transform.Affine;
/**
* TextFont for grid-based text / character graphics
* <p>
* TextFonts are used for drawing text and character graphics. Each character is
* assumed to be uniform in size, fitting within a square with sides of length
* {@link #getSquareSize()}. The given font file should contain a monospaced
* font of a type suitable for JavaFX, such as OpenType or TrueType.
*
* <p>
* Additional horizontal and vertical positioning and scaling can be used to
* make the font fit with the square-shaped concept.
*
* <p>
* See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics
* context for writing with the font, or
* {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly
* horizontal scaling, e.g. to make half-width letters ("hires" mode).
*
*/
public class TextFont {
public static final int ATTR_INVERSE = 0x01;
public static final int ATTR_ITALIC = 0x02;
public static final int ATTR_BOLD = 0x04;
public static final int ATTR_OUTLINE = 0x08;
public static final int ATTR_UNDERLINE = 0x10;
public static final int ATTR_OVERLINE = 0x20;
public static final int ATTR_LINE_THROUGH = 0x40;
public static final int ATTR_OVERSTRIKE = 0x80;
public static final int ATTR_CLIP = 0x80;
public static final int ATTR_NO_FAKE_CHARS = 0x100;
public static final int ATTR_BLINK = 0x200; // NOT IMPLEMENTED
public static final int ATTR_FAINT = 0x400; // NOT IMPLEMENTED
public static final int ATTR_BRIGHT = 0x800;
private static final String[] searchPath = { "", "../", "../fonts/" };
private static final Map<String, String> loadedFonts = new HashMap<>();
private static final double thin = 2.0, thick = 4.0;
private static final String[] boxDrawingShapes = { // lines
"--..", "**..", "..--", "..**",
// dashed lines
"3--..", "3**..", "3..--", "3..**", "4--..", "4**..", "4..--", "4..**",
// corners
".-.-", ".*.-", ".-.*", ".*.*", "-..-", "*..-", "-..*", "*..*", ".--.", ".*-.", ".-*.", ".**.", "-.-.",
"*.-.", "-.*.", "*.*.",
// |-
".---", ".*--", ".-*- ", ".--* ", ".-**", ".**-", ".*-*", ".***", "-.--", "*.--", "-.*-", "-.-*", "-.**",
"*.*-", "*.-*", "*.**",
// T
"--.-", "*-.-", "-*.-", "**.-", "--.*", "*-.*", "-*.*", "**.*", "---.", "*--. ", "-*-.", "**-.", "--*.",
"*-*.", "-**.", "***.",
// +
"----", "*---", "-*--", "**--", "--*-", "---*", "--**", "*-*-", "-**-", "*--*", "-*-*", "***-", "**-*",
"*-**", "-***", "****",
// dashes
"2--..", "2**..", "2..--", "2..**",
// double lines
"==..", "..==", ".=.-", ".-.=", ".N.N", "=..-", "-..=", "M..M", ".=-.", ".-=.", ".MM.", "=.-.", "-.=.",
"N.N.", ".=--", ".s==", ".zMN", "=.--", "s.==", "z.NM", "==.s", "--.=", "MN.z", "==s.", "--=.", "NMz.",
"==--", "--==", "zzzz",
// round corners
"0.-.-", "0-..-", "0-.-.", "0.--.",
// diagonals
"////", "\\\\\\\\", "//\\\\\\",
// short lines
"-...", "..-.", ".-..", "...-", "*...", "..*.", ".*..", "...*",
// thin/thick lines
"-*..", "..-*", "*-..", "..*-" };
public static void drawMasked(GraphicsContext ctx, Image src, Image mask, double x, double y, boolean invert,
boolean blend) {
int w = (int) src.getWidth();
int h = (int) src.getHeight();
PixelReader pixelSrc = src.getPixelReader();
PixelReader pixelMask = mask.getPixelReader();
PixelWriter pixelWriter = ctx.getPixelWriter();
Affine transform = ctx.getTransform();
Point2D point = transform.transform(x, y);
int dx = (int) point.getX(), dy = (int) point.getY();
for (int px = 0; px < w; px++) {
for (int py = 0; py < h; py++) {
int a = pixelMask.getArgb(px, py) >>> 24;
int rgb = pixelSrc.getArgb(px, py);
if (invert)
a = ~a & 0xff;
if (blend)
a = ((rgb >>> 24) * a) >>> 8;
pixelWriter.setArgb(px + dx, py + dy, (a << 24) | rgb);
}
}
}
public static void fillInverse(GraphicsContext ctx, Image img, double x, double y) {
int w = (int) img.getWidth();
int h = (int) img.getHeight();
PixelReader pixelReader = img.getPixelReader();
PixelWriter pixelWriter = ctx.getPixelWriter();
Affine transform = ctx.getTransform();
Point2D point = transform.transform(x, y);
int dx = (int) point.getX(), dy = (int) point.getY();
Color c = ctx.getFill() instanceof Color ? (Color) ctx.getFill() : Color.BLACK;
int rgb = ((int) (c.getRed() * 255)) << 16 | ((int) (c.getGreen() * 255)) << 8 | ((int) (c.getBlue() * 255));
for (int px = 0; px < w; px++) {
for (int py = 0; py < h; py++) {
int a = (~pixelReader.getArgb(px, py) & 0xff000000);
// if(a != 0)
pixelWriter.setArgb(px + dx, py + dy, a | rgb);
}
}
}
/**
* Load a font.
*
* Will first try to use the provided string as a file name, and load the font
* from a font file (should be in a format supported by JavaFX). If loading from
* file fails, we assume we have been given the name of a font which is passed
* directly to JavaFX for loading. A fallback font is used if this fails.
*
* When looking for files, his method will search for the file in a search path
* starting with the folder containing the {@link TextFont} class file (using
* Java's standard {@link Class#getResourceAsStream(String)} system). The search
* path also includes ".." (parent directory) and "../fonts".
*
* The loaded font will be cached, so that additional calls with the same file
* name will not cause the file to be loaded again.
*
* If the font cannot be loaded, a default font will be substituted. You may
* check which font you got using {@link Font#getName()} or
* {@link Font#getFamily()}.
*
* @param name
* Font name, or relative path to the file
* @param size
* Desired point size of the font
* @return A JavaFX font
*/
public static final Font findFont(String name, double size) {
return findFont(name, size, TextFont.class);
}
/**
* Load a font.
*
* Will first try to use the provided string as a file name, and load the font
* from a font file (should be in a format supported by JavaFX). If loading from
* file fails, we assume we have been given the name of a font which is passed
* directly to JavaFX for loading. A fallback font is used if this fails.
*
* When looking for files, this method will search for the file in a search path
* starting with the folder containing the provided Java class (using Java's
* standard {@link Class#getResourceAsStream(String)} system). The search path
* also includes ".." (parent directory) and "../fonts".
*
* The loaded font will be cached, so that additional calls with the same file
* name will not cause the file to be loaded again.
*
* If the font cannot be loaded, a default font will be substituted. You may
* check which font you got using {@link Font#getName()} or
* {@link Font#getFamily()}.
*
* @param name
* Name of a font, or relative path to the font file
* @param size
* Desired point size of the font
* @param clazz
* A class for finding files relative to
* @return A JavaFX font
*/
public static final Font findFont(String name, double size, Class<?> clazz) {
if (name == null || size < 0)
throw new IllegalArgumentException();
if (loadedFonts.containsKey(name))
return Font.font(loadedFonts.get(name), size);
for (String path : searchPath) {
try (InputStream stream = clazz.getResourceAsStream(path + name)) {
Font font = Font.loadFont(stream, size);
if (font != null) {
loadedFonts.put(name, font.getName());
// System.err.println("Found: " + font.getName());
return font;
}
} catch (FileNotFoundException e) {
// we'll just try the next alternative in the search path
} catch (IOException e) {
e.printStackTrace();
}
}
Font font = Font.font(name, size);
if (font == null)
font = Font.font(size);
if (font == null)
throw new RuntimeException("Even the default font seems to be unavailable this shouldn't happen! :(");
if (font.getName().equals(Font.getDefault().getName())) {
System.err.println("TextFont: Default font '" + font.getName() + "' substituted for '" + name + "'");
}
// System.err.println("Found: " + name + "=" + font.getName());
loadedFonts.put(name, name);
return font;
}
private Canvas tmpCanvas;
private final WritableImage img;
/**
* The JavaFX font
*/
private Font font;
/** Font size */
private final double size;
/**
* Horizontal positioning of letters.
*
* Each letter should be approximately centered within its available
* square-shaped space.
*/
private final double xTranslate;
/**
* Vertical positioning of letters.
*
* Each letter should be positioned on the baseline so that ascenders and
* descenders fall within its available square-shaped space.
*/
private final double yTranslate;
/**
* Horizontal scaling factor (1.0 means no scaling)
*
* Most fonts are relatively tall and narrow, and need horizontal scaling to fit
* a square shape.
*/
private final double xScale;
/**
* Vertical scaling factor (1.0 means no scaling)
*/
private final double yScale;
/**
* Width and height of the square-shaped space each letter should fit within
*/
private double squareSize;
private String fileName;
SnapshotParameters snapshotParameters = new SnapshotParameters();
{
snapshotParameters.setFill(Color.TRANSPARENT);
}
/**
* Create a new TextFont.
*
* <p>
* TextFonts are used for drawing text and character graphics. Each character is
* assumed to be uniform in size, fitting within a square with sides of length
* {@link #getSquareSize()}. The given font file should contain a monospaced
* font of a type suitable for JavaFX, such as OpenType or TrueType.
*
* <p>
* Additional horizontal and vertical positioning and scaling can be used to
* make the font fit with the square-shaped concept.
*
* <p>
* See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics
* context for writing with the font, or
* {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly
* horizontal scaling, e.g. to make half-width letters ("hires" mode).
*
*
* @param font
* Name of the font file. Will search for the file in the same folder
* as the TextFont class, as well as ".." and "../fonts".
* @param size
* Point size of the font.
* @param squareSize
* The width and height of a square defining the bounds of letters
* @param xTranslate
* Horizontal positioning of letters
* @param yTranslate
* Vertical positioning of letters
* @param xScale
* Horizontal scaling factor
* @param yScale
* Vertical scaling factor
*/
public TextFont(Font font, double squareSize, double xTranslate, double yTranslate, double xScale, double yScale) {
super();
this.fileName = font.getName();
this.font = font;
this.size = font.getSize();
this.squareSize = squareSize;
this.xTranslate = xTranslate;
this.yTranslate = yTranslate;
this.xScale = xScale;
this.yScale = yScale;
this.img = new WritableImage((int) squareSize, (int) squareSize);
}
/**
* Create a new TextFont.
*
* <p>
* TextFonts are used for drawing text and character graphics. Each character is
* assumed to be uniform in size, fitting within a square with sides of length
* {@link #getSquareSize()}. The given font file should contain a monospaced
* font of a type suitable for JavaFX, such as OpenType or TrueType.
*
* <p>
* Additional horizontal and vertical positioning and scaling can be used to
* make the font fit with the square-shaped concept.
*
* <p>
* See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics
* context for writing with the font, or
* {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly
* horizontal scaling, e.g. to make half-width letters ("hires" mode).
*
*
* @param font
* Name of the font file. Will search for the file in the same folder
* as the TextFont class, as well as ".." and "../fonts".
* @param size
* Point size of the font.
* @param squareSize
* The width and height of a square defining the bounds of letters
* @param xTranslate
* Horizontal positioning of letters
* @param yTranslate
* Vertical positioning of letters
* @param xScale
* Horizontal scaling factor
* @param yScale
* Vertical scaling factor
*/
public TextFont(String fileName, double size, double squareSize, double xTranslate, double yTranslate,
double xScale, double yScale) {
super();
this.fileName = fileName;
this.font = findFont(fileName, size);
this.size = size;
this.squareSize = squareSize;
this.xTranslate = xTranslate;
this.yTranslate = yTranslate;
this.xScale = xScale;
this.yScale = yScale;
this.img = new WritableImage((int) squareSize, (int) squareSize);
}
/**
* Create a new TextFont.
*
* <p>
* TextFonts are used for drawing text and character graphics. Each character is
* assumed to be uniform in size, fitting within a square with sides of length
* {@link #getSquareSize()}. The given font file should contain a monospaced
* font of a type suitable for JavaFX, such as OpenType or TrueType.
*
* <p>
* Additional horizontal and vertical positioning and scaling can be used to
* make the font fit with the square-shaped concept.
*
* <p>
* See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics
* context for writing with the font, or
* {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly
* horizontal scaling, e.g. to make half-width letters ("hires" mode).
*
*
* @param font
* Name of the font file. Will search for the file in the same folder
* as the TextFont class, as well as ".." and "../fonts".
* @param size
* Point size of the font.
* @param squareSize
* The width and height of a square defining the bounds of letters
* @param xTranslate
* Horizontal positioning of letters
* @param yTranslate
* Vertical positioning of letters
* @param xScale
* Horizontal scaling factor
* @param yScale
* Vertical scaling factor
* @param deferLoading
* True if the font file shouldn't be loaded before the font is
* actually used
*/
public TextFont(String fileName, double size, double squareSize, double xTranslate, double yTranslate,
double xScale, double yScale, boolean deferLoading) {
super();
this.fileName = fileName;
this.font = deferLoading ? null : findFont(fileName, size);
this.size = size;
this.squareSize = squareSize;
this.xTranslate = xTranslate;
this.yTranslate = yTranslate;
this.xScale = xScale;
this.yScale = yScale;
this.img = new WritableImage((int) squareSize, (int) squareSize);
}
/**
* Create a copy of this font, with the given adjustments to the translation and
* scaling.
*
* @param deltaXTranslate
* @param deltaYTranslate
* @param deltaXScale
* @param deltaYScale
* @return
*/
public TextFont adjust(double size, double deltaXTranslate, double deltaYTranslate, double deltaXScale,
double deltaYScale) {
if (size == 0.0) {
return new TextFont(fileName, this.size, squareSize, xTranslate + deltaXTranslate,
yTranslate + deltaYTranslate, xScale + deltaXScale, yScale + deltaYScale);
} else {
return new TextFont(fileName, this.size + size, squareSize, xTranslate + deltaXTranslate,
yTranslate + deltaYTranslate, xScale + deltaXScale, yScale + deltaYScale);
}
}
/**
* Draw the given text at position (0,0).
*
* The <code>ctx</code> should normally be translated to the appropriate text
* position before calling this method.
*
* Text will be clipped so each character fits its expected square-shaped area,
* and the area will be cleared to transparency before drwaing.
*
* The graphics context's current path will be overwritten.
*
* @param ctx
* a grapics context
* @param text
* string to be printed
*/
public void drawText(GraphicsContext ctx, String text) {
textAt(ctx, 0.0, 0.0, text, 1.0, true, true, ctx.getFill(), null, 0, null);
}
/**
* Draw the given text at position (0,0), with horizontal scaling.
*
* The <code>ctx</code> should normally be translated to the appropriate text
* position before calling this method.
*
* Text will be clipped so each character fits its expected square-shaped area,
* and the area will be cleared to transparency before drwaing.
*
* The graphics context's current path will be overwritten.
*
* @param ctx
* a grapics context
* @param text
* string to be printed
* @param xScaleFactor
* a horizontal scaling factor
*/
public void drawText(GraphicsContext ctx, String text, double xScaleFactor) {
textAt(ctx, 0.0, 0.0, text, xScaleFactor, true, false, ctx.getFill(), null, 0, null);
}
/**
* Draw the given text at position (x,y).
*
* Text will be clipped so each character fits its expected square-shaped area,
* and the area will be cleared to transparency before drwaing.
*
* The graphics context's current path will be overwritten.
*
* @param ctx
* a grapics context
* @param x
* X-position of the lower left corner of the text
* @param y
* Y-position of the lower left corner of the text
* @param text
* string to be printed
*/
public void drawTextAt(GraphicsContext ctx, double x, double y, String text) {
textAt(ctx, x, y, text, 1.0, true, false, ctx.getFill(), null, 0, null);
}
/**
* Draw the given text at position (x, y), with horizontal scaling.
*
* Text will be clipped so each character fits its expected square-shaped area,
* and the area will be cleared to transparency before drwaing.
*
* The graphics context's current path will be overwritten.
*
* @param ctx
* a grapics context
* @param x
* X-position of the lower left corner of the text
* @param y
* Y-position of the lower left corner of the text
* @param text
* string to be printed
* @param xScaleFactor
* a horizontal scaling factor
*/
public void drawTextAt(GraphicsContext ctx, double x, double y, String text, double xScaleFactor, int mode,
Paint bg) {
textAt(ctx, x, y, text, xScaleFactor, true, false, ctx.getFill(), null, mode, bg);
}
private void fakeBlockElement(GraphicsContext ctx, String text, double xScaleFactor) {
text.codePoints().forEach((int c) -> {
// System.out.printf("%s %x%n", text, c);
if (c >= 0x2596 && c <= 0x259f) { // quadrants
int bits = BlocksAndBoxes.unicodeBlocksString.indexOf(c);
if ((bits & 1) > 0) { // lower right
ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize / 2, (squareSize / 2) * xScaleFactor,
squareSize / 2);
}
if ((bits & 2) > 0) { // lower left
ctx.fillRect(0, -squareSize / 2, (squareSize / 2) * xScaleFactor, squareSize / 2);
}
if ((bits & 4) > 0) { // upper right
ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize, (squareSize / 2) * xScaleFactor,
squareSize / 2);
}
if ((bits & 8) > 0) { // upper left
ctx.fillRect(0, -squareSize, (squareSize / 2) * xScaleFactor, squareSize / 2);
}
} else if (c == 0x2580) { // upper half
ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize / 2);
} else if (c > 0x2580 && c <= 0x2588) { // x/8 block
int height = c - 0x2580;
ctx.fillRect(0, -(height * squareSize) / 8, (squareSize) * xScaleFactor, (height * squareSize) / 8);
} else if (c >= 0x2589 && c <= 0x258f) { // x/8 block
int width = 8 - (c - 0x2588);
ctx.fillRect(0, -squareSize, ((width * squareSize) / 8) * xScaleFactor, squareSize);
} else if (c == 0x2590) { // right half
ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize, (squareSize / 2) * xScaleFactor, squareSize);
} else if (c == 0x2591) { // light shade
ctx.save();
ctx.setGlobalAlpha(0.25);
ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize);
ctx.restore();
} else if (c == 0x2592) { // medium shade
ctx.save();
ctx.setGlobalAlpha(0.5);
ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize);
ctx.restore();
} else if (c == 0x2593) { // dark shade
ctx.save();
ctx.setGlobalAlpha(0.75);
ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize);
ctx.restore();
} else if (c == 0x2594) { // upper eighth
ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize / 8);
} else if (c == 0x2595) { // right eighth
ctx.fillRect(((7 * squareSize) / 8) * xScaleFactor, -squareSize, (squareSize / 8) * xScaleFactor,
squareSize);
} else if (c == 0x2571) {
ctx.save();
ctx.setLineWidth(2.0);
ctx.strokeLine(0, 0, squareSize * xScaleFactor, -squareSize);
ctx.restore();
} else if (c == 0x2572) {
ctx.save();
ctx.setLineWidth(2.0);
ctx.strokeLine(0, -squareSize, squareSize * xScaleFactor, 0);
ctx.restore();
} else if (c == 0x2573) {
ctx.save();
ctx.setLineWidth(2.0);
ctx.strokeLine(0, 0, squareSize * xScaleFactor, -squareSize);
ctx.strokeLine(0, -squareSize, squareSize * xScaleFactor, 0);
ctx.restore();
} else if (c >= 0x2500 && c <= 0x257f) {
ctx.save();
ctx.setLineCap(StrokeLineCap.BUTT);
String spec = boxDrawingShapes[c - 0x2500];
int i = 0;
double extraThickness = 0.0;
if (Character.isDigit(spec.charAt(i))) {
extraThickness = setContextFromChar(ctx, spec.charAt(i++));
}
char s;
double hThickness = Math.max(setContextFromChar(ctx, spec.charAt(i)),
setContextFromChar(ctx, spec.charAt(i + 1))) + extraThickness;
double vThickness = Math.max(setContextFromChar(ctx, spec.charAt(i + 2)),
setContextFromChar(ctx, spec.charAt(i + 3))) + extraThickness;
if (c >= 0x2550 || spec.charAt(i) != spec.charAt(i + 1)) {
s = spec.charAt(i++);
setContextFromChar(ctx, s);
strokeHalfLine(ctx, xScaleFactor, -1, 0, s, vThickness);
s = spec.charAt(i++);
setContextFromChar(ctx, s);
strokeHalfLine(ctx, xScaleFactor, 1, 0, s, vThickness);
} else {
s = spec.charAt(i++);
s = spec.charAt(i++);
setContextFromChar(ctx, s);
strokeFullLine(ctx, xScaleFactor, 1, 0, s);
}
if (c >= 0x2550 || spec.charAt(i) != spec.charAt(i + 1)) {
s = spec.charAt(i++);
setContextFromChar(ctx, s);
strokeHalfLine(ctx, xScaleFactor, 0, -1, s, hThickness);
s = spec.charAt(i++);
setContextFromChar(ctx, s);
strokeHalfLine(ctx, xScaleFactor, 0, 1, s, hThickness);
} else {
s = spec.charAt(i++);
s = spec.charAt(i++);
setContextFromChar(ctx, s);
strokeFullLine(ctx, xScaleFactor, 0, 1, s);
}
ctx.restore();
}
});
}
/**
* @return the font
*/
public Font getFont() {
if (font == null)
font = findFont(fileName, size);
return font;
}
/**
* @return the size
*/
public double getSize() {
return size;
}
/**
* Width and height of the square-shaped space each letter should fit within
*
* @return the squareSize
*/
public double getSquareSize() {
return squareSize;
}
private Canvas getTmpCanvas() {
if (tmpCanvas == null) {
tmpCanvas = new Canvas(squareSize, squareSize);
} else {
tmpCanvas.getGraphicsContext2D().clearRect(0, 0, squareSize, squareSize);
}
return tmpCanvas;
}
/**
* Horizontal scaling factor (1.0 means no scaling)
*
* Most fonts are relatively tall and narrow, and need horizontal scaling to fit
* a square shape.
*
* @return the xScale
*/
public double getxScale() {
return xScale;
}
/**
* Horizontal positioning of letters.
*
* Each letter should be approximately centered within its available
* square-shaped space.
*
* @return the xTranslate
*/
public double getxTranslate() {
return xTranslate;
}
/**
* Vertical scaling factor (1.0 means no scaling)
*
* @return the yScale
*/
public double getyScale() {
return yScale;
}
/**
* /** Vertical positioning of letters.
*
* Each letter should be positioned on the baseline so that ascenders and
* descenders fall within its available square-shaped space.
*
* @return the yTranslate
*/
public double getyTranslate() {
return yTranslate;
}
private double setContextFromChar(GraphicsContext ctx, char c) {
switch (c) {
case '0':
return -2 * thin;
case '2':
ctx.setLineDashes(new double[] { 14.75, 2.5 });
break;
case '3':
ctx.setLineDashes(new double[] { 9, 2.5 });
break;
case '4':
ctx.setLineDashes(new double[] { 6.125, 2.5 });
break;
case '.':
return 0.0;
case '-':
case 's':
ctx.setLineWidth(thin);
return thin;
case '*':
ctx.setLineWidth(thick);
return thick;
case '=':
case 'N':
case 'M':
case 'z':
ctx.setLineWidth(thin);
return thin;
}
return 0.0;
}
/**
* Set up a graphics context for drawing with this font.
*
* Caller should call {@link GraphicsContext#save()} first, and then
* {@link GraphicsContext#restore()} afterwards, to clean up adjustments to the
* transformation matrix (i.e., translation, scaling).
*
* The GraphicsContext should be translated to the coordinates where the text
* should appear <em>before</em> calling this method, since this method will
* modify the coordinate system.
*
* @param ctx
* A GraphicsContext
*/
public void setGraphicsContext(GraphicsContext ctx) {
ctx.setFont(getFont());
ctx.translate(xTranslate, yTranslate);
ctx.scale(xScale, yScale);
}
/**
* Set up a graphics context for drawing with this font.
*
* Caller should call {@link GraphicsContext#save()} first, and then
* {@link GraphicsContext#restore()} afterwards, to clean up adjustments to the
* transformation matrix (i.e., translation, scaling).
*
* The GraphicsContext should be translated to the coordinates where the text
* should appear <em>before</em> calling this method, since this method will
* modify the coordinate system.
*
* @param ctx
* A GraphicsContext
* @param xScaleFactor
* Additional horizontal scaling, normally 0.5 (for half-width
* characters)
*/
public void setGraphicsContext(GraphicsContext ctx, double xScaleFactor) {
ctx.setFont(getFont());
ctx.translate(xTranslate * xScaleFactor, yTranslate);
ctx.scale(xScale * xScaleFactor, yScale);
}
private void strokeFullLine(GraphicsContext ctx, double xScaleFactor, double xDir, double yDir, char c) {
double x = squareSize * xScaleFactor / 2, y = -squareSize / 2;
if (c != '.') {
// System.out.printf("(%4.1f %4.1f %4.1f %4.1f) w=%1.4f%n", x * yDir, y * xDir,
// x * yDir + xDir * squareSize,
// y * xDir + yDir * squareSize, ctx.getLineWidth());
if (c == '=') {
ctx.setLineWidth(2.0);
ctx.strokeLine((x - 2) * yDir, (y - 2) * xDir, (x - 2) * yDir + xDir * squareSize * xScaleFactor,
(y - 2) * xDir + yDir * -squareSize);
ctx.strokeLine((x + 2) * yDir, (y + 2) * xDir, (x + 2) * yDir + xDir * squareSize * xScaleFactor,
(y + 2) * xDir + yDir * -squareSize);
} else {
ctx.strokeLine(x * yDir, y * xDir, x * yDir + xDir * squareSize * xScaleFactor,
y * xDir + yDir * -squareSize);
}
}
}
private void strokeHalfLine(GraphicsContext ctx, double xScaleFactor, double xDir, double yDir, char c,
double otherWidth) {
if (c != '.') {
double factor = 1.0, dblFactor = 0.0;
if (c == 's' || c == 'z')
factor = -1;
if (c == 'N')
dblFactor = -2;
if (c == 'M')
dblFactor = 2;
double x = squareSize * xScaleFactor / 2, y = -squareSize / 2;
x -= xDir * factor * (otherWidth / 2);
y -= yDir * factor * (otherWidth / 2);
if (c == '=' || c == 'z' || c == 'N' || c == 'M') {
ctx.setLineWidth(2.0);
double x0 = x - 2 * yDir;
double y0 = y - 2 * xDir;
x0 += dblFactor * xDir * (otherWidth / 2);
y0 += dblFactor * yDir * (otherWidth / 2);
ctx.strokeLine(x0, y0, x0 + xDir * squareSize * xScaleFactor, y0 + yDir * squareSize);
double x1 = x + 2 * yDir;
double y1 = y + 2 * xDir;
x1 -= dblFactor * xDir * (otherWidth / 2);
y1 -= dblFactor * yDir * (otherWidth / 2);
ctx.strokeLine(x1, y1, x1 + xDir * squareSize * xScaleFactor, y1 + yDir * squareSize);
} else {
ctx.strokeLine(x, y, x + xDir * squareSize * xScaleFactor, y + yDir * squareSize);
}
}
}
/**
* Draw text at the given position.
*
* For most cases, the simpler {@link #drawText(GraphicsContext, String)} or
* {@link #drawText(GraphicsContext, String, double)} will be easier to use.
*
* If <code>clip</code> is true, the graphics context's current path will be
* overwritten.
*
* @param ctx
* A GraphicsContext
* @param x
* X-position of the lower left corner of the text
* @param y
* Y-position of the lower left corner of the text
* @param text
* The text to be printed
* @param xScaleFactor
* Horizontal scaling factor, normally 1.0 (full width) or 0.5 (half
* width)
* @param clear
* True if the area should be cleared (to transparency) before
* drawing; normally true.
* @param clip
* True if the text drawing should be clipped to fit the expected
* printing area; normally true.
* @param fill
* True if the letter shapes should be filled; normally true.
* @param stroke
* True if the letter shapes should be stroked (outlined); normally
* false.
*/
public void textAt(GraphicsContext ctx, double x, double y, String text, double xScaleFactor, boolean clear,
boolean clip, Paint fill, Paint stroke, int mode, Paint bg) {
if ((mode & ATTR_BRIGHT) != 0) {
fill = fill instanceof Color ? ((Color) fill).deriveColor(0.0, 1.0, 3.0, 1.0) : fill;
stroke = stroke instanceof Color ? ((Color) stroke).deriveColor(0.0, 1.0, 3.0, 1.0) : stroke;
}
if ((mode & ATTR_FAINT) != 0) {
fill = fill instanceof Color ? ((Color) fill).deriveColor(0.0, 1.0, 1.0, .5) : fill;
stroke = stroke instanceof Color ? ((Color) stroke).deriveColor(0.0, 1.0, 1.0, .5) : stroke;
}
GraphicsContext target = ctx;
int width = text.codePointCount(0, text.length());
ctx.save(); // save 1
ctx.setFill(fill);
ctx.setStroke(stroke);
ctx.translate(x, y);
if (clear && (mode & ATTR_INVERSE) == 0 && (mode & ATTR_OVERSTRIKE) == 0) {
ctx.clearRect(0 + 0.5, -squareSize + 0.5, squareSize * width * xScaleFactor - 1, squareSize - 1);
}
if (bg != null && bg != Color.TRANSPARENT) {
ctx.save(); // save 2
ctx.setFill(bg);
ctx.fillRect(0, -squareSize, squareSize * width * xScaleFactor, squareSize);
ctx.restore(); // restore 2
}
boolean drawIndirect = clip || (mode & (ATTR_INVERSE | ATTR_CLIP)) != 0;
Canvas tmpCanvas = getTmpCanvas();
if (drawIndirect) {
target = tmpCanvas.getGraphicsContext2D();
target.save(); // save 2 if drawIndirect
target.translate(0, squareSize);
target.setFill((mode & ATTR_INVERSE) != 0 ? Color.BLACK : fill);
target.setStroke((mode & ATTR_INVERSE) != 0 ? Color.BLACK : stroke);
}
if (text.length() > 0 && text.charAt(0) >= 0x2500 && text.charAt(0) <= 0x259f
&& (mode & ATTR_NO_FAKE_CHARS) == 0) {
target.save(); // save 3
target.setStroke(target.getFill());
fakeBlockElement(target, text, xScaleFactor);
target.restore(); // restore 3
} else {
target.save(); // save 3
if ((mode & ATTR_ITALIC) != 0) {
target.translate(-0.2, 0);
target.transform(new Affine(Affine.shear(-0.2, 0)));
}
setGraphicsContext(target, xScaleFactor);
if (fill != null)
target.fillText(text, 0.0, 0.0);
if ((mode & ATTR_BOLD) != 0) {
// System.err.println("stroke2");
target.save(); // save 4
target.setLineWidth(thin);
target.setStroke(target.getFill());
target.strokeText(text, 0.0, 0.0);
target.restore(); // restore 4
}
if (stroke != null || (mode & ATTR_OUTLINE) != 0) {
// System.err.println("stroke2");
target.setLineWidth(((mode & ATTR_BOLD) != 0) ? thin : thin / 2);
target.strokeText(text, 0.0, 0.0);
}
target.restore(); // restore 3
}
if ((mode & ATTR_UNDERLINE) != 0) {
target.fillRect(0, yTranslate - 2, width * squareSize * xScaleFactor, thin);
}
if ((mode & ATTR_OVERLINE) != 0) {
target.fillRect(0, -squareSize + 2, width * squareSize * xScaleFactor, thin);
}
if ((mode & ATTR_LINE_THROUGH) != 0) {
target.fillRect(0, -squareSize / 2 + 2, width * squareSize * xScaleFactor, thin);
}
if (drawIndirect) {
target.restore(); // restore 2 if drawIndirect
tmpCanvas.snapshot(snapshotParameters, img);
if ((mode & ATTR_INVERSE) != 0) {
fillInverse(ctx, img, 0, -squareSize);
} else
ctx.drawImage(img, 0, -squareSize);
}
ctx.restore(); // restore 1
}
}

View File

@ -0,0 +1,235 @@
package inf101.v18.gfx.textmode;
import inf101.v18.gfx.Screen;
import inf101.v18.gfx.textmode.Printer;
import javafx.application.Application;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class TextFontAdjuster extends Application {
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;
public static TextFontAdjuster getInstance() {
return demo;
}
public static void main(String[] args) {
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 Screen screen;
private boolean paused;
private Printer printer;
private boolean grid = true;
private double adjustAmount = 0.1;
private double adjustX(KeyCode code) {
switch (code) {
case LEFT:
return -1 * adjustAmount;
case RIGHT:
return 1 * adjustAmount;
default:
return 0;
}
}
private double adjustY(KeyCode code) {
switch (code) {
case UP:
return 1 * adjustAmount;
case DOWN:
return -1 * adjustAmount;
default:
return 0;
}
}
private void drawBackgroundGrid() {
if (grid) {
printer.drawCharCells();
/*
* painter.turnTo(0); for (int y = 0; y < printer.getPageHeight(); y++) {
* painter.jumpTo(0, y * printer.getCharHeight()); for (int x = 0; x <
* printer.getLineWidth(); x++) { painter.setInk( (x + y) % 2 == 0 ?
* Color.CORNFLOWERBLUE : Color.CORNFLOWERBLUE.brighter().brighter());
* painter.fillRectangle(printer.getCharWidth(), printer.getCharHeight());
* painter.jump(printer.getCharWidth()); } }
*/
} else {
screen.clearBackground();
}
}
private void printHelp() {
printer.moveTo(1, 1);
printer.setAutoScroll(false);
printer.println(" " + Printer.center("TextFontAdjuster", 36) + " ");
printer.println(" ");
printer.println(" ");
printer.println(" ");
printer.println("________________________________________");
printer.println("Adjust letter parameters: ");
printer.println(" Font size: CTRL +, CTRL - ");
printer.println(" Position: LEFT, RIGHT, UP, DOWN ");
printer.println(" Scaling: CTRL-(LEFT, RIGHT, UP, DOWN)");
printer.println("Commands / options (with CTRL key): ");
printer.println(" Hires (R) Grid (G) Fullscreen (F) ");
printer.println(" Help (H) Quit (Q) ");
printer.println("Write text with any other key. ");
printer.println("_-*-_-*-_-*-_-*-_-*-_-*-_-*-_-*-_-*-_-*-");
printer.println(" ");
printer.println("Sample text: ");
printer.println("the quick brown fox jumps over the lazy ");
printer.println(" dog, THE QUICK BROWN FOX JUMPS OVER THE");
printer.println("LAZY DOG den vågale røde reven værer den");
printer.println("sinte hunden DEN VÅGALE RØDE REVEN VÆRER");
printer.println("DEN SINTE HUNDEN !\"#%&/()?,._-@£${[]}?|^");
// printer.print(" ");
printer.moveTo(1, 15);
printer.setAutoScroll(true);
}
private void printInfo() {
printer.moveTo(1, 3);
printer.println(String.format("Font: %s at %1.1fpt ", textFont.getFont().getName(),
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());
printer.moveTo(1, 15);
}
private void setup() {
drawBackgroundGrid();
printHelp();
printInfo();
}
@Override
public void start(Stage stage) {
demo = this;
screen = Screen.startPaintScene(stage);
printer = screen.createPrinter();
printer.setInk(Color.BLACK);
printer.setFont(textFont);
screen.setKeyOverride((KeyEvent event) -> {
KeyCode code = event.getCode();
// System.out.println(event);
if (event.isControlDown() || event.isShortcutDown()) {
if (code == KeyCode.Q) {
System.exit(0);
} else if (code == KeyCode.P) {
paused = !paused;
return true;
} else if (code == KeyCode.R) {
printer.cycleMode(true);
drawBackgroundGrid();
return true;
} else if (code == KeyCode.S) {
if (event.isAltDown())
screen.fitScaling();
else
screen.zoomCycle();
drawBackgroundGrid();
return true;
} else if (code == KeyCode.A) {
screen.cycleAspect();
return true;
} else if (code == KeyCode.G) {
grid = !grid;
drawBackgroundGrid();
return true;
} else if (code == KeyCode.H) {
printHelp();
printInfo();
return true;
} else if (code == KeyCode.F) {
stage.setFullScreen(!stage.isFullScreen());
return true;
} else if (code == KeyCode.M) {
printer.print("\r");
return true;
} else if (code == KeyCode.L) {
printer.redrawTextPage();
return true;
} else if (code == KeyCode.DIGIT1) {
DemoPages.printBoxDrawing(printer);
return true;
} else if (code == KeyCode.DIGIT2) {
DemoPages.printZX(printer);
return true;
} else if (code == KeyCode.DIGIT3) {
DemoPages.printBlockPlotting(printer);
return true;
} else if (code == KeyCode.DIGIT4) {
DemoPages.printVideoAttributes(printer);
return true;
} else if (code == KeyCode.DIGIT5) {
DemoPages.printAnsiArt(printer);
return true;
} else if (code == KeyCode.PLUS) {
textFont = textFont.adjust(adjustAmount, 0.0, 0.0, 0.0, 0.0);
printer.setFont(textFont);
printer.redrawTextPage();
printInfo();
return true;
} else if (code == KeyCode.MINUS) {
textFont = textFont.adjust(-adjustAmount, 0.0, 0.0, 0.0, 0.0);
printer.setFont(textFont);
printer.redrawTextPage();
printInfo();
return true;
} else if (code == KeyCode.LEFT || code == KeyCode.RIGHT || code == KeyCode.UP
|| code == KeyCode.DOWN) {
textFont = textFont.adjust(0.0, 0.0, 0.0, adjustX(code), adjustY(code));
printer.setFont(textFont);
printer.redrawTextPage();
printInfo();
return true;
}
} else if (code == KeyCode.LEFT || code == KeyCode.RIGHT || code == KeyCode.UP || code == KeyCode.DOWN) {
textFont = textFont.adjust(0.0, adjustX(code), adjustY(code), 0.0, 0.0);
printer.setFont(textFont);
printer.redrawTextPage();
printInfo();
return true;
} else if (code == KeyCode.ENTER) {
printer.print("\n");
return true;
}
return false;
});
screen.setKeyTypedHandler((KeyEvent event) -> {
if (event.getCharacter() != KeyEvent.CHAR_UNDEFINED) {
printer.print(event.getCharacter());
return true;
}
return false;
});
setup();
stage.show();
}
}

View File

@ -0,0 +1,158 @@
package inf101.v18.gfx.textmode;
import inf101.v18.gfx.Screen;
public enum TextMode {
/** Low resolution, wide screen (20:11, fits 16:9) text mode 40x22 */
MODE_40X22(Constants.H40, Constants.V22, Screen.ASPECT_WIDE),
/** Low resolution, 16:10 aspect text mode 40x25 */
MODE_40X25(Constants.H40, Constants.V25, Screen.ASPECT_MEDIUM),
/** Low resolution, 4:3 aspect text mode 40x30 */
MODE_40X30(Constants.H40, Constants.V30, Screen.ASPECT_CLASSIC),
/** High resolution, wide screen (20:11, fits 16:9) text mode 80x22 */
MODE_80X22(Constants.H80, Constants.V22, Screen.ASPECT_WIDE),
/** High resolution, 16:10 aspect text mode 80x25 */
MODE_80X25(Constants.H80, Constants.V25, Screen.ASPECT_MEDIUM),
/** High resolution, 4:3 aspect text mode 80x30 */
MODE_80X30(Constants.H80, Constants.V30, Screen.ASPECT_CLASSIC);
protected static class Constants {
protected static final int H40 = 0, H80 = 1;
protected static final int[] HREZ = { 40, 80 };
protected static final int V22 = 0, V25 = 1, V30 = 2;
protected static final int[] VREZ = { 22, 25, 30 };
private static TextMode[] MODES = null;
// work around initialization order for statics and enums
protected static TextMode getMode(int i) {
if (MODES == null)
MODES = new TextMode[] { MODE_40X22, MODE_40X25, MODE_40X30, MODE_80X22, MODE_80X25, MODE_80X30 };
return MODES[(i + MODES.length) % MODES.length];
}
}
/**
* Size of the square-shaped "box" bounds of character cells.
*
* For "high" resolution, characters will be squeezed horizontally to fit half
* the width.
*/
public static final double CHAR_BOX_SIZE = 32;
/**
* Maximum length of a line in any resolution mode
*/
public static final int LINE_WIDTH_MAX = Constants.HREZ[Constants.HREZ.length - 1];
/**
* Maximum height of a page in any resolution mode
*/
public static final int PAGE_HEIGHT_MAX = Constants.VREZ[Constants.VREZ.length - 1];
private int aspect;
private int w;
private int h;
private int hIndex;
private int vIndex;
private TextMode(int w, int h, int aspect) {
this.hIndex = w;
this.vIndex = h;
this.aspect = aspect;
this.w = Constants.HREZ[w];
this.h = Constants.VREZ[h];
}
private TextMode findMode(int hIndex, int vIndex) {
hIndex = (hIndex + Constants.HREZ.length) % Constants.HREZ.length;
vIndex = (vIndex + Constants.VREZ.length) % Constants.VREZ.length;
return Constants.getMode(vIndex + hIndex * Constants.VREZ.length);
}
/**
* Get aspect ration descriptor for use with {@link Screen#setAspect()}
*
* @return One of {@link Screen#ASPECT_WIDE}, {@link Screen#ASPECT_MEDIUM} or
* {@link Screen#ASPECT_CLASSIC}
*/
public int getAspect() {
return aspect;
}
public double getCharBoxSize() {
return CHAR_BOX_SIZE;
}
public double getCharHeight() {
return CHAR_BOX_SIZE;
}
public double getCharWidth() {
return w == 80 ? CHAR_BOX_SIZE / 2 : CHAR_BOX_SIZE;
}
public int getLineWidth() {
return w;
}
public int getPageHeight() {
return h;
}
/**
* Cycle through horizontal modes
*
* @return Next available horizontal mode (vertical resolution unchanged)
*/
public TextMode nextHorizMode() {
return findMode((hIndex + 1) % Constants.HREZ.length, vIndex);
}
/**
* Cycle through modes
*
* @return Next available mode
*/
public TextMode nextMode() {
int m = vIndex + hIndex * Constants.VREZ.length;
return Constants.getMode(m + 1);
}
/**
* Cycle through vertical modes
*
* @return Next available vertical mode (horizontal resolution unchanged)
*/
public TextMode nextVertMode() {
return findMode((hIndex + 1) % Constants.HREZ.length, vIndex);
}
/**
* Cycle through horizontal modes
*
* @return Previous available horizontal mode (vertical resolution unchanged)
*/
public TextMode prevHorizMode() {
return findMode((hIndex - 1) % Constants.HREZ.length, vIndex);
}
/**
* Cycle through modes
*
* @return Previous available mode
*/
public TextMode prevMode() {
int m = vIndex + hIndex * Constants.VREZ.length;
return Constants.getMode(m);
}
/**
* Cycle through vertical modes
*
* @return Previous available vertical mode (horizontal resolution unchanged)
*/
public TextMode prevVertMode() {
return findMode((hIndex - 1) % Constants.HREZ.length, vIndex);
}
}

View File

@ -0,0 +1,95 @@
      leahciM  
  ▓████ ▄▀   ▓█
███ ▄▄ ▀▀▀▀██▐
 ████▀ ▀▀▀    
       :yb neercS  
 ▓████     ▓██
██▄  ▀ ████ ░ ▓
███▐▄ ▓███▀▄▄█▀
▀▀                 
   ▓███▄ ▄▀█ 
 ▓████▄ ▀▀ ███
▄ ░ ▓███▀▄ ▓███
▀ ▄▄██▀▀▀       
         ▓███  
▄   ▓███▄▄▀▀▀▀
▄▄  ░░█▌ ▓███
▀▄ ▓▓██▀  ▄█▀
██▀▀ █     ▀▀▀ ▀▀▀
▓███     ▓███▄
 ▀▀▄▄▄   ▄ ░  
██▄▀▄█ ▄▄ ▓
██▀  ▄▄▄███▀   
  ▄▀▀▀▀▀███ ▐ 
 ▐▄ ▓██▄  █▄
 ▀▀▀▀░░▀█▀▀▀██
█   ▄ ▓██     ▄▄
▄▄▄      ▄▄▄▄▄▄
▄▌▌ ▒▓ ▐ ▄▄▄
▄   ▄▄▄  ▄█████▀▀███
███▄▀▀▀▀▀▄▄█   
                     
  ▐▌ ▒▓▓▐ ▐██▀
▀▀▀ ▀▀▀▀▀▀▄███████
▓▓▓▀██████████▀▀▀  
                     
 ▌ ▐ ▒▒▓▄ █████
█▀▀▀███▓▓██▀▀▄██▓
▓▓▀▀▓▓▓▓▓██████▄▄▀
▀                   
  ▌ ▌  ▓▓▌ ██▀█
██████████▓▓▓▓▓▄▄▀▀
▓▓▓▓████▄▄▄▄▄██
 ▀                 
     ▐  ▄▓▌ ▓█▄▄
▄██████████▄▀█▄▄▄███▄▄▄
▀▀█▀▀ ▄██ ▀▀    
               ▌    ▄
▄▄░▓█▀██▀▀▀▄▄▄▄▄▀▀
▓▓████▀▀▀▀▓▓▓▓▓███
▀ ▄▄█▀▀▀▀▀▀▀   
                 ▀▄▄▄
▀▀████████▓▓▓▓▓█▓▓▓▓
▓▓███▀████▓▓▓▓███
▀▀ ▄▄▄▄▄▄▄▄█   
                 ▄███
███████▓▓▓▓▓███▀█▓▓▓
▓▓▓███▀▄▄▄▄███████
█▀                   
          ▄ ▀▀▄███
█████████▄▄▐▌▀█▄▀▀▀
▀▀▀▄▄█▀▐  ▄▀▀▄▄▄███
▀▀                
            ██▄▄▄
▄▄▄▄▄▄▄▄▀▀▀▀▀▀█▄
 ▀▀▀▄██▀▀▀▀▀  ██
▄▄▄█           
               ▀ ██
           ▀▀▄▄▄▀▀
▀█▓▐▐█▓▀▀▀▀▄▄▄▀▀ 
█▀  ▄▄▄          
                 ██
▄          ▀▄▀█▄
███▓▓█▌▌▐█▓█████
█▀▄█  █           
                    
 ███           ▄▀
▄▄██████▐█▒█▐████
███▄▀▄  █▀      
                     
  ▄██▄           
 ▄▄█▀▄▄▄██████████
▄▄▄▀▄  ▄ █▀   
                     
    ▌█▄           
    ▄▄▄▄█▀▄▄▄██▄▄
▄▀█▄▄▄    ▄▄█  
                      
    ▌▄            
        ▄▄▄▄▄▄▄▄▄
▄▄▄         ▄▄▄     
                      
 ▄▄▄                
                     
                     

View File

@ -0,0 +1,69 @@
package inf101.v18.grid;
import java.util.Arrays;
import java.util.List;
public enum GridDirection {
EAST(0, 1, 0, 4), NORTH(90, 0, -1, 1), WEST(180, -1, 0, 8), SOUTH(270, 0, 1, 2), //
NORTHEAST(45, 1, -1, 5), NORTHWEST(135, -1, -1, 9), SOUTHWEST(225, -1, 1, 10), SOUTHEAST(315, 1, 1, 6), //
CENTER(0, 0, 0, 0);
/**
* The four cardinal directions: {@link #NORTH}, {@link #SOUTH}, {@link #EAST},
* {@link #WEST}.
*/
public static final List<GridDirection> FOUR_DIRECTIONS = Arrays.asList(EAST, NORTH, WEST, SOUTH);
/**
* The eight cardinal and intercardinal directions: {@link #NORTH},
* {@link #SOUTH}, {@link #EAST}, {@link #WEST}, {@link #NORTHWEST},
* {@link #NORTHEAST}, {@link #SOUTHWEST}, {@link #SOUTHEAST}.
*/
public static final List<GridDirection> EIGHT_DIRECTIONS = Arrays.asList(EAST, NORTHEAST, NORTH, NORTHWEST, WEST,
SOUTHWEST, SOUTH, SOUTHEAST);
/**
* The eight cardinal and intercardinal directions ({@link #EIGHT_DIRECTIONS}),
* plus {@link #CENTER}.
*/
public static final List<GridDirection> NINE_DIRECTIONS = Arrays.asList(EAST, NORTHEAST, NORTH, NORTHWEST, WEST,
SOUTHWEST, SOUTH, SOUTHEAST, CENTER);
private final double degrees;
private final int dx;
private final int dy;
private final int mask;
private GridDirection(double degrees, int dx, int dy, int mask) {
this.degrees = degrees;
this.dx = dx;
this.dy = dy;
this.mask = mask;
}
/**
* @return The angle of this direction, with 0° facing due {@link #EAST} and 90°
* being {@link #NORTH}.
*/
public double getDegrees() {
return degrees;
}
/**
* @return The change to your X-coordinate if you were to move one step in this
* direction
*/
public int getDx() {
return dx;
}
/**
* @return The change to your Y-coordinate if you were to move one step in this
* direction
*/
public int getDy() {
return dy;
}
public int getMask() {
return mask;
}
}

View File

@ -0,0 +1,161 @@
package inf101.v18.grid;
import java.util.List;
import java.util.stream.Stream;
/**
* Representation of a two-dimensional area.
* <p>
* This describes the coordinate system of the area and defines the set of valid
* locations within the area.
* <p>
* See {@link #location(int, int)} to covert (x,y)-coordinates to
* {@link ILocation}s.
* <p>
* An {@link IArea} does not <em>store</em> anything, it just defines valid
* storage locations (e.g., for an {@link IGrid}), and the relationships between
* locations (e.g., how to find neighbours of a given location).
*
* @author Anya Helene Bagge, UiB
*/
public interface IArea extends Iterable<ILocation> {
/**
* Check if a (x,y) is inside the area.
*
* @param x
* X-coordinate
* @param y
* Y-coordinate
* @return True if the (x,y) position lies within the area
*/
boolean contains(int x, int y);
/**
* Check if a position is inside the area.
*
* @param pos
* A position
* @return True if the position lies within the area
*/
boolean contains(IPosition pos);
@Override
boolean equals(Object other);
/**
* 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);
/** @return Height of the area */
int getHeight();
/**
* Returns the number of legal positions in the area
*
* @return Same as getWidth()*getHeight()
*/
int getSize();
/** @return Width of the area */
int getWidth();
@Override
int hashCode();
/**
* Return an object for iterating over all the neighbours of the given position,
* suitable for use in a new-style for-loop.
* <p>
* The iterator will yield up to eight positions (less if the given position is
* at the edge of the area, and the coordinates are not wrapped). E.g., for a
* 1x1 area, the iterator will yield nothing (if the area is not wrapped), or
* the same position two or eight times (if the area is wrapped horizontally,
* vertically or both).
*
* @param pos
* A position in the area
* @return An iterable over positions, with {@link #contains(ILocation)} being
* true for each position.
* @see #wrapsHorizontally(), {@link #wrapsVertically()}
* @throws IndexOutOfBoundsException
* if !contains(pos)
*/
Iterable<ILocation> neighboursOf(ILocation pos);
@Override
String toString();
/**
* If the area wraps horizontally, then x will be the same as x+(k*getWidth())
* for any k i.e., it will be as if the 2D area is projected on a cylinder or
* torus in 3D-space.
* <p>
* With no wrapping, accessing positions outside (0,0)(getWidth(),getHeight())
* is illegal.
*
* @return True if the area wraps around horizontally
*/
boolean wrapsHorizontally();
/**
* If the area wraps vertically, then y will be the same as y+(k*getHeight())
* for any k i.e., it will be as if the 2D area is projected on a cylinder or
* torus in 3D-space.
* <p>
* With no wrapping, accessing positions outside (0,0)(getWidth(),getHeight())
* is illegal.
*
* @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

@ -0,0 +1,215 @@
package inf101.v18.grid;
import java.util.function.Function;
import java.util.stream.Stream;
public interface IGrid<T> extends Iterable<T> {
/**
* Make a copy
*
* @return A fresh copy of the grid, with the same elements
*/
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.
*
* 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);
/**
* 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);
/**
* Initialise the contents of all cells using an initialiser function.
*
* The function will be called with the (x,y) position of an element, and is
* expected to return the element to place at that position. For example:
*
* <pre>
* // fill all cells with the position as a string (e.g., "(2,2)")
* grid.setAll((x, y) -> String.format("(%d,%d)", x, y));
* </pre>
*
* @param initialiser
* The initialiser function
*/
void fill(Function<ILocation, T> initialiser);
/**
* Set the contents of all cells to <code>element</code>
*
* For example:
*
* <pre>
* // clear the grid
* grid.setAll(null);
* </pre>
*
* @param initialiser
*/
void fill(T element);
/**
* Iterate over all grid locations
* <p>
* See also {@link #iterator()} using the grid directly in a for-loop will
* iterate over the elements.
* <p>
* All locations obtained through the iterator are guaranteed to be valid
* according to {@link #isValid(ILocation)}.
*
* @return An iterable for iterating over all the locations in the grid
*/
Iterable<ILocation> locations();
/**
* Create a {@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
*/
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()}
*/
Stream<ILocation> locationParallelStream();
/**
* Create a {@link Stream} with all the elements in this grid.
*
* @return A stream
*/
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();
}

View File

@ -0,0 +1,107 @@
package inf101.v18.grid;
import java.util.Collection;
import java.util.List;
/**
* Represents a location within an {@link IArea}.
* <p>
* <em>Immutable:</em> Locations are immutable; i.e., a particular
* <code>ILocation</code> will always have the same x- and y-coordinates and
* cannot be changed. Methods that find neighbours (such as
* {@link #go(GridDirection)}) will return another <code>ILocation</code>.
* <p>
* <em>Area:</em> All {@link ILocation}s are tied to an {@link IArea}, thus they
* will know whether they are on the edge of the area (and not all neighbouring
* grid coordinates will be valid locations within the area) and other
* coordinate system properties that are determined by the {@link IArea} (e.g.,
* if the coordinate system wraps around like on the surface of a donut).
* <p>
* <em>Unique objects:</em> All {@link ILocation} in an {@link IArea} are
* unique. This means that <code>area.location(x,y) == area.location(x,y)</code>
* for all <em>x</em> and <em>y</em>, and that:
*
* <pre>
* // equality is reference equality for locations in the same area
* if (l1.getArea() == l2.getArea())
* assertEquals(l1.equals(l2), (l1 == l2));
* </pre>
*
*
* @author Anya Helene Bagge, UiB
*/
public interface ILocation extends IPosition {
/**
* Checks whether you can go towards direction dir without going outside the
* area bounds
*
* @param dir
* @return True if go(dir) will succeed
*/
boolean canGo(GridDirection dir);
/**
* Return the next location in direction dir.
* <p>
* This <em>does not</em> change the location object; rather it returns the
* ILocation you would end up at if you go in the given direction from this
* ILocation.
*
* @param dir
* A direction
* @return A neighbouring location
* @throws IndexOutOfBoundsException
* if !canGo(dir)
*/
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).
*
* This is will be a list of length {@link #gridDistanceTo(other)}, containing
* the cells that a chess queen would visit when moving to <code>other</code>.
* <p>
* Computes the maximum of the horizontal and the vertical distance. For
* example, to go from (0,0) to (3,5), you could go three steps SOUTHEAST and
* two steps SOUTH, so the {@link #gridDistanceTo(IPosition)} is 5.
*
* @param other
* @return Number of horizontal/vertical/diagonal (<em>queen</em>-like) steps to
* other
*/
List<ILocation> gridLineTo(ILocation other);
}

View File

@ -0,0 +1,214 @@
package inf101.v18.grid;
import java.util.List;
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 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
* An element to be added to the cell
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
default void add(int x, int y, T element) {
get(x, y).add(element);
}
/**
* 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);
}
/**
* 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);
}
/**
* Check if a cell contains an element.
*
*
* @param loc
* The (x,y) position of the grid cell
* @param element
* An element to search for.
* @return true if element is at the given location
* @throws IndexOutOfBoundsException
* if !isValid(loc)
*/
default boolean contains(ILocation loc, T element) {
return get(loc).contains(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 predicate
* Search predicate.
* @return true if an element matching the predicate was found
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
default boolean contains(int x, int y, Predicate<T> predicate) {
return contains(this.getArea().location(x, y), predicate);
}
/**
* Check if a cell contains an element.
*
*
* @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 boolean contains(ILocation loc, Predicate<T> predicate) {
for (T t : get(loc)) {
if (predicate.test(t))
return true;
}
return false;
}
/**
* 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
* @param element
* An element to be removed from the cell
* @return Number of elements removed
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
default int remove(int x, int y, T element) {
return get(x, y).remove(element) ? 1 : 0;
}
/**
* Remove an element from the cell at the given location.
*
* @param loc
* The location of the grid cell
* @param element
* An element to be removed from the cell.
* @return Number of elements removed
* @throws IndexOutOfBoundsException
* if !isValid(loc)
*/
default int remove(ILocation loc, T element) {
return get(loc).remove(element) ? 1 : 0;
}
/**
* 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
* @param predicate
* Predicate which should be true for elements to be removed
* @return Number of elements removed
* @throws IndexOutOfBoundsException
* if !isValid(x,y)
*/
default int remove(int x, int y, Predicate<T> predicate) {
return remove(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();
}
/**
* 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 predicate
* Search predicate.
* @return true if an element matching the predicate was found
* @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());
}
}

View File

@ -0,0 +1,80 @@
package inf101.v18.grid;
public interface IPosition {
/**
* Find the distance in grid cells to another location.
*
* This is different from {@link #stepDistanceTo(IPosition)} in that diagonal
* steps are allowed, and is the same as the number of steps a queen would take
* on a chess board.
* <p>
* Computes the maximum of the horizontal and the vertical distance. For
* example, to go from (0,0) to (3,5), you could go three steps SOUTHEAST and
* two steps SOUTH, so the {@link #gridDistanceTo(IPosition)} is 5.
*
* @param other
* @return Number of horizontal/vertical/diagonal (<em>queen</em>-like) steps to
* other
*/
int gridDistanceTo(IPosition other);
/**
* Find the number of non-diagonal steps needed to go from this location the
* other location.
*
* This is different from {@link #gridDistanceTo(IPosition)} in that only
* non-diagonal steps are allowed, and is the same as the number of steps a rook
* would take on a chess board.
* <p>
* Computes the distance to another location as the sum of the absolute
* difference between the x- and y-coordinates. For example, to go from (0,0) to
* (3,5), you would need to go three steps EAST and five steps SOUTH, so the
* {@link #stepDistanceTo(IPosition)} is 8.
*
* @param other
* @return Number of horizontal/vertical (<em>rook</em>-like) steps to other
*/
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)" */
String toString();
}

View File

@ -0,0 +1,16 @@
package inf101.v18.grid;
import java.util.ArrayList;
import java.util.List;
public class MultiGrid<T> extends MyGrid<List<T>> implements IMultiGrid<T> {
public MultiGrid(IArea area) {
super(area, (l) -> new ArrayList<T>());
}
public MultiGrid(int width, int height) {
super(width, height, (l) -> new ArrayList<T>());
}
}

View File

@ -0,0 +1,225 @@
package inf101.v18.grid;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
/** A Grid contains a set of cells in a square 2D matrix. */
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.
*
* The initialiser function will be called with the (x,y) position of an
* element, and is expected to return the element to place at that position. For
* example:
*
* <pre>
* // fill all cells with the position as a string (e.g., "(2,2)")
* new MyGrid(10, 10, ((x, y) -> String.format("(%d,%d)", x, y));
* </pre>
*
* @param width
* @param height
* @param initialiser
* The initialiser function
*/
public MyGrid(int width, int height, Function<ILocation, T> initialiser) {
this(new RectArea(width, height), initialiser);
}
/**
* Construct a grid with the given dimensions.
*
* @param width
* @param height
* @param initElement
* What the cells should initially hold (possibly null)
*/
public MyGrid(IArea area, T initElement) {
if (area == null) {
throw new IllegalArgumentException();
}
this.area = area;
this.cells = new ArrayList<T>(area.getSize());
for (int i = 0; i < area.getSize(); ++i) {
cells.add(initElement);
}
}
/**
* Construct a grid with the given dimensions.
*
* The initialiser function will be called with the (x,y) position of an
* element, and is expected to return the element to place at that position. For
* example:
*
* <pre>
* // fill all cells with the position as a string (e.g., "(2,2)")
* new MyGrid(10, 10, ((x, y) -> String.format("(%d,%d)", x, y));
* </pre>
*
* @param width
* @param height
* @param initialiser
* The initialiser function
*/
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));
}
}
@Override
public IGrid<T> copy() {
MyGrid<T> newGrid = new MyGrid<>(getWidth(), getHeight(), (l) -> get(l));
return newGrid;
}
@Override
public T get(int x, int y) {
return cells.get(area.toIndex(x, y));
}
@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);
}
}
@Override
public void fill(Function<ILocation, T> initialiser) {
if (initialiser == null)
throw new NullPointerException();
for (int i = 0; i < area.getSize(); i++) {
cells.set(i, initialiser.apply(area.fromIndex(i)));
}
}
@Override
public void fill(T element) {
for (int i = 0; i < area.getSize(); i++) {
cells.set(i, element);
}
}
@Override
public Iterable<ILocation> locations() {
return area;
}
@Override
public Stream<ILocation> locationStream() {
return area.stream();
}
@Override
public Stream<T> elementStream() {
return cells.stream();
}
@Override
public IArea getArea() {
return area;
}
@Override
public Stream<ILocation> locationParallelStream() {
return area.parallelStream();
}
@Override
public Stream<T> elementParallelStream() {
return cells.parallelStream();
}
}

View File

@ -0,0 +1,10 @@
# A new (improved?) version of the grid ADT
## Changes from the Cellular Automaton version
* If you look at `MyGrid.java`, you will notice that it no longer uses `IList<T>`/`MyList<T>` to hold the grid cells, and both `IList.java` and `MyList.java` have been removed. Instead we use standard built-in Java lists, with the `List<T>` interface (and `ArrayList<T>` as the class for new list objects).
* This saves us the work of maintaining and improving our own list, and makes it immediately compatible with useful standard Java library operations on lists.
* In general, you should always use built-in APIs (such as the Java Collections API which provides lists and similar) when available. Not only does it save time, but they're likely to be much better tested (and probably better designed) than you can do yourself, so they're less likely to have, e.g., stupid bugs that only show up in corner cases. You may also get better performance.
* You might want to do things yourself if you want to learn how stuff works; also, in some cases, there may not be a suitable API available or the API doesn't really fully fit with what we want to do. While Java has one-dimensional lists and arrays, it doesn't really have built-in classes for grids, so we're making our own.
* An alternative to IGrid/MyGrid would be to use two-dimensional arrays but these aren't *actually* two-dimensional arrays, but rather arrays-of-arrays, which makes them fit less well with the concept of a “grid” (the same goes for making a list of lists). In addition to being a bit inconvenient, they're also much less efficient in use and they won't let us do useful things like “please give me the cell to the north of this cell” or “please give me all cells neighbouring this cell”.

View File

@ -0,0 +1,346 @@
package inf101.v18.grid;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;
public class RectArea implements IArea {
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) {
this(width, height, false, false);
}
private RectArea(int width, int height, boolean horizWrap, boolean vertWrap) {
if (width < 1 || height < 1) {
throw new IllegalArgumentException("Width and height must be positive");
}
this.hWrap = horizWrap;
this.vWrap = vertWrap;
this.width = width;
this.height = height;
this.size = width * height;
List<ILocation> l = new ArrayList<>(size);
for (int y = 0, i = 0; y < height; y++) {
// set N or S bits if we're on the northern or southern edge
int edge = (y == 0 ? GridDirection.NORTH.getMask() : 0)
| (y == height - 1 ? GridDirection.SOUTH.getMask() : 0);
for (int x = 0; x < width; x++, i++) {
// set W or E bits if we're on the western or eastern edge
int e = edge | (x == 0 ? GridDirection.WEST.getMask() : 0)
| (x == width - 1 ? GridDirection.EAST.getMask() : 0);
l.add(new Location(x, y, i, e));
}
}
locs = Collections.unmodifiableList(l);
}
/**
* @param x
* X-coordinate
* @return The same x, wrapped to wrapX(x)
* @throws IndexOutOfBoundsException
* if coordinate is out of range, and wrapping is not enabled
*/
protected int checkX(int x) {
x = wrapX(x);
if (x < 0 || x >= width) {
throw new IndexOutOfBoundsException("x=" + x);
}
return x;
}
/**
* @param y
* Y-coordinate
* @return The same y, wrapped to wrapY(y)
* @throws IndexOutOfBoundsException
* if coordinate is out of range, and wrapping is not enabled
*/
protected int checkY(int y) {
y = wrapY(y);
if (y < 0 || y >= height) {
throw new IndexOutOfBoundsException("y=" + y);
}
return y;
}
@Override
public boolean contains(int x, int y) {
x = wrapX(x);
y = wrapY(y);
return x >= 0 && x < width && y >= 0 && y < height;
}
@Override
public boolean contains(IPosition pos) {
return (pos instanceof ILocation && ((ILocation) pos).getArea() == this) || contains(pos.getX(), pos.getY());
}
@Override
public int getHeight() {
return height;
}
@Override
public int toIndex(int x, int y) {
x = checkX(x);
y = checkY(y);
return y * width + x;
}
@Override
public int getSize() {
return size;
}
@Override
public int getWidth() {
return width;
}
@Override
public Iterator<ILocation> iterator() {
return locs.iterator();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("RectArea [width=").append(width).append(", height=").append(height).append(", hWrap=")
.append(hWrap).append(", vWrap=").append(vWrap).append("]");
return builder.toString();
}
@Override
public boolean wrapsHorizontally() {
return hWrap;
}
@Override
public boolean wrapsVertically() {
return vWrap;
}
protected int wrapX(int x) {
if (hWrap) {
if (x < 0) {
return getWidth() + x % getWidth();
} else {
return x % getWidth();
}
} else {
return x;
}
}
protected int wrapY(int y) {
if (hWrap) {
if (y < 0) {
return getHeight() + y % getHeight();
} else {
return y % getHeight();
}
} else {
return y;
}
}
/** 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

@ -0,0 +1,125 @@
package inf101.v18.grid.tests;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.IArea;
import inf101.v18.grid.ILocation;
import inf101.v18.util.IGenerator;
import inf101.v18.util.generators.AreaGenerator;
import inf101.v18.util.generators.LocationGenerator;
import static org.junit.Assert.*;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
public class AreaRetting {
private static final int N = 10000;
private IGenerator<IArea> areaGen = new AreaGenerator();
public void allAreaProperties() {
}
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();
assertEquals(l.getArea().contains(x, y), l.canGo(dir));
try {
assertNotNull(l.go(dir));
assertTrue(String.format("Expected true: %s.canGo(%s) for %d,%d", l, dir, x, y), l.canGo(dir));
} catch (IndexOutOfBoundsException e) {
assertFalse(String.format("Expected false: %s.canGo(%s) for %d,%d", l, dir, x, y), l.canGo(dir));
}
}
@Test
public void uniqueLocations() {
for (int i = 0; i < N / 10; i++) {
IArea area = areaGen.generate();
uniqueLocationsProperty(area);
}
}
@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);
}
}
}
@Test
public void gridLineTest() {
for (int i = 0; i < 10; i++) {
IArea area = areaGen.generate();
IGenerator<ILocation> lGen = new LocationGenerator(area);
for (int j = 0; j < N; j++) {
ILocation l1 = lGen.generate();
ILocation l2 = lGen.generate();
distanceProperty(l1, l2);
gridLineProperty(l1, l2);
}
}
}
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);
}
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));
}
}

View File

@ -0,0 +1,108 @@
package inf101.v18.grid.tests;
import inf101.v18.grid.IGrid;
import inf101.v18.grid.ILocation;
import inf101.v18.util.IGenerator;
import inf101.v18.util.generators.GridGenerator;
import inf101.v18.util.generators.LocationGenerator;
import inf101.v18.util.generators.StringGenerator;
import static org.junit.Assert.assertEquals;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
public class GridRetting {
private static final int N = 10000;
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()) {
assertEquals(val, grid.get(l));
}
}
public <T> void fillProperty2(IGrid<T> grid, Function<ILocation, T> fun) {
grid.fill(fun);
for (ILocation l : grid.locations()) {
assertEquals(fun.apply(l), grid.get(l));
}
}
@Test
public void fillTest1() {
for (int i = 0; i < N / 10; i++) {
IGrid<String> grid = gridGen.generate();
String s = strGen.generate();
fillProperty1(grid, s);
}
}
@Test
public void fillTest2() {
for (int i = 0; i < N / 10; i++) {
IGrid<String> grid = gridGen.generate();
fillProperty2(grid, (l) -> l.toString());
}
}
@Test
public void uniqueLocations() {
for (int i = 0; i < N / 10; i++) {
}
}
/** Test that get gives back the same value after set. */
@Test
public void setGetTest() {
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 l = lGen.generate();
String s = strGen.generate();
setGetProperty(grid, l, s);
}
}
}
}

View File

@ -0,0 +1,146 @@
package inf101.v18.rogue101;
import java.io.PrintWriter;
import java.io.StringWriter;
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 javafx.application.Application;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class Main extends Application {
public static final int LINE_MAP_BOTTOM = 20;
public static final int LINE_STATUS = 21;
public static final int LINE_MSG1 = 22;
public static final int LINE_MSG2 = 23;
public static final int LINE_MSG3 = 24;
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);
}
@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);
if (grid)
printer.drawCharCells();
printer.setAutoScroll(false);
screen.setKeyPressedHandler((KeyEvent event) -> {
KeyCode code = event.getCode();
if (event.isShortcutDown()) {
if (code == KeyCode.Q) {
System.exit(0);
} else if (code == KeyCode.R) {
printer.cycleMode(true);
if (grid)
printer.drawCharCells();
// game.draw();
return true;
} else if (code == KeyCode.A) {
screen.cycleAspect();
if (grid)
printer.drawCharCells();
return true;
} else if (code == KeyCode.G) {
grid = !grid;
if (grid)
printer.drawCharCells();
else
screen.clearBackground();
return true;
} else if (code == KeyCode.F) {
screen.setFullScreen(!screen.isFullScreen());
return true;
} else if (code == KeyCode.L) {
printer.redrawTextPage();
return true;
}
} else if (code == KeyCode.ENTER) {
try {
//game.doTurn();
// game.draw();
} catch (Exception e) {
printer.printAt(1, 25, "Exception: " + e.getMessage(), Color.RED);
e.printStackTrace();
}
return true;
} else {
try {
// game.keyPressed(code);
// game.draw();
} catch (Exception e) {
e.printStackTrace();
try {
StringWriter sw = new StringWriter();
PrintWriter writer = new PrintWriter(sw);
e.printStackTrace(writer);
writer.close();
String trace = sw.toString().split("\n")[0];
game.displayDebug("Exception: " + trace);
} catch (Exception e2) {
System.err.println("Also got this exception trying to display the previous one");
e2.printStackTrace();
game.displayDebug("Exception: " + e.getMessage());
}
}
return true;
}
return false;
});
/*
* screen.setKeyTypedHandler((KeyEvent event) -> { if (event.getCharacter() !=
* KeyEvent.CHAR_UNDEFINED) { printer.print(event.getCharacter()); return true;
* } return false; });
*/
setup();
// game = new Game(screen, painter, printer);
// game.draw();
primaryStage.show();
}
private void setup() {
}
public static String BUILTIN_MAP = "40 20\n" //
+ "########################################\n" //
+ "#...... ..C.R ......R.R......... ..R...#\n" //
+ "#.R@R...... ..........RC..R...... ... .#\n" //
+ "#.......... ..R......R.R..R........R...#\n" //
+ "#R. R......... R..R.........R......R.RR#\n" //
+ "#... ..R........R......R. R........R.RR#\n" //
+ "###############################....R..R#\n" //
+ "#. ...R..C. ..R.R..........C.RC....... #\n" //
+ "#..C.....R..... ........RR R..R.....R..#\n" //
+ "#...R..R.R..............R .R..R........#\n" //
+ "#.R.....R........RRR.......R.. .C....R.#\n" //
+ "#.C.. ..R. .....R.RC..C....R...R..C. .#\n" //
+ "#. R..............R R..R........C.....R#\n" //
+ "#........###############################\n" //
+ "# R.........R...C....R.....R...R.......#\n" //
+ "# R......... R..R........R......R.RR..##\n" //
+ "#. ..R........R.....R. ....C...R.RR...#\n" //
+ "#....RC..R........R......R.RC......R...#\n" //
+ "#.C.... ..... ......... .R..R....R...R.#\n" //
+ "########################################\n" //
;
}

View File

@ -0,0 +1,55 @@
package inf101.v18.rogue101.events;
import inf101.v18.rogue101.objects.IItem;
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;
private T 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
* @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) {
this.name = name;
this.source = source;
this.target = target;
this.value = value;
}
@Override
public IItem getSource() {
return source;
}
@Override
public IItem getTarget() {
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

@ -0,0 +1,38 @@
package inf101.v18.rogue101.events;
import inf101.v18.rogue101.objects.IItem;
public interface IEvent<T> {
/**
* @return Extra data stored in this event
*/
T getData();
/**
* @return The name of this event
*/
String getEventName();
/**
* The source is the item that caused the event
* <p>
* Could be null if the source is unknown or not relevant.
*
* @return The source of this event
*/
IItem getSource();
/**
* The target is the item that is affected by the event
* <p>
* Could be null if the target is unknown or not relevant.
*
* @return The target of this event, or null
*/
IItem getTarget();
/**
* @param value Extra data to store in this event
*/
void setData(T value);
}

View File

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

View File

@ -0,0 +1,80 @@
package inf101.v18.rogue101.examples;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.objects.IItem;
import javafx.scene.paint.Color;
public class Carrot implements IItem {
private int hp = getMaxHealth();
public void doTurn(IGame game) {
hp = Math.min(hp+1, getMaxHealth());
}
@Override
public int getDefence() {
return 0;
}
@Override
public int getMaxHealth() {
return 10;
}
@Override
public int getCurrentHealth() {
return hp;
}
@Override
public int getSize() {
return 2;
}
@Override
public String getSymbol() {
return "C";
}
@Override
public double getHealthStatus() {
return getCurrentHealth() / getMaxHealth();
}
@Override
public int handleDamage(IGame game, IItem source, int amount) {
hp -= amount;
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

@ -0,0 +1,54 @@
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 int getDefence() {
return 0;
}
@Override
public int getMaxHealth() {
return 1;
}
@Override
public int getCurrentHealth() {
return hp;
}
@Override
public int getSize() {
return 1;
}
@Override
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

@ -0,0 +1,104 @@
package inf101.v18.rogue101.examples;
import java.util.ArrayList;
import java.util.Collections;
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;
public class Rabbit implements INonPlayer {
private int food = 0;
private int hp = getMaxHealth();
@Override
public void doTurn(IGame game) {
if (food == 0)
hp--;
else
food--;
if (hp < 1)
return;
for (IItem item : game.getLocalItems()) {
if (item instanceof Carrot) {
System.out.println("found carrot!");
int eaten = item.handleDamage(game, this, 5);
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() + ")");
return;
}
}
}
// TODO:
List<GridDirection> possibleMoves = new ArrayList<>();
for (GridDirection dir : GridDirection.FOUR_DIRECTIONS) {
if (game.canGo(dir))
possibleMoves.add(dir);
}
if (!possibleMoves.isEmpty()) {
Collections.shuffle(possibleMoves);
game.move(possibleMoves.get(0));
}
}
@Override
public int getAttack() {
return 1000;
}
@Override
public int getDamage() {
return 1000;
}
@Override
public int getDefence() {
return 1000;
}
@Override
public int getMaxHealth() {
return 10;
}
@Override
public int getCurrentHealth() {
return hp;
}
@Override
public int getSize() {
return 4;
}
@Override
public String getSymbol() {
return hp > 0 ? "R" : "¤";
}
@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 "rabbit";
}
}

View File

@ -0,0 +1,79 @@
package inf101.v18.rogue101.game;
import java.util.Collection;
import java.util.List;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.map.IMapView;
import inf101.v18.rogue101.objects.IItem;
public interface IGame {
IMapView getMap();
ILocation move(GridDirection dir);
ILocation attack(GridDirection dir, IItem target);
ILocation rangedAttack(GridDirection dir, IItem target);
Collection<IItem> getLocalItems();
/**
* Get the current actor's location.
* <p>
* You should only call this from an IActor that is currently doing its move.
*
* @return Location of the current actor
*/
ILocation getLocation();
/**
* 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
*/
ILocation getLocation(GridDirection dir);
IItem pickUp(IItem item);
boolean drop(IItem item);
boolean canGo(GridDirection dir);
/**
* Get a list of all locations that are visible from the current location.
* <p>
* The location list is sorted so that nearby locations come earlier in the
* list. E.g., if <code>l = getVisible()<code> and <code>i < j</code>then
* <code>getLocation().gridDistanceTo(l.get(i)) < getLocation().gridDistanceTo(l.get(j))</code>
*
* @return A list of grid cells visible from the {@link #getLocation()}
*/
List<ILocation> getVisible();
int getWidth();
int getHeight();
IItem createItem(String symbol);
void addItem(IItem item);
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);
}

View File

@ -0,0 +1,20 @@
package inf101.v18.rogue101.game;
/**
* An exception to be thrown when an moving object tries to do an illegal move,
* for example to a position outside the map.
*
* @author larsjaffke
*
*/
public class IllegalMoveException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 7641529271996915740L;
public IllegalMoveException(String message) {
super(message);
}
}

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.GridDirection;
import inf101.v18.grid.IArea;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.game.IllegalMoveException;
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);
void add(ILocation loc, IItem item);
boolean has(ILocation loc, IItem target);
ILocation getLocation(int x, int y);
ILocation getLocation(IItem item);
IArea getArea();
int getWidth();
int getHeight();
}

View File

@ -0,0 +1,101 @@
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;
import inf101.v18.grid.IGrid;
import inf101.v18.grid.MyGrid;
/**
* A class to read a Boulder Dash map from a file. After the file is read, the
* map is stored as an {@link #IGrid} whose entries are characters. This can
* then be used to be passed to the constructor in {@link #BDMap}.
*
* The first line of the file should could contain two numbers. First the width
* and then the height of the map. After that follows a matrix of characters
* describing which object goes where in the map. '*' for wall, ' ' for empty,
* 'P' for the player, 'b' for a bug, 'r' for a rock, and '#' for sand. For
* example
*
* <pre>
* {@code
* 5 6
* *## p
* * rr#
* *####
* * *
* * d
* b
* }
* </pre>
*
* @author larsjaffke (original boulderdash version, 2017)
* @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.
*/
private static void fillMap(IGrid<String> symbolMap, Scanner in) {
// we need to store x and y in an object (array) rather than as variables,
// otherwise the foreach and lambda below won't work.
int[] xy = new int[2]; // xy[0] is x, xy[1] is y
xy[1] = 0;
while (in.hasNextLine()) {
xy[0] = 0;
in.nextLine().codePoints().forEach((codePoint) -> {
if (xy[0] < symbolMap.getWidth())
symbolMap.set(xy[0]++, xy[1], String.valueOf(Character.toChars(codePoint)));
});
xy[1]++;
}
}
}

View File

@ -0,0 +1,21 @@
40 20
########################################
#...... ..C.R ......R.R......... ..R...#
#.R@R...... ..........RC..R...... ... .#
#.......... ..R......R.R..R........R...#
#R. R......... R..R.........R......R.RR#
#... ..R........R......R. R........R.RR#
###############################....R..R#
#. ...R..C. ..R.R..........C.RC....... #
#..C.....R..... ........RR R..R.....R..#
#...R..R.R..............R .R..R........#
#.R.....R........RRR.......R.. .C....R.#
#.C.. ..R. .....R.RC..C....R...R..C. .#
#. R..............R R..R........C.....R#
#........###############################
# R.........R...C....R.....R...R.......#
# R......... R..R........R......R.RR..##
#. ..R........R.....R. ....C...R.RR...#
#....RC..R........R......R.RC......R...#
#.C.... ..... ......... .R..R....R...R.#
########################################

View File

@ -0,0 +1,52 @@
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 int getDefence() {
return 0;
}
@Override
public int getMaxHealth() {
return 0;
}
@Override
public int getCurrentHealth() {
return 0;
}
@Override
public int getSize() {
return 1;
}
@Override
public String getSymbol() {
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

@ -0,0 +1,7 @@
package inf101.v18.rogue101.objects;
public interface IActor extends IItem {
int getAttack();
int getDamage();
}

View File

@ -0,0 +1,178 @@
package inf101.v18.rogue101.objects;
import inf101.v18.gfx.gfxmode.ITurtle;
import inf101.v18.rogue101.events.IEvent;
import inf101.v18.rogue101.game.IGame;
/**
* An {@link IItem} is something that can be placed on the map or into a
* container of items.
* <p>
* An {@link IActor} is a special case of {@link IItem} used for things that are
* alive, such as the player or AI-controlled monsters/NPCs.
* <p>
* By default, all items have a hit point / health system (they can be damaged),
* and can be picked up. So you could potentially destroy an item or pick up a
* monster.
*
* @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());
}
/**
* Draw this item on the screen.
* <p>
* The turtle-painter will be positioned in the centre of the cell. You should
* avoid drawing outside the cell (size is indicated by the <code>w</code> and
* <code>h</code> parameters, so you can move ±w/2 and ±h/2 from the initial
* position). If you want to start in the lower left corner of the cell, you can
* start by doing <code>painter.jump(-w/2,h/2)</code> ((0,0) is in the top-left
* corner of the screen, so negative X points left and positive Y points down).
* <p>
* If this method returns <code>true</code>, the game will <em>not</em> print
* {@link #getSymbol()} in the cell.
* <p>
* All calls to <code>painter.save()</code> must be matched by a call to
* <code>painter.restore()</code>.
*
* @param painter
* A turtle-painter for drawing
* @param w
* The width of the cell
* @param h
* The height of the cell
* @return False if the letter from {@link #getSymbol()} should be drawn instead
*/
default boolean draw(ITurtle painter, double w, double h) {
return false;
}
}

View File

@ -0,0 +1,7 @@
package inf101.v18.rogue101.objects;
import inf101.v18.rogue101.game.IGame;
public interface INonPlayer extends IActor {
void doTurn(IGame game);
}

View File

@ -0,0 +1,8 @@
package inf101.v18.rogue101.objects;
import inf101.v18.rogue101.game.IGame;
import javafx.scene.input.KeyCode;
public interface IPlayer extends IActor {
void keyPressed(IGame game, KeyCode key);
}

View File

@ -0,0 +1,52 @@
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 int getDefence() {
return 10;
}
@Override
public int getMaxHealth() {
return 1000;
}
@Override
public int getCurrentHealth() {
return hp;
}
@Override
public int getSize() {
return Integer.MAX_VALUE;
}
@Override
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

@ -0,0 +1,16 @@
package inf101.v18.rogue101.tests;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class GameMapTest {
@Test
void testSortedAdd() {
// TODO
fail("Not yet implemented");
}
}

View File

@ -0,0 +1,53 @@
package inf101.v18.util;
import java.util.List;
import java.util.Random;
/**
* An interface for generators of random data values (to be used in testing).
*
* @param <T>
* The type of data generated.
*/
public interface IGenerator<T> {
/**
* Generate a random object.
*
* @return An object of type T
*/
T generate();
/**
* Generate a random object.
*
* @param r
* A random generator
* @return An object of type T
*/
T generate(Random r);
/**
* Generate a number of equal objects.
*
* @param n
* The number of objects to generate.
* @return A list of objects, with the property that for any two objects a,b in
* the collection a.equals(b).
*
*/
List<T> generateEquals(int n);
/**
* Generate a number of equal objects.
*
* @param r
* A random generator
* @param n
* The number of objects to generate.
* @return A list of objects, with the property that for any two objects a,b in
* the collection a.equals(b).
*
*/
List<T> generateEquals(Random r, int n);
}

View File

@ -0,0 +1,35 @@
package inf101.v18.util.generators;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import inf101.v18.util.IGenerator;
public abstract class AbstractGenerator<T> implements IGenerator<T> {
private static final Random commonRandom = new Random();
@Override
public T generate() {
return generate(commonRandom);
}
@Override
public List<T> generateEquals(int n) {
return generateEquals(commonRandom, n);
}
@Override
public List<T> generateEquals(Random r, int n) {
long seed = r.nextLong();
List<T> list = new ArrayList<T>();
for (int i = 0; i < n; i++) {
list.add(generate(new Random(seed)));
}
return list;
}
}

View File

@ -0,0 +1,46 @@
package inf101.v18.util.generators;
import inf101.v18.grid.IArea;
import inf101.v18.grid.RectArea;
import inf101.v18.util.IGenerator;
import java.util.Random;
public class AreaGenerator extends AbstractGenerator<IArea> {
/**
* Generator for the x-coordinate
*/
private final IGenerator<Integer> xGenerator;
/**
* Generator for the y-coordinate
*/
private final IGenerator<Integer> yGenerator;
/**
* Generate random Areas between (1,1) and (100,100)
*/
public AreaGenerator() {
this.xGenerator = new IntGenerator(1, 100);
this.yGenerator = new IntGenerator(1, 100);
}
/**
* Generate random Areas between (0,0) and (maxWidth,maxHeight)
*
* @param maxWidth
* @param maxHeight
*/
public AreaGenerator(int maxWidth, int maxHeight) {
if (maxWidth < 1 || maxHeight < 1) {
throw new IllegalArgumentException("Width and height must be 0 or greater");
}
this.xGenerator = new IntGenerator(1, maxWidth);
this.yGenerator = new IntGenerator(1, maxHeight);
}
@Override
public IArea generate(Random r) {
return new RectArea(xGenerator.generate(r), yGenerator.generate(r));
}
}

View File

@ -0,0 +1,89 @@
package inf101.v18.util.generators;
import java.util.Random;
/**
* Generator for doubles, with uniform distribution.
*
* Will never generate positive or negative infinity or NaN.
*
* @author anya
*
*/
public class DoubleGenerator extends AbstractGenerator<Double> {
private final double minValue;
private final double diff;
/**
* Make a generator for doubles between 0.0 (inclusive) and 1.0 (exclusive).
*/
public DoubleGenerator() {
this.minValue = 0;
diff = 1.0;
}
/**
* Make a generator for positive doubles from 0 (inclusive) to maxValue
* (exclusive).
*
* @param maxValue
* The max value, or 0 for the full range of positive doubles
*/
public DoubleGenerator(double maxValue) {
if (maxValue < 0.0) {
throw new IllegalArgumentException("maxValue must be positive or 0.0");
}
this.minValue = 0.0;
double mv = Double.MAX_VALUE;
if (maxValue != 0.0) {
mv = maxValue;
}
diff = mv - minValue;
}
/**
* Make a generator for numbers from minValue (inclusive) to maxValue
* (exclusive).
*
* Due to the way the numbers are constructed, the range from minValue to
* maxValue can only span half the total range of available double values. If
* the range is too large the bounds will be divided by two (you can check
* whether they range is too large by checking
* <code>Double.isInfinite(maxValue-minValue)</code>).
*
* @param minValue
* The minimum value
* @param maxValue
* The maximum value, minValue < maxValue
*/
public DoubleGenerator(double minValue, double maxValue) {
if (minValue >= maxValue) {
throw new IllegalArgumentException("minValue must be less than maxValue");
}
if (Double.isInfinite(minValue)) {
minValue = -Double.MAX_VALUE / 2.0;
}
if (Double.isInfinite(maxValue)) {
maxValue = Double.MAX_VALUE / 2.0;
}
if (Double.isInfinite(maxValue - minValue)) {
maxValue /= 2.0;
minValue /= 2.0;
}
this.minValue = minValue;
diff = maxValue - minValue;
assert !Double.isInfinite(diff);
}
@Override
public Double generate(Random rng) {
double d = rng.nextDouble();
double r = minValue + (d * diff);
return r;
}
}

View File

@ -0,0 +1,38 @@
package inf101.v18.util.generators;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
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.
*
* @requires collection must not be empty
*/
public ElementGenerator(Collection<T> elts) {
if (elts.size() == 0)
throw new IllegalArgumentException();
this.elts = new ArrayList<>(elts);
}
@Override
public T generate(Random r) {
return elts.get(r.nextInt(elts.size()));
}
}

View File

@ -0,0 +1,21 @@
package inf101.v18.util.generators;
import java.util.List;
import inf101.v18.grid.GridDirection;
public class GridDirectionGenerator extends ElementGenerator<GridDirection> {
/**
* New DirectionGenerator, will generate directions between 0° and 360°
*/
public GridDirectionGenerator() {
super(GridDirection.EIGHT_DIRECTIONS);
}
/**
* New DirectionGenerator, will generate directions between minValue and maxVaue
*/
public GridDirectionGenerator(List<GridDirection> dirs) {
super(dirs);
}
}

View File

@ -0,0 +1,80 @@
package inf101.v18.util.generators;
import inf101.v18.grid.IGrid;
import inf101.v18.grid.MyGrid;
import inf101.v18.util.IGenerator;
import java.util.Random;
public class GridGenerator<T> extends AbstractGenerator<IGrid<T>> {
/**
* Generator for the width of a random grid
*/
private final IGenerator<Integer> widthGenerator;
/**
* Generator for the height of a random grid
*/
private final IGenerator<Integer> heightGenerator;
/**
* Generator for one element of a random grid
*/
private final IGenerator<T> elementGenerator;
/**
* Generator for random 2D grids. Each dimension will be from 1 to 100.
*
* @param elementGenerator
* Generator for the elements
*/
public GridGenerator(IGenerator<T> elementGenerator) {
this.elementGenerator = elementGenerator;
this.widthGenerator = new IntGenerator(1, 100);
this.heightGenerator = new IntGenerator(1, 100);
}
/**
* Generator for random 2D grids using the supplied generators to generate width
* and height.
*
* @param elementGenerator
* Generator for the elements
* @param widthGenerator
* Should only generate positive numbers
* @param heightGenerator
* Should only generate positive numbers
*/
public GridGenerator(IGenerator<T> elementGenerator, IGenerator<Integer> widthGenerator,
IGenerator<Integer> heightGenerator) {
this.elementGenerator = elementGenerator;
this.widthGenerator = widthGenerator;
this.heightGenerator = heightGenerator;
}
/**
* Generator for random 2D grids with the given max dimensions.
*
* @param elementGenerator
* Generator for the elements
* @param maxWidth
* @param maxHeight
*/
public GridGenerator(IGenerator<T> elementGenerator, int maxWidth, int maxHeight) {
if (maxWidth < 1 || maxHeight < 1) {
throw new IllegalArgumentException("Width and height must be 1 or greater");
}
this.elementGenerator = elementGenerator;
this.widthGenerator = new IntGenerator(1, maxWidth);
this.heightGenerator = new IntGenerator(1, maxHeight);
}
@Override
public IGrid<T> generate(Random r) {
int w = widthGenerator.generate(r);
int h = heightGenerator.generate(r);
IGrid<T> grid = new MyGrid<>(w, h, (l) -> elementGenerator.generate(r));
return grid;
}
}

View File

@ -0,0 +1,88 @@
package inf101.v18.util.generators;
import java.util.Random;
public class IntGenerator extends AbstractGenerator<Integer> {
/**
* Generate a random long in the interval [0,n)
*
* @param rng
* A random generator
* @param n
* The maximum value (exclusive)
* @return A uniformly distributed random integer in the range 0 (inclusive) to
* n (exclusive)
*
* @author Adapted from JDK implementation for nextInt(n)
*/
public static long nextLong(Random rng, long n) {
if (n <= 0) {
throw new IllegalArgumentException("n must be positive");
}
long bits, val;
do {
bits = rng.nextLong() & ~Long.MIN_VALUE; // force to positive
val = bits % n;
} while (bits - val + (n - 1L) < 0L);
return val;
}
private final long minValue;
private final long diff;
/**
* Make a generator for the full range of integers.
*/
public IntGenerator() {
this.minValue = Integer.MIN_VALUE;
long maxValue = Integer.MAX_VALUE + 1L; // generate up to and including
// Integer.MAX_VALUE
diff = maxValue - minValue;
}
/**
* Make a generator for positive numbers from 0 (inclusive) to maxValue
* (exclusive).
*
* @param maxValue
* The max value, or 0 for the full range of positive integers
*/
public IntGenerator(int maxValue) {
if (maxValue < 0) {
throw new IllegalArgumentException("maxValue must be positive or 0");
}
this.minValue = 0;
long mv = Integer.MAX_VALUE + 1L; // generate up to and including
// Integer.MAX_VALUE
if (maxValue != 0) {
mv = maxValue;
}
diff = mv - minValue;
}
/**
* Make a generator for numbers from minValue (inclusive) to maxValue
* (exclusive).
*
* @param minValue
* The minimum value
* @param maxValue
* The maximum value, minValue < maxValue
*/
public IntGenerator(int minValue, int maxValue) {
if (minValue >= maxValue) {
throw new IllegalArgumentException("minValue must be less than maxValue");
}
this.minValue = minValue;
diff = ((long) maxValue) - ((long) minValue);
}
@Override
public Integer generate(Random rng) {
long r = minValue + nextLong(rng, diff);
return (int) r;
}
}

View File

@ -0,0 +1,44 @@
package inf101.v18.util.generators;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import inf101.v18.util.IGenerator;
public class ListGenerator<T> extends AbstractGenerator<List<T>> {
/**
* Generator for the length of the list
*/
private final IGenerator<Integer> lengthGenerator;
/**
* Generator for one element of a random grid
*/
private final IGenerator<T> elementGenerator;
public ListGenerator(IGenerator<T> elementGenerator) {
this.elementGenerator = elementGenerator;
this.lengthGenerator = new IntGenerator(1, 100);
}
public ListGenerator(IGenerator<T> elementGenerator, int maxLength) {
if (maxLength < 1) {
throw new IllegalArgumentException("Length must be 1 or greater");
}
this.elementGenerator = elementGenerator;
this.lengthGenerator = new IntGenerator(1, maxLength);
}
@Override
public List<T> generate(Random r) {
int l = lengthGenerator.generate(r);
List<T> result = new ArrayList<>(l);
for (int i = 0; i < l; i++) {
result.add(elementGenerator.generate(r));
}
return result;
}
}

View File

@ -0,0 +1,24 @@
package inf101.v18.util.generators;
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;
/**
* New LocationGenerator, will generate locations within the area
*/
public LocationGenerator(IArea area) {
this.area = area;
}
@Override
public ILocation generate(Random r) {
return area.location(r.nextInt(area.getWidth()), r.nextInt(area.getHeight()));
}
}

View File

@ -0,0 +1,59 @@
package inf101.v18.util.generators;
import java.util.Random;
public class StringGenerator extends AbstractGenerator<String> {
private final int minLength;
private final int maxLength;
public StringGenerator() {
this.minLength = 0;
this.maxLength = 15;
}
public StringGenerator(int maxLength) {
if (maxLength < 0) {
throw new IllegalArgumentException("maxLength must be positive or 0");
}
this.minLength = 0;
this.maxLength = maxLength;
}
public StringGenerator(int minLength, int maxLength) {
if (minLength >= maxLength) {
throw new IllegalArgumentException("minLength must be less than maxLength");
}
this.minLength = minLength;
this.maxLength = maxLength;
}
@Override
public String generate(Random r) {
int len = r.nextInt(maxLength - minLength) + minLength;
StringBuilder b = new StringBuilder();
for (int i = 0; i < len; i++) {
b.append(generateChar(r));
}
return b.toString();
}
public char generateChar(Random r) {
int kind = r.nextInt(7);
switch (kind) {
case 0:
case 1:
case 2:
return (char) ('a' + r.nextInt(26));
case 3:
case 4:
return (char) ('A' + r.nextInt(26));
case 5:
return (char) ('0' + r.nextInt(10));
case 6:
return (char) (' ' + r.nextInt(16));
}
return ' ';
}
}