This commit is contained in:
Kristian Knarvik 2020-02-14 14:48:29 +01:00
parent 8700dffaef
commit 95c7637af0
112 changed files with 2700 additions and 2262 deletions

View File

@ -1,11 +0,0 @@
<?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>

5
.gitignore vendored
View File

@ -1 +1,4 @@
/bin/
out
target
.idea
.settings

View File

@ -1,11 +0,0 @@
<component name="ArtifactManager">
<artifact type="jar" name="Four-In-A-Row">
<output-path>$PROJECT_DIR$/out/artifacts/Four_In_A_Row</output-path>
<root id="archive" name="Four-In-A-Row.jar">
<element id="directory" name="META-INF">
<element id="file-copy" path="$PROJECT_DIR$/META-INF/MANIFEST.MF" />
</element>
<element id="module-output" name="Four-In-A-Row" />
</root>
</artifact>
</component>

View File

@ -1,11 +0,0 @@
<component name="libraryTable">
<library name="junit:junit:5.0-SNAPSHOT" type="repository">
<properties maven-id="junit:junit:5.0-SNAPSHOT" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/junit/junit/5.0-SNAPSHOT/junit-5.0-SNAPSHOT.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<<<<<<< HEAD
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
=======
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK" />
>>>>>>> e31307eb015ce4a67ca5cbf51755ecb654024a62
<component name="ProjectRootManager" version="2" project-jdk-name="11.0.5" project-jdk-type="JavaSDK" />
</project>

View File

@ -2,14 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<<<<<<< HEAD
<module fileurl="file://$PROJECT_DIR$/Four-In-A-Row.iml" filepath="$PROJECT_DIR$/Four-In-A-Row.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/Four-In-A-Row.iml" filepath="$PROJECT_DIR$/.idea/Four-In-A-Row.iml" />
</modules>
</component>
</project>
=======
<module fileurl="file://$PROJECT_DIR$/inf101.v18.sem2.iml" filepath="$PROJECT_DIR$/inf101.v18.sem2.iml" />
</modules>
</component>
</project>
>>>>>>> e31307eb015ce4a67ca5cbf51755ecb654024a62

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>inf101.v18.sem2</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

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

View File

@ -1,12 +0,0 @@
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

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

View File

@ -1,3 +0,0 @@
# Authors
* *YOU*

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

View File

@ -1,10 +1,3 @@
Copyright © 2018 The author(s)
Du kan velge copyright-lisens selv, for eksempel MIT lisensen som er vist under, eller en annen open source lisens: https://en.wikipedia.org/wiki/Open-source_license se forøvrig https://en.wikipedia.org/wiki/Software_license
EXAMPLE:
# MIT License
#
# Copyright (c) 2015-2018 University of Bergen

View File

@ -1,3 +1,3 @@
Manifest-Version: 1.0
Main-Class: Main
Main-Class: inf101.v18.connectfour.Main

View File

@ -1,58 +1,3 @@
# [Semesteroppgave 2: “Fire på rad”](https://retting.ii.uib.no/inf101.v18.sem2/blob/master/SEM-2.md)
* **README**
* [Oppgavetekst](SEM-2.md)
Dette prosjektet inneholder [Semesteroppgave 2](SEM-2.md). Du kan også [lese oppgaven online](https://retting.ii.uib.no/inf101.v18.oppgaver/inf101.v18.sem2/blob/master/SEM-2.md) (kan evt. ha små oppdateringer i oppgaveteksten som ikke er med i din private kopi).
**Innleveringsfrist:**
* Hele oppgaven skal være ferdig til **fredag 27. april kl. 2359** ([AoE](https://www.timeanddate.com/worldclock/fixedtime.html?msg=4&iso=20180427T2359&p1=3399))
* [Ekstra tips til innlevering](https://retting.ii.uib.no/inf101/inf101.v18/wikis/innlevering)
(Kryss av under her, i README.md, så kan vi følge med på om du anser deg som ferdig med ting eller ikke.)
**Utsettelse:** Hvis du trenger forlenget frist er det mulig å be om det (spør gruppeleder evt. foreleser/assistenter hvis det er en spesiell situasjon). Hvis du ber om utsettelse bør du være i gang (ha gjort litt ting, og pushet) før fristen
* En dag eller to går greit uten begrunnelse.
* Eksamen er relativt tidlig i år, så vi vil helst unngå lange utsettelser.
* Om det er spesielle grunner til at du vil trenge lengre tid, så er det bare å ta kontakt, så kan vi avtale noe. Ta også kontakt om du [trenger annen tilrettelegging](http://www.uib.no/student/49241/trenger-du-tilrettelegging-av-ditt-studiel%C3%B8p).
# Fyll inn egne svar/beskrivelse/kommentarer til prosjektet under
* Levert av: *NAVN* (*BRUKERNAVN*)
* [ ] hele semesteroppgaven er ferdig og klar til retting!
* Code review:
* [ ] jeg har fått tilbakemelding underveis fra @brukernavn, ...
* [ ] jeg har gitt tilbakemelding underveis til @brukernavn, ...
* Sjekkliste:
* [ ] Kjørbart Fire på Rad-spill
* [ ] Forklart designvalg, hvordan koden er organisert, abstraksjon, og andre ting
* [ ] Tester
* [ ] Dokumentasjon (JavaDoc, kommentarer, diagrammer, README, etc.)
* [ ] Fornuftige navn på klasser, interfaces, metoder og variabler
* [ ] Fornuftige abstraksjoner og innkapsling (bruk av klasser, interface, metoder, etc.)
## Oversikt
*(oversikt over koden din og det du har gjort)*
### Bruk
* For å starte programmet kjør: `fyll.inn.her`
## Designvalg
*(hvordan du har valgt å løse oppgaven)*
### Bruk av abstraksjon
*(hvordan du har valgt objekter/klasser for å representere ting i spillet)*
### Erfaring hvilke valg viste seg å være gode / dårlige?
*(designerfaringer er det noe du ville gjort annerledes?)*
## Testing
*(hvordan du har testet ting)*
## Funksjonalitet, bugs
*(hva virker / virker ikke)*
## Evt. erfaring fra code review
*(lærte du noe av å gå gjennom din eller andres kode?)*
## Annet
*(er det noe du ville gjort annerledes?)*
# Connect Four
Originally created as a programming task in INF101.
Contains five different variations, two of which are only available if run the compiled .jar file from a UNIX commandline.

220
SEM-2.md
View File

@ -1,220 +0,0 @@
# [Semesteroppgave 2: “Fire på rad”](https://retting.ii.uib.no/inf101.v18.sem2/blob/master/SEM-2.md)
* [README](README.md) for utfylling
* **Oppgavetekst**
## Læringsmål
- Å få erfaring med å ta egne designvalg, i stedet for å bare følge en oppskrift.
- Å utarbeide programmer og tester på egenhånd.
- Å beskrive design og lage dokumentasjon.
## Spillet
I denne oppgaven skal du lage spillet [fire på rad](https://en.wikipedia.org/wiki/Connect_Four).
![](Connect_4_Board_and_Box.jpg)
Fire på Rad spilles av to spillere. Dersom du spilte det når du var liten, husker du kanskje at du starter med en tom, stående plastramme med adskilte kolonner der man kan slippe brikker ned fra toppen, og hver sine brikker av forskjellig farge (f.eks. rød og gul). Spilleren som starter slipper en brikke med sin farge ned i en valgfri kolonne på spillrammen. Neste spiller gjør det samme, og spillet fortsetter slik til en av spillerne har fått fire brikker på rad, loddrett, vannrett eller diagonalt, og dermed vinner spillet.
![](Connect_Four.gif)
Du skal lage et program for å spille Fire på Rad. Du velger selv hvordan du vil lage brukergrensesnitt, hva du vil kalle klassene dine og hvordan du vil organisere koden. Du får utlevert et tomt Java-prosjekt som du skal levere programmet i. Forøvrig står du fritt til å utvikle prosjektet som du vil, men du vil få poeng (fra 0-100) ut ifra hvor godt du har brukt det du har lært i INF101. Vi kommer til å hjelpe deg med en plan for hvordan du kan legge opp arbeidet, noen tips til ting som er lurt å tenke på, og forslag til hvordan du kan gjøre oppgaven mer spennende.
*Husk at du kan spørre om hjelp og tips underveis!* Gruppelederne hjelper deg gjerne om du lurer på noe, er usikker på om det du har gjort er lurt, eller om du blir stående fast. Det er meningen at du skal få det til vi lar deg starte fra grunnen av for at du skal få erfaring med det, og ikke fordi vi skal teste at du kan “alt”.
## Innleveringen
Sjekk ut koden fra:
https://retting.ii.uib.no/<brukernavn>/inf101.v18.sem2.git
(Husk å bytte ut `<brukernavn>` med brukernavnet ditt.) Her ligger oppgaveteksten og et tomt Java-prosjekt. Du skal utvikle programmet ditt i dette prosjektet, og pushe til repositoriet ditt slik som før. Når vi retter oppgaven din kommer vi til å se etter:
* Et kjørbart Fire på Rad-spill som vi kan spille alene (mot en AI) eller mot et annen, ekte spiller
* [README-fil](README.md) som forklarer designvalg, hvordan koden er organisert, abstraksjon, og andre ting vi bør vite om koden din.
* Tester
* Dokumentasjon (JavaDoc, kommentarer, diagrammer, README, etc.)
* At navn på klasser, interfaces, metoder og variabler er fornuftige
* Fornuftige abstraksjoner og innkapsling (bruk av klasser, interface, metoder, etc.) forklart i README
Du må gjerne (!) diskutere oppgaven med andre, men dere må skrive individuell kode. Dersom du samarbeider tett med noen så beskriv det i README-filen. Se også om [code review under](SEM-2.md#code-review-feedback-p%C3%A5-hverandres-kode)
## Programmet
Målet med oppgaven er å skrive et spillbart program for 1-2 spillere, av Fire på Rad. Krav til programmet ditt er at:
* Det er spillbart, ut ifra [reglene](SEM-2.md#reglene) til det tradisjonelle Fire på Rad.
* Det har et [brukergrensesnitt](SEM-2.md#brukergrensesnitt): programmet skal kunne brukes av 1-2 "ekte" spillere, f.eks. du og gruppelederen din. For å kunne spille må kunne gi input til programmet, f.eks. ved hjelp av `Scanner`-objekt og `System.out.println`, eller ved hjelp av taster og grafikk eller et klikkbart grafisk grensesnitt. Dette er også den delen av programmet som viser hvordan brikkene er stilt opp og om noen har vunnet enda.
* Det er spillbart for kun én spiller: det vil si at du må ha en AI-spiller. Den trenger ikke være særlig intelligent, det holder at den gjør tilfeldige gyldige trekk.
* Koden din viser tydelig, ved hjelp av objekt-orientering, klassenavn, interfaces, metodenavn og feltvariabler hvordan du har [abstrahert Fire på Rad-spillet](#spill-abstraksjon). Beskriv dette gjerne utfyllende i README-filen.
## Poeng/karakter
Hvor mange poeng du får på oppgaven kommer an på hvor god løsningen din er. Fire på Rad er et relativt enkelt spill å implementere, og det *kan* gjøres i én fil uten klasser og metoder (i INF100-stil). Dersom du løser oppgaven på denne måten vil du få tilnærma 0 poeng. Vi forventer så klart at programmet oppfører seg ca som det skal, men for å få god uttelling på poenget må du i tilegg vise at du kan bruke objekt-orientering på en god måte, og at du kan teste og dokumentere koden din. ***Det er bedre å levere en oppgave som har høy kvalitet på koden og et par bugs, enn et spill som kjører korrekt, men har rotete kode med dårlig inndeling i klasser og manglende dokumentasjon!***
Vi forventer at programmet kjører, du har klasser for fornuftige deler av spillet (se under) og disse er dokumentert og testet og fungerer sammen. Dersom det er mangler i oppgaven vil vi trekke mindre for dem dersom du kan forklare dem og hva som er problemet / hvordan du ville løst det i README-filen din. Dette gjelder også designvalgene hvis du oppdager at du har gjort dumme valg, hjelper det at du forklarer erfaringen og hvordan du ville gjort det annerledes, selv om du ikke ikke har tid til å fikse implementasjonen.
## Code Review / Feedback på hverandres kode
*Dette er frivillig, men kan være både lærerikt og hjelpe deg til å forbedre innleveringen din.*
Det er veldig nyttig å måtte [forklare](https://en.wikipedia.org/wiki/Rubber_duck_debugging) hvordan man selv har tenkt og hvordan ens egen kode virker, og det er også veldig nyttig å prøve å sette seg inn i hvordan andre har tenkt når de har programmert. Vi anbefaler derfor at du finner går sammen med noen andre studenter (f.eks. i grupper på 24 personer) og gjør litt [lightweight](http://codingsight.com/lightweight-code-review/) [code reviews](https://en.wikipedia.org/wiki/Software_peer_review) av hverandres kode:
* Det er praktisk å gå gjennom ting muntlig: sitt rundt samme datamaskin, og forklar din egen kode til de andre (eller prøv å forstå og forklare noen andres kode!) dere kan diskutere gode og dårlige løsninger, ideer til ting som kan gjøres annerledes eller forbedres osv.
* Du kan også gi tilgang til repositoriet [slik du lærte i Lab 4](https://retting.ii.uib.no/inf101.v18.oppgaver/inf101.v18.lab4/blob/master/LAB-4-I_GIT.md#11-tilgang), og så kan dere prøve lese (og eventuelt kjøre) hverandres kode, og f.eks. gi tilbakemeldinger gjennom *Issues*-systemet i GitLab.
* **Viktig:** poenget med dette er *å lære*, og *å forbedre* innleveringene for at det skal funke er det viktig å være *positiv og konstruktiv* når du gir tilbakemeldinger eller kommentarer (ellers er det ingen som tør å vise deg koden sin neste gang!)
* Det burde passe greit å gjøre dette en gang i løpet av den første uken, og så en gang til noen dager før innlevering.
* Det er et eget punkt i [README-filen](README.md) som dere kan krysse av hvis dere prøver dere på en eller annen variant av “code review” skrive gjerne også noen setninger om hva dere gjorde og hvordan det fungerte (lærte du noe? forbedret du noe?)
## Design Programmet sine deler
Videre i denne teksten beskriver vi typiske deler av programmet. Dersom du vil dele programmet ditt opp på en annen måte er helt greit: men ***forklar abstraksjonene dine i README-filen, uansett hva du velger å bruke***. Det er viktig at vi skjønner hva du har tenkt, og hvis noe er rart i koden din, kan en god forklaring redde deg fra unødig trekk. Se f.eks. forklaringene du har fått på utlevert kode tidligere for eksempler.
Hver del av programmet bør være så adskilt som mulig fra resten av koden. De kan f.eks. gjerne ligge i hver sine pakker, og prøv at de ikke kaller hverandre unødig mye. Les mer om [modulær](SEM-2.md#modulært) kode under [Ekstra](SEM-2.md#ekstra-utfordringer).
### Kjernen av programmet
Programmet trenger en kjerne, som kontrollerer programflyten. Dette vil typisk være en klasse som knytter sammen de øvrige delene og som har en løkke der hver gjennomkjøring er én spillers runde. Les de neste seksjonene og kikk gjerne på koden fra Semesteroppgave 1 før du bestemmer deg for hvordan å sette opp ditt program.
## Spill-abstraksjon
De fleste spill består av cirka de samme delene:
* Spill eller Game
* Spiller eller Player
* Brikker, Piece eller andre "ting" man bruker til å spille med
* noen spill har et Brett eller Board
Se gjerne på tidligere lab-oppgaver og forrige semesteroppgave for typisk oppsett.
I tillegg kan vi dele spillere opp i AI-spillere og en abstraksjon av den "ekte" spilleren. AI-spillere har gjerne sin egen måte å bestemme oppførsel på (slik som Rabbits i oblig 1), mens "ekte" spillere må kunne ta imot kommandoer fra en spiller i den virkelige verden.
Vi kan også skille ut Regler fra selve Spillet. Det er litt mer komplisert, men kan være veldig nyttig: da kan man enklere gjenbruke koden i resten av spillet selv om man vil bytte reglene til å f.eks. spille Fem på Rad eller Fire i Firkant. I så fall er det nyttig å tenke at vi har
* Spill, som har blant annet
* [Regler](#regler), som har blant annet
* [Win conditions](#win-conditions), som beskriver nøyaktig hva som må være oppfylt for å vinne spillet.
I følgende seksjoner beskriver vi de forskjellige delene av spillet, dersom vi bruker denne inndelingen.
### Brettet og objekter på det
"Spillet" er ofte en samle-klasse for brett og regler. Brettet kan gjerne inneholde spillbrikker, og spillbrikkene kan ha hver sin farge. Dersom du vil bruke et tekst-grensesnitt kan du representere hver farge med f.eks. en bokstav.
Dersom du ser på brettet i det vedlagt bildet, ser du at selv om brettet står på høykant og har runde hull, så er de viktigste egenskapene for spillet at det har en "Grid"-fuksjonalitet. Det gjør at vi kan tenke på brettet som en to-dimensjonal liste om vi vil, eller som en Grid, slik vi allerede har brukt i oblig 1. Du kan bruke interfaces og klasser fra de tidligere oppgavene og din egen kode - men les dokumentasjonen godt, og endre kode eller kommentarer der du ser at det trengs. Dersom du vil bruke IGrid-interfacet, er det praktisk å bruke en av de senere utgavene som er generisk, slik at du kan ha et grid av brikker.
## Brukergrensesnitt
Brukergrensesnittet er den delen av programmet som tar imot input fra "ekte" spillere (f.eks. du og gruppelederen din) og viser et output fra programmet. Typiske input er klikk-events eller streng-kommandoer; typiske output er å tegne brettet på i et grafikk-vindu eller ved hjelp av tegn og bokstaver i en terminal, og å fortelle spillerne hvem sin tur det er og om noen har vunnet.
Du trenger ikke å lage et grafisk brukergrensesnitt, det holder med tekst-interaksjon. Det viktige er at denne delen av koden din er klart skilt ut fra resten og godt dokumentert.
Hvis du vil sjekke om du har klart å skille koden for brukergrensesnittet klart fra resten (vi kaller det "modulær" kode) så kan du prøve å bytte ut brukergrensesnittet ditt med noen annens: dersom du har et fornuftig API til resten av programmet ditt bør det være relativt enkelt å koble noen andres brukergrensesnitt til resten av koden din, og på den måten endre én del av programmet uten å måtte endre kode som ikke har med input/output å gjøre. Du kan nok ikke bytte ut klassene direkte, men det bør gå an med litt jobb, og hvis dere blir enig om en lur måte å skrive interfacene på som dere har felles, kan dere bytte moduler uten å endre øvrig kode.
*Det er ikke et krav for oppgaven å bytte kode med andre studenter, men hvis du får det til uten særlig mye arbeid ligger du sannsynligvis godt an.*
### Tips til brukergrensesnitt
Du kan f.eks.
* ...bruke konsoll-I/O slik som i INF100, med `Scanner` og `System.out.println()`.
* ...kopiere grafikkbiblioteket vi har brukt i INF101, f.eks. fra [Semesteroppgave 1](https://retting.ii.uib.no/inf101.v18.oppgaver/inf101.v18.sem1/tree/master/src/inf101/v18/gfx). Grafikken kan tegnes som tekst (slik som i semesteroppgaven, og liknende til `System.out`), eller med skilpaddegrafikk eller shapes se f.eks. hvordan endene er tegnet i [Lab 6](https://retting.ii.uib.no/inf101.v18.oppgaver/inf101.v18.lab6/blob/master/src/inf101/v18/pond/Duck.java). Se på [Main-klassen](https://retting.ii.uib.no/inf101.v18.oppgaver/inf101.v18.sem1/blob/master/src/inf101/v18/rogue101/Main.java) for oppsett (kan gjøres mye enklere enn i Sem1), og for å se hvordan du kan motta tastetrykk.
* ...kopiere den klikkbare grid-GUIen fra [ekstraoppgaven om lyttere](https://retting.ii.uib.no/inf101.v18.oppgaver/inf101.v18.xtra.listeners); denne har vært brukt på tidligere INF101-obliger også.
* ...bruke [Swing](https://docs.oracle.com/javase/tutorial/uiswing/components/index.html) eller [JavaFX](https://docs.oracle.com/javase/8/javafx/get-started-tutorial/jfx-overview.htm) til å lage brukergrensesnitt (kan ta litt tid å sette seg inn i). Se f.eks. [JavaFX 8 GUI Tutorial](https://code.makery.ch/library/javafx-8-tutorial/)
## Regler
Regler for Fire På Rad:
* Spillere har hver sine brikker (f.eks. med forskjellige farger eller symboler)
* Spillere har hver sin tur til å legge ned brikker
* På sin tur skal spilleren velge en kolonne å slippe sin brikke ned i.
* (Implisitt regel: en brikke som slippes ned i en kolonne faller til den lander enten oppå en annen brikke eller på bunnen av brettet. Dette er ikke beskrevet i Fire på Rad-spillregler, men er en konsekvens av designet på brettet. Dere velger selv om dere vil vise det grafisk eller ikke, men det er viktig at ikke brikken blir liggende på toppen av kolonnen som er valgt, eller at spilleren får velge x og y-koordinater å legge brikken sin på.)
* Spillet stopper når en spiller vinner, ved å få *fire brikker ligger på rad, enten vannrett, loddrett eller diagonalt*. Dette kravet kalles ofte en [win condition](SEM-2.md#win-condition) eller "victory condition": denne betingelsen må være oppfylt for at spillet skal være vunnet.
### Spillere
En ekte spiller (f.eks. du eller gruppelederen din) trenger en intern representasjon i programmet. Dette er abstraksjon av den ekte spilleren, så tenk over hva som har betydning å ha med. La deg gjerne inspirere av Player-klassen fra oblig 1.
I tillegg til en vanlig spiller, må du kunne lage AI-spillere. Bruk gjerne et Spiller-interface som begge disse typene implementerer, og kanskje en felles superklasse dersom de ser ut til å dele oppførsel. Eventuelt kan AI-spiller arve fra Spiller. Vurder selv hvordan du vil gjøre det, og begrunn valget ditt.
### Win Conditions
Du vinner spillet ved å ha fire brikker på rad: vannrett (-), loddrett (|) eller diabonalt(/ \).
Man kan tenke seg at en annen versjon av spillet der man velger andre regler. Derfor ønsker vi å ha reglene innkapslet i en egen klasse; f.eks. ved at den har en metode som ser på et brett og avgjør om noen har vunnet. Dette gjør programmet mer modulært, og gjør at vi enklere kan gjenbruke resten av programmet til å lage spillet Fem på Rad, kun ved å bytte ut én objekt-type. Du trenger ikke lage Fem på Rad (enda), men du må lage en egen klasse for reglene slik at vi lettere kan utvide spillet dersom vi ønsker det.
## Ekstra tips
### Tips for å komme i gang
Vi har to forslag til hvordan du kan angripe oppgaven.
Enten kan du tenke over hva som er essensielle egenskaper ved Fire på Rad, og hvordan du kan representere dem abstrakt. Du kan finne ut hvilke interfaces og klasser du tror du trenger, hvilke metoder og feltvariabler, og kanskje skrive tester for disse. Deretter kan du implementere abstraksjonen ("modellen") din. Se [modellerings-delen](https://retting.ii.uib.no/inf101.v18.oppgaver/inf101.v18.sem1/blob/master/SEM-1_DEL-A.md#oversikt-modellering) av Sem1 Del A for et eksempel til hvordan du kan gjøre og beskrive dette.
Eller, så kan du skrive et fungerende monster-program i en eller to klasser, få det til å fungere, og deretter splitte det opp ([refaktorere](https://en.wikipedia.org/wiki/Code_refactoring)) til flere klasser, flere metoder, osv.
Du kan også gjøre en miks av disse to tilnærmingene.
### Tips for kjerne-programmet
Det er ofte forvirrende at vi må tenke på Spill som en abstraksjon av Fire på Rad (med brett, brikker og spillere), og på main-metoden i et program. Da kan det være nyttig å bruke to forskjellige klasser til det, feks en Main-klasse som starter programmet og initialiserer et Spill-objekt, og en Spill-klasse som er abstrakt og logisk og ikke har noen main-metode.
Dersom Main og Spill er hver sine klasser, og du ønsker å implementere runder i en løkke, kan det være vanskelig å finne ut hva som skal skje hvor. For å hjelpe deg videre her, kan det være nyttig å sette seg med penn og papir og skrive ned:
* nøyaktig hva skjer i løpet av en runde?
* hva har alle runder til felles?
* hva er annerledes under en AI-runde i forhold til en menneske-runde?
* hva skjer når spillet starter, men ikke hver runde?
Siden alle runder (hver spiller sin) likner på hverandre, og alle hele spill (fra start til slutt) likner på hverandre, er det typisk at man lager en løkke for hver, og legger dem inni hverandre:
```java
main(){
loop until program should stop {
gjør ting som har med initialisering av spill-objekter;
loop per single game {
gjør ting som trengs for å starte spillet;
loop per round {
gjør ting som trengs per runde;
}
}
}
```
Vi skriver loop og mener en eller annen form for løkke (for, while, do-while): hvordan du vil skrive det finner du ut av selv (eller spør gruppelederen din). Dette er et godt sted å bruke Iteratorer.
### Tips til bruk av INF101-konsepter
***Dette er ikke en sjekkliste du må oppfylle, og den er ikke komplett.*** Dette er en liste av INF101-konsepter som kan være nyttige og tips til hvordan å bruke dem. Du må sikkert bruke ting som ikke står på listen, og du kan gjerne la være å bruke ting fra listen dersom det ikke passer i koden din.
* **Interfaces**. Det er naturlig at du bruker interface til å definere oppførselen til klassene dine. Der du kan bør du bruke interface-typen i stedet for klasse-typen. *Det gjør programmet ditt mer modulært og enklere å bytte ut kode.*
* **Arv**. Der du ser at objekter eller klasser deler oppførsel eller kode, kan du gjerne prøve å få til *gjenbruk av kode*. Det kan du gjøre ved at den ene arver fra den andre, eller at koden legges i en felles hjelper-klasse, eller abstrakt klasse. *Unntaket er hvis det bryter med abstraksjonen, altså virker ulogisk og rart.*
* **Factory**. Dersom du trenger å lage mange objekter av en klasse, eller du trenger veldig typiske objekter med mange ganger, kan det være praktisk å legge inn en Factory. Dersom du oppretter objektene bare av og til, og det er kun et konstruktør-kall som skal til, er det ofte ikke vits å bruke fabrikker. Men hvis oppretting av objekter fører til at du må legge inn "boilerplate" kode, at du gjentar f.eks. if-setninger for fargevalg, eller liknende, er det nyttig å legge det inn i en Factory.
* **Tester**. Test koden din så godt du kan. Se tidligere oppgaver for tips til hvordan.
* **Forkrav**. Legg inn forkrav i metoder og konstruktører der det er nyttig.
* **Datainvariant**. Legg gjerne inn datainvariant (sjekk på at feltvariablene har en gyldig kombinasjon av verdier) i klassene dine der du kan. Det vil hjelpe deg med debugging og regnes som en del av dokumentasjonen.
* **Datastrukturer**. Se Grid fra tidligere oppgaver.
* **Generisk type**. Datastrukturen din bør være generisk, og kanskje andre deler av programmet.
* **Iterator**. Du kan bruke iteratorer på mange måter her: du kan iterere over spillerne i runde-løkken; du kan iterere over brikker på brettet for å sjekke om noen har vunnet (det kan være litt vanskelig å implementere sånn); du kan du kan lagre tidligere spill og iterere over dem for å produsere en total poengsum; osv.
* **Enum?** Det er ikke sikkert du trenger enums, men de kan ofte være hendige (kanskje for brikker?)
* **Klassediagram**. Tegn gjerne et diagram over koden din. Det er veldig nyttig for din egen del, og gjerne også for gruppelederne.
* ***Abstraksjon***. Se seksjonen for Spill-abstraksjon.
* ***Innkapsling*** eller encapsulation. Pass på at du bruker private modifiers der du kan, og at du gjemmer vekk så mye som mulig av den indre tilstanden til klassene (feltvariabler, nøyaktig implementasjon). Det er nyttig å bruke interfaces til dette.
## Ekstra utfordringer
Dette er ikke krav til innleveringen, men forslag til videreutvikling.
### Random Events
To mix things up a litte, the implementation can support the functionality of random events: These events are executed in each round with a certain probability and change the state of the game. Examples are the following (but it can be anything fun, be creative!):
- Throwing 'blocker' tokens into a random column that keep both players to throw their tokens there.
- Looking at each token in the board and switching its owner with a certain probability. (I.e. some of the blue tokens become red and vice versa.)
- Switching the owners of tokens in a random (small) area of the board.
- Rotating the board.
- ...
### Modulært
Dersom du skriver programmet ditt modulært, er det lett å koble dine moduler sammen med andres og på den måten f.eks. bytte ut ditt brukergrensesnitt med noen andre sitt, eller koble noen andre sin AI til ditt spill. For å få dette til må du ha god enkapsulering, fornuftig navngiving og forståelig dokumentasjon. Husk at tester er en del av dokumentasjonen.
For å få programmet modulært må det bestå av flere komponenter eller deler, der hver del implementeres separat fra kjerneprogrammet. Dette betyr ikke at dere trenger forskjellige prosjekter, men at kode-komponenter er klart adskilte (funksjonalitet for AI skal være begrenset til AI-modulen og ikke "lekke" inn i I/O-modulen. Spillets funksjonalitet og regler må ikke påvirkes av at man bytter ut modulen for I/O, osv.
Siden vi ikke har gitt dere noen interface eller API å følge, er det usannsynlig at du har nøyaktig samme APIer som andre studenter. Derfor må dere nok jobbe litt for å koble modulene deres sammen. En måte å gjøre det på, er å skrive en "koblings-klasse" som oversetter mellom interfacene deres. Den må dere gjerne skrive sammen. Dette blir altså en ny klasse, som ikke finnes i noen av programmene fra før, og som har som jobb å "oversette" mellom forskjellige APIer. Det gjør at dere slipper å endre deres egen kode.
Eventuelt kan dere bli enige om et felles interface, en *standard*, og følge denne begge to.
Dersom du samarbeider med noen for å få dette til, skriv i README-filen hvem og hvordan.
### Bedre AI
AI-spilleren kan gjøres smartere på mange måter. Du kan finne på dine egene strategier, f.eks. ved å ha en egen klasse for Strategi, og la AI-spillere spille mot hverandre. Hvis du gjør modul-delen over kan du la din AI-spiller konkurrere mot andres.
### Langton's Fire på Rad
Dersom du likte Langton's Ant fra ukesoppgaven, kan du slå sammen disse oppgavene. Da kan du la maur spille Fire på Rad mot hverandre, ved hjelp av celleautomat-regler. Dette er en litt annen form for AI, og blir nok litt vanskelig å implementere :)

View File

@ -1,70 +0,0 @@
Two dimensional array.
Render each subarray from bottom to top.
Place each piece in the start of the array.
Let user choose from a button above each row where to place a chip.
Keep count of player names and colors, maybe with some text input.
objects.Player has name and color.
Turn + Events
Width = 7, Height = 6
Check from each block of a color (maybe keep track of blocked pieces for effectiveness).
Maybe allow for custom combinations required to win.
Game
- Winning Conditions
- board.Board
- GameObjects
- Random events
- Input/Output
- Players
Players
Class with info about player <- passive
- name
- color (of token)
Interface for a player's actions <- active
- Methods for throwing token into column.
- +Info
Input/Output
- Input interface
* int getNextColumn(objects.Player p); //Retrieve user input
- Output interface
* void displayBoard(board.IBoard b);
* void displayMessage(String s);
Random Events
- Interface events.IRandomEvent
* void act(board.IBoard b);
* String announcement();
events.IRandomEvent
Abstract events.RandomEvent implements events.IRandomEvent (void flipCoin // Does something if heads)
Shuffle extends events.RandomEvent
objects.GameObject
objects.Token
Owner instanceof objects.Player
GameObjects(objects.Token, empty)
* One class
* getColor();
Winning Conditions
- Interface events.IWinCondition
* boolean hasWon(objects.Player p, board.IBoard board);
"Main" method (Game)
- Outer loop: "While the game has not been finished"
1) Determine who's turn
2) Get next input: int j = input.getNextColumn(p);
3) Execute move: p throws in column j.
4) Check if p has won.
5) Update view (output.displayBoard())
6) events.RandomEvent.act();

108
pom.xml Normal file
View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>inf101.v18.connectfour</groupId>
<artifactId>connect-four</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>Connect Four</name>
<url>https://git.knarcraft.net/EpicKnarvik97/ConnectFour</url>
<inceptionYear>2018</inceptionYear>
<licenses>
<license>
<name>GNU GENERAL PUBLIC LICENSE</name>
<url>https://fsf.org/</url>
</license>
</licenses>
<developers>
<developer>
<id>EpicKnarvik97</id>
<name>Kristian Knarvik</name>
<url>https://kristianknarvik.knarcraft.net</url>
<roles>
<role>leader</role>
<role>developer</role>
</roles>
<timezone>Europe/Oslo</timezone>
</developer>
</developers>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<!-- Build an executable JAR -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>inf101.v18.connectfour.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>true</appendAssemblyId>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>inf101.v18.connectfour.Main</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-jar-with-dependencies</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<directory>${project.basedir}/target</directory>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
<finalName>${project.artifactId}-${project.version}</finalName>
<testOutputDirectory>${project.build.directory}/test-classes</testOutputDirectory>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
</resource>
</resources>
<testResources>
<testResource>
<directory>${project.basedir}/src/test/resources</directory>
</testResource>
</testResources>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,47 +0,0 @@
import board.Board;
import events.FourInARow;
import events.IWinCondition;
import interfaces.FourInARowGUI;
import interfaces.NInARowGUI;
import interfaces.TicTacToeGUI;
import objects.Empty;
import javax.swing.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
class Main {
private static final int BOARD_WIDTH = 7;
private static final int BOARD_HEIGHT = 6;
private enum Gamemode {TICTACTOE, FOURINAROWCLASSIC, FOURINAROW}
public static void main(String[] args) {
HashMap<String, Gamemode> modeList = new HashMap<>();
modeList.put("Tic-Tac-Toe", Gamemode.TICTACTOE);
modeList.put("Four in a row classic", Gamemode.FOURINAROWCLASSIC);
modeList.put("Four in a row", Gamemode.FOURINAROW);
Object[] keys = modeList.keySet().toArray();
String selectedValue = (String)JOptionPane.showInputDialog(null, "Select a game from the list below:", "Car model...", JOptionPane.QUESTION_MESSAGE,null, keys, keys[0]);
if (selectedValue == null) {
System.exit(0);
}
List<IWinCondition> con = new ArrayList<>();
con.add(new FourInARow());
switch (modeList.get(selectedValue)) {
case TICTACTOE:
new TicTacToeGUI();
break;
case FOURINAROWCLASSIC:
new FourInARowGUI(BOARD_WIDTH, BOARD_HEIGHT, new Board(BOARD_WIDTH, BOARD_HEIGHT, new Empty(), con));
break;
case FOURINAROW:
new NInARowGUI(BOARD_WIDTH, BOARD_HEIGHT, new Board(BOARD_WIDTH, BOARD_HEIGHT, new Empty(), con));
break;
default:
System.exit(0);
}
}
}

View File

@ -1,70 +0,0 @@
Two dimensional array.
Render each subarray from bottom to top.
Place each piece in the start of the array.
Let user choose from a button above each row where to place a chip.
Keep count of player names and colors, maybe with some text input.
objects.Player has name and color.
Turn + Events
Width = 7, Height = 6
Check from each block of a color (maybe keep track of blocked pieces for effectiveness).
Maybe allow for custom combinations required to win.
Game
- Winning Conditions
- board.Board
- GameObjects
- Random events
- Input/Output
- Players
Players
Class with info about player <- passive
- name
- color (of token)
Interface for a player's actions <- active
- Methods for throwing token into column.
- +Info
Input/Output
- Input interface
* int getNextColumn(objects.Player p); //Retrieve user input
- Output interface
* void displayBoard(board.IBoard b);
* void displayMessage(String s);
Random Events
- Interface events.IRandomEvent
* void act(board.IBoard b);
* String announcement();
events.IRandomEvent
Abstract events.RandomEvent implements events.IRandomEvent (void flipCoin // Does something if heads)
Shuffle extends events.RandomEvent
objects.GameObject
objects.Token
Owner instanceof objects.Player
GameObjects(objects.Token, empty)
* One class
* getColor();
Winning Conditions
- Interface events.IWinCondition
* boolean hasWon(objects.Player p, board.IBoard board);
"Main" method (Game)
- Outer loop: "While the game has not been finished"
1) Determine who's turn
2) Get next input: int j = input.getNextColumn(p);
3) Execute move: p throws in column j.
4) Check if p has won.
5) Update view (output.displayBoard())
6) events.RandomEvent.act();

View File

@ -1,17 +0,0 @@
package events;
import board.IBoard;
import objects.Immovable;
public class Block extends RandomEvent {
@Override
public void act(IBoard b) {
int x = random.nextInt(b.getWidth());
b.drop(x, new Immovable());
}
@Override
public String announcement() {
return "An immovable object was placed.";
}
}

View File

@ -1,10 +0,0 @@
package events;
import board.IBoard;
public class FourInARow extends InARow {
@Override
public boolean hasWon(int x, int y, IBoard board) {
return hasWon(x, y, board, 3);
}
}

View File

@ -1,8 +0,0 @@
package events;
import board.IBoard;
public interface IRandomEvent {
void act(IBoard b);
String announcement();
}

View File

@ -1,10 +0,0 @@
package events;
import board.IBoard;
public class ThreeInARow extends InARow {
@Override
public boolean hasWon(int x, int y, IBoard board) {
return hasWon(x, y, board, 2);
}
}

View File

@ -1,35 +0,0 @@
package inf101.v18.sem2;
import java.util.Arrays;
import java.util.List;
public class AppInfo {
/**
* Your application name.
*/
public static final String APP_NAME = "Connect4";
/**
* The main class, for starting the application
*/
public static final Class<?> APP_MAIN_CLASS = null; // e.g., inf101.v18.sem2.Main.class
/**
* Your name.
*/
public static final String APP_DEVELOPER = "INF101-Student";
/**
* A short description.
*/
public static final String APP_DESCRIPTION = "Semesteroppgave2";
/**
* List of extra credits (e.g. for media sources)
*/
public static final List<String> APP_EXTRA_CREDITS = Arrays.asList(//
/* "Graphics by Foo Bar" */
);
/**
* Help text. Could be used for an in-game help page, perhaps.
* <p>
* Use <code>\n</code> for new lines, <code>\f<code> between pages (if multi-page).
*/
public static final String APP_HELP = "Help:\n";
}

View File

@ -1,61 +0,0 @@
package interfaces;
import board.IBoard;
import objects.GameObject;
import objects.Player;
import objects.Token;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
public abstract class GUI implements ActionListener, IGameInterface {
final int BUTTON_HEIGHT = 25;
final int BUTTON_WIDTH = 50;
final int TILE_HEIGHT = 50;
final int HEIGHT_GAP = 10;
final int WIDTH_GAP = 10;
JButton[] dropButtons;
JButton resetButton;
JTextPane[] tiles;
JLabel infoLabel;
IBoard board;
JPanel mainPanel;
JFrame frame;
Player currentPlayer;
public void displayBoard() {
for (JTextPane pane : this.tiles) {
this.mainPanel.remove(pane);
}
this.tiles = new JTextPane[this.board.getWidth() * this.board.getHeight()];
readTiles();
this.frame.validate();
}
void readTiles() {
int k = 0;
for (int i = board.getHeight() - 1; i > -1; i--) {
for (int j = 0; j < board.getWidth(); j++) {
JTextPane textPane = new JTextPane();
this.tiles[k++] = textPane;
textPane.setEditable(false);
textPane.setBackground(Color.BLUE);
GameObject obj = board.get(j,i);
textPane.setBorder(BorderFactory.createLineBorder(obj.getColor(), 50, true));
textPane.setPreferredSize(new Dimension(BUTTON_WIDTH, TILE_HEIGHT));
mainPanel.add(textPane, BorderLayout.PAGE_START);
}
}
}
boolean dropObject(int i) {
return this.board.drop(i, new Token(currentPlayer));
}
public void displayMessage(String s) {
infoLabel.setText(s);
}
}

View File

@ -1,6 +0,0 @@
package interfaces;
interface IGameInterface {
void displayMessage(String s);
void displayBoard();
}

View File

@ -1,181 +0,0 @@
package interfaces;
import board.IBoard;
import events.Block;
import events.Bomb;
import objects.GameObject;
import objects.Player;
import objects.Token;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class NInARowGUI extends GUI {
private Bomb bomb = new Bomb();
private Block block = new Block();
private JLabel eventLabel;
public NInARowGUI(int width, int height, IBoard board) {
if (width < 3 || height < 3 || height > 50 || width > 50) {
throw new IllegalArgumentException("Invalid game size.");
}
if (width != board.getWidth() || height != board.getHeight()) {
throw new IllegalArgumentException("The board does not match the gui.");
}
this.board = board;
int playerNum = 0;
while (playerNum == 0) {
try {
String num = JOptionPane.showInputDialog("Number of players: ");
if (num == null) {
System.exit(0);
}
playerNum = Integer.parseInt(num);
if (playerNum > width) {
JOptionPane.showMessageDialog(null, "Number of players can't be higher than " + width, "Error", JOptionPane.ERROR_MESSAGE);
playerNum = 0;
}
} catch (NumberFormatException e) {
JOptionPane.showMessageDialog(null, "Invalid input.", "Error", JOptionPane.ERROR_MESSAGE);
}
}
for (int i = 0; i < playerNum; i++) {
String playerName = "";
while (playerName.equals("")) {
playerName = JOptionPane.showInputDialog(String.format("Name of player %d: ", i+1));
if (playerName == null) {
System.exit(0);
}
}
Color color = null;
while (color == null) {
color = JColorChooser.showDialog(null, "Choose a color", Color.RED);
if (color == null) {
System.exit(0);
}
if (color == Color.lightGray || color == Color.blue || color == Color.black) {
JOptionPane.showMessageDialog(null, "The chosen color is reserved. Please choose another color.", "Error", JOptionPane.ERROR_MESSAGE);
color = null;
}
}
Player player = new Player(playerName, color);
if (!this.board.addPlayer(player)) {
JOptionPane.showMessageDialog(null, "Player is not unique. Please try again.", "Error", JOptionPane.ERROR_MESSAGE);
i--;
}
}
initialize(width, height);
}
private void initialize(int width, int height) {
JFrame frame = new JFrame("Four in a row");
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
int fullWidth = BUTTON_WIDTH * width + (WIDTH_GAP + 1) * width;
int fullHeight = BUTTON_HEIGHT + (TILE_HEIGHT * height) + HEIGHT_GAP * (height) + TILE_HEIGHT;
this.frame = frame;
JPanel container = new JPanel();
container.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
container.setPreferredSize(new Dimension(fullWidth, fullHeight + TILE_HEIGHT + 2 * BUTTON_HEIGHT));
frame.add(container);
JPanel panel3 = new JPanel();
this.resetButton = new JButton("Reset");
this.resetButton.addActionListener(this);
panel3.add(this.resetButton);
for (Player player : board.getPlayers()) {
JLabel playerLabel = new JLabel(player.toString());
playerLabel.setForeground(player.getColor());
panel3.add(playerLabel);
}
container.add(panel3);
JPanel panel2 = new JPanel();
this.mainPanel = panel2;
panel2.setBackground(Color.BLUE);
panel2.setLayout(new FlowLayout(FlowLayout.CENTER, HEIGHT_GAP, WIDTH_GAP));
panel2.setPreferredSize(new Dimension(fullWidth, fullHeight));
dropButtons = new JButton[width];
for (int i = 0; i < width; i++) {
JButton button = new JButton();
button.addActionListener(this);
button.setPreferredSize(new Dimension(BUTTON_WIDTH, BUTTON_HEIGHT));
dropButtons[i] = button;
panel2.add(button, BorderLayout.PAGE_START);
}
tiles = new JTextPane[width * height];
readTiles();
container.add(panel2);
infoLabel = new JLabel();
infoLabel.setPreferredSize(new Dimension(fullWidth, BUTTON_HEIGHT));
infoLabel.setBorder(new EmptyBorder(10, 10, 10, 10));
container.add(infoLabel);
eventLabel = new JLabel();
eventLabel.setPreferredSize(new Dimension(fullWidth, BUTTON_HEIGHT));
eventLabel.setBorder(new EmptyBorder(10, 10, 10, 10));
container.add(eventLabel);
frame.validate();
frame.pack();
frame.setVisible(true);
this.currentPlayer = board.nextPlayer();
displayMessage("It's " + this.currentPlayer + "'s turn.");
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == this.resetButton) {
this.board.flush();
displayBoard();
this.currentPlayer = this.board.nextPlayer();
displayMessage("It's " + this.currentPlayer + "'s turn.");
}
if (!(this.board.won() || this.board.isFull())) {
for (int i = 0; i < this.dropButtons.length; i++) {
if (e.getSource() == this.dropButtons[i]) {
if (dropObject(i)) {
displayBoard();
if (this.board.won()) {
GameObject winnerObject = this.board.get(i, this.board.getTopObject(i));
displayMessage(((Token)winnerObject).getOwner() + " won.");
} else {
triggerEvents();
if (board.isFull()) {
displayMessage("It's a draw");
} else {
this.currentPlayer = board.nextPlayer();
displayMessage("It's " + this.currentPlayer + "'s turn.");
}
}
} else {
displayMessage("That column is full.");
}
}
}
}
}
private void triggerEvents() {
if (bomb.rollDie(this.board, 2)) {
eventLabel.setText(bomb.announcement());
displayBoard();
} else if (block.rollDie(this.board, 1)) {
eventLabel.setText(block.announcement());
displayBoard();
} else {
eventLabel.setText("");
}
}
}

View File

@ -0,0 +1,87 @@
package inf101.v18.connectfour;
import inf101.v18.connectfour.interfaces.*;
import javax.swing.*;
import java.util.ArrayList;
import java.util.Scanner;
/**
* This is a "base" class, which lets the user choose a game.
*/
public class Main {
private static final int BOARD_WIDTH = 7;
private static final int BOARD_HEIGHT = 6;
public static void main(String[] args) {
ArrayList<String> gamemodes = new ArrayList<>();
gamemodes.add("Connect Four");
gamemodes.add("Connect Four classic");
gamemodes.add("Tic-Tac-Toe");
if (System.console() != null) {
gamemodes.add("Connect Four CLI");
gamemodes.add("Tic-Tac-Toe CLI");
}
JOptionPane.showMessageDialog(
null,
"This software contains several games:\n" +
"* Connect Four is a custom version with customizable number of players.\n" +
"* Connect Four classic is a version closer to that of the original analogue version. Two human players are required.\n" +
"* Tic-Tac-Toe is a game built upon the same framework as Connect Four, but behaves quite differently. Two human players are required.\n" +
"* Connect Four CLI is a CLI implementation of connect four. It supports two players or one player and one ai.\n" +
"* Tic-Tac-Toe CLI is a CLI implementation of Tic-Tac-Toe. It supports two players or one player and one ai.",
"Game-types",
JOptionPane.INFORMATION_MESSAGE);
String selectedValue = (String)JOptionPane.showInputDialog(
null,
"Select a game from the list below:",
"Game type",
JOptionPane.QUESTION_MESSAGE,
null,
gamemodes.toArray(),
gamemodes.get(0));
if (selectedValue == null) {
System.exit(0);
}
boolean playAgain = true;
switch (selectedValue) {
case "Tic-Tac-Toe":
new TicTacToeGUI();
break;
case "Connect Four classic":
new ConnectFourClassicGUI(BOARD_WIDTH, BOARD_HEIGHT);
break;
case "Connect Four":
new ConnectFourGUI(BOARD_WIDTH, BOARD_HEIGHT);
break;
case "Connect Four CLI":
while (playAgain) {
playAgain = CLILoop(new ConnectFourCLI(7, 6));
}
break;
case "Tic-Tac-Toe CLI":
while (playAgain) {
playAgain = CLILoop(new TicTacToeCLI());
}
break;
default:
System.exit(0);
}
}
private static boolean CLILoop(IGameInterface gameInterface) {
Scanner in = new Scanner(System.in);
while (!gameInterface.hasWon() && !gameInterface.hasDrawn()) {
gameInterface.displayBoard();
gameInterface.doTurn();
}
if (gameInterface.hasDrawn() && !gameInterface.hasWon()) {
gameInterface.displayMessage("It's a draw.");
}
gameInterface.displayMessage("Do you want to play again? (y/n)");
return in.nextLine().toLowerCase().contains("y");
}
}

View File

@ -1,51 +1,29 @@
package board;
package inf101.v18.connectfour.board;
import events.IWinCondition;
import objects.GameObject;
import objects.Player;
import objects.Token;
import inf101.v18.connectfour.events.winning.IWinCondition;
import inf101.v18.connectfour.objects.IGameObject;
import inf101.v18.connectfour.objects.IPlayerObject;
import inf101.v18.connectfour.player.IPlayer;
import java.util.ArrayList;
import java.util.List;
/**
* A board takes care of players, interacting with the grid and win-conditions.
*/
public class Board implements IBoard{
private final Grid<GameObject> grid;
private final List<Player> players;
private int playersTurn = 0;
private final List<IWinCondition> conditionList;
private boolean won = false;
IGrid<IGameObject> grid;
List<IPlayer> players;
int playersTurn = 0;
final List<IWinCondition> conditionList;
boolean won = false;
final IGameObject empty;
public Board(int width, int height, GameObject obj, List<IWinCondition> conditions) {
grid = new Grid<>(width, height, obj);
public Board(int width, int height, IGameObject empty, List<IWinCondition> conditions) {
grid = new Grid<>(width, height, empty);
players = new ArrayList<>();
conditionList = conditions;
}
@Override
public boolean addPlayer(Player p) {
if (!playerExists(p)) {
players.add(p);
return true;
} else {
return false;
}
}
@Override
public ArrayList<Player> getPlayers() {
return new ArrayList<>(players);
}
/**
* Gets the next player.
*
* @return A Player
*/
public Player nextPlayer() {
Player p = players.get(playersTurn);
this.playersTurn = this.playersTurn + 2 > this.players.size() ? 0 : this.playersTurn + 1;
return p;
this.empty = empty;
}
/**
@ -54,8 +32,8 @@ public class Board implements IBoard{
* @param player The new player to test
* @return True if the player is non-unique
*/
private boolean playerExists(Player player) {
for (Player p : players) {
private boolean playerExists(IPlayer player) {
for (IPlayer p : players) {
if (player.equals(p)) {
return true;
}
@ -63,24 +41,6 @@ public class Board implements IBoard{
return false;
}
@Override
public boolean isFull() {
return grid.isFull();
}
/**
* Checks if anyone has won.
*
* @param i The column of the last dropped object
*/
private void checkWin(int i) {
for (IWinCondition con : conditionList) {
if (con.hasWon(i, getTopObject(i), this)) {
won = true;
}
}
}
/**
* Checks if anyone has won.
*
@ -95,6 +55,36 @@ public class Board implements IBoard{
}
}
@Override
public boolean addPlayer(IPlayer p) {
if (!playerExists(p)) {
players.add(p);
return true;
} else {
return false;
}
}
@Override
public ArrayList<IPlayer> getPlayers() {
return new ArrayList<>(players);
}
@Override
public IPlayer nextPlayer() {
if (players.isEmpty()) {
return null;
}
IPlayer p = players.get(playersTurn);
this.playersTurn = this.playersTurn + 2 > this.players.size() ? 0 : this.playersTurn + 1;
return p;
}
@Override
public boolean isFull() {
return grid.isFull();
}
@Override
public boolean won() {
return won;
@ -107,11 +97,6 @@ public class Board implements IBoard{
won = false;
}
@Override
public int getTopObject(int column) {
return grid.getTopObject(column);
}
@Override
public int getHeight() {
return grid.getHeight();
@ -123,27 +108,28 @@ public class Board implements IBoard{
}
@Override
public void set(int x, int y, GameObject element) {
public void set(int x, int y, IGameObject element) {
grid.set(x, y, element);
if (element instanceof Token) {
if (element instanceof IPlayerObject) {
checkWin(x, y);
}
}
@Override
public boolean drop(int i, GameObject element) {
boolean dropped = grid.drop(i, element);
if (element instanceof Token) {
checkWin(i);
}
return dropped;
}
public IGameObject get(int x, int y) { return grid.get(x, y); }
@Override
public GameObject get(int x, int y) { return grid.get(x, y); }
@Override
public IGrid<GameObject> copy() {
public IGrid<IGameObject> copy() {
return grid.copy();
}
@Override
public IBoard copyAll() {
Board newBoard = new Board(getWidth(), getHeight(), empty, conditionList);
newBoard.players = new ArrayList<>(players);
newBoard.won = won;
newBoard.grid = copy();
newBoard.playersTurn = playersTurn;
return newBoard;
}
}

View File

@ -1,14 +1,30 @@
package board;
import objects.Token;
package inf101.v18.connectfour.board;
/**
* An implementation of IGrid
*
* @param <T> Can be anything
*/
public class Grid<T> implements IGrid<T> {
private final T[][] grid;
private final int width;
private final int height;
private final T empty;
/**
* Creates a new grid.
*
* @param width The width of the new grid
* @param height The height of the new grid
* @param object The default object for all cells
*/@SuppressWarnings("unchecked")
public Grid(int width, int height, T object) {
if (!(width > 0 && height > 0)) {
throw new IllegalArgumentException("Negative or zero height and/or width.");
}
if (object == null) {
throw new IllegalArgumentException("Grid can't be initialized with null");
}
T[][] list = (T[][])new Object[width][height];
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
@ -21,6 +37,7 @@ public class Grid<T> implements IGrid<T> {
this.height = height;
}
@Override
public void flush() {
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
@ -29,57 +46,36 @@ public class Grid<T> implements IGrid<T> {
}
}
@Override
public boolean isFull() {
int empty = 0;
for (T[] list : grid) {
for (T item : list) {
if (this.empty.getClass().isInstance(item)) {
empty++;
return false;
}
}
}
return empty < 1;
}
@Override
public int getTopObject(int column) {
for (int j = this.height - 1; j > -1; j--) {
if (grid[column][j] instanceof Token) {
return j;
}
}
return -1;
return true;
}
@Override
public int getHeight() {
return this.height;
return height;
}
@Override
public int getWidth() {
return this.width;
}
@Override
public boolean drop(int i, T element) {
if (i > width || i < 0) {
throw new IllegalArgumentException("Index out of bounds.");
}
for (int j = 0; j < this.height; j++) {
if (this.empty.getClass().isInstance(grid[i][j])) {
grid[i][j] = element;
return true;
}
}
return false;
return width;
}
@Override
public void set(int x, int y, T element) {
if (x > width || y > height || x < 0 || y < 0) {
if (x >= width || y >= height || x < 0 || y < 0) {
throw new IllegalArgumentException("Index out of bounds.");
}
if (element == null) {
throw new IllegalArgumentException("This grid does not accept null.");
}
grid[x][y] = element;
}
@ -94,7 +90,9 @@ public class Grid<T> implements IGrid<T> {
@Override
public IGrid<T> copy() {
Grid<T> newGrid = new Grid<>(width, height, empty);
System.arraycopy(grid, 0, newGrid.grid, 0, width);
for (int i = 0; i < width; i++) {
System.arraycopy(grid[i], 0, newGrid.grid[i], 0, height);
}
return newGrid;
}
}

View File

@ -1,25 +1,28 @@
package board;
package inf101.v18.connectfour.board;
import objects.GameObject;
import objects.Player;
import inf101.v18.connectfour.objects.IGameObject;
import inf101.v18.connectfour.player.IPlayer;
import java.util.List;
public interface IBoard extends IGrid<GameObject> {
/**
* An interface representing a game board.
*/
public interface IBoard extends IGrid<IGameObject> {
/**
* Adds a new player to the board.
*
* @param p The player to add
* @return True if the player is valid
*/
boolean addPlayer(Player p);
boolean addPlayer(IPlayer p);
/**
* Gets the next player from the board.
*
* @return A player
*/
Player nextPlayer();
IPlayer nextPlayer();
/**
* Find out if someone has won.
@ -33,18 +36,17 @@ public interface IBoard extends IGrid<GameObject> {
*/
void flush();
/**
* Gets the position of the topmost object in the board's grid.
*
* @param column The target column
* @return A position or -1 if all rows are empty
*/
int getTopObject(int column);
/**
* Gets a copy of the players list.
*
* @return A list of players
*/
List<Player> getPlayers();
List<IPlayer> getPlayers();
/**
* Makes a copy of the entire board.
*
* @return A new, but identical board
*/
IBoard copyAll();
}

View File

@ -1,5 +1,10 @@
package board;
package inf101.v18.connectfour.board;
/**
* An IGrid can store and retrieve anything of type T.
*
* @param <T>
*/
public interface IGrid<T> {
/**
@ -19,30 +24,20 @@ public interface IGrid<T> {
* 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 change the contents of.
* @param y The row of the cell to change the contents of.
* @param element The contents the cell is to have.
* @param x The column of the cell to change the contents of
* @param y The row of the cell to change the contents of
* @param element The contents the cell is to have
*/
void set(int x, int y, T element);
/**
* Drops an element into column i.
*
* @param i The target column
* @param element The element to be dropped
* @return True if the column had space for the element
*/
boolean drop(int i, T element);
/**
*
* Get the contents of the cell in the given x,y location.
*
* y must be greater than or equal to 0 and less than getHeight().
* x must be greater than or equal to 0 and less than getWidth().
*
* @param x The column of the cell to get the contents of.
* @param y The row of the cell to get contents of.
* @param x The column of the cell to get the contents of
* @param y The row of the cell to get contents of
*/
T get(int x, int y);
@ -61,10 +56,7 @@ public interface IGrid<T> {
boolean isFull();
/**
* Retrieves the position of the topmost element from a column.
*
* @param column The target column
* @return The y-position of the top element
* Sets all spaces in the grid to the element representing an empty tile.
*/
int getTopObject(int column);
void flush();
}

View File

@ -0,0 +1,55 @@
package inf101.v18.connectfour.board;
import inf101.v18.connectfour.objects.IGameObject;
/**
* An IBoard with added methods for vertical interactions.
*/
public interface IVerticalBoard extends IBoard {
/**
* Retrieves the position of the "topmost" element in a column.
*
* Will return -1 if the column is empty.
*
* @param column The target column
* @return The y-position of the top element
*/
int getTopObject(int column);
/**
* Retrieves the position of the "topmost" element of class clazz in a column.
*
* Will return -1 if the column is empty.
*
* @param column The target column
* @param clazz The target class
* @return The y-position of the top element
*/
int getTopObject(int column, Class<?> clazz);
/**
* Drops an element into column i.
*
* i must be greater than or equal to 0 and less than getWidth.
*
* @param i The target column
* @param element The element to be dropped
* @return True if the column had space for the element
*/
boolean drop(int i, IGameObject element);
/**
* Moves all floating elements one tile down.
*/
void gravitate();
/**
* Tries to count the amount of steps required to gravitate all floating objects.
*
* @return An integer
*/
int floatingObjects();
@Override
IVerticalBoard copyAll();
}

View File

@ -0,0 +1,118 @@
package inf101.v18.connectfour.board;
import inf101.v18.connectfour.events.winning.IWinCondition;
import inf101.v18.connectfour.objects.IGameObject;
import inf101.v18.connectfour.objects.IPlayerObject;
import java.util.ArrayList;
import java.util.List;
/**
* A board which supports dropping objects and gravity.
*/
public class VerticalBoard extends Board implements IVerticalBoard {
public VerticalBoard(int width, int height, IGameObject empty, List<IWinCondition> conditions) {
super(width, height, empty, conditions);
}
/**
* Checks if anyone has won.
*
* @param i The column of the last dropped object
*/
private void checkWin(int i) {
for (IWinCondition con : conditionList) {
if (con.hasWon(i, getTopObject(i, IPlayerObject.class), this)) {
won = true;
}
}
}
@Override
public int floatingObjects() {
int maxFloat = 0;
for (int i = 0; i < getWidth(); i++) {
int topPos = -1;
int holes = 0;
for (int j = getHeight() - 1; j > -1; j--) {
if (!(empty.getClass().isInstance(grid.get(i, j)))) {
topPos = j;
} else if (topPos != -1) {
holes++;
}
}
if (holes > maxFloat) {
maxFloat = holes;
}
}
return maxFloat;
}
@Override
public void gravitate() {
for (int i = 0; i < getWidth(); i++) {
for (int j = 1; j < getHeight(); j++) {
if (!(empty.getClass().isInstance(grid.get(i, j))) && empty.getClass().isInstance(grid.get(i, j - 1))) {
grid.set(i, j - 1, grid.get(i, j));
grid.set(i, j, empty);
}
}
}
}
@Override
public boolean drop(int i, IGameObject element) {
if (i >= getWidth() || i < 0) {
throw new IllegalArgumentException("Index out of bounds.");
}
if (element == null) {
throw new IllegalArgumentException("This grid does not accept null.");
}
for (int j = 0; j < getHeight(); j++) {
if (empty.getClass().isInstance(get(i, j))) {
set(i, j, element);
if (element instanceof IPlayerObject) {
checkWin(i);
}
return true;
}
}
return false;
}
@Override
public int getTopObject(int i) {
if (i < 0 || i >= getWidth()) {
throw new IllegalArgumentException("Index out of bounds.");
}
for (int j = getHeight() - 1; j > -1; j--) {
if (!(empty.getClass().isInstance(get(i, j)))) {
return j;
}
}
return -1;
}
@Override
public int getTopObject(int i, Class<?> clazz) {
if (i < 0 || i >= getWidth()) {
throw new IllegalArgumentException("Index out of bounds.");
}
for (int j = getHeight() - 1; j > -1; j--) {
if (clazz.isInstance(get(i, j))) {
return j;
}
}
return -1;
}
@Override
public IVerticalBoard copyAll() {
VerticalBoard newBoard = new VerticalBoard(getWidth(), getHeight(), empty, conditionList);
newBoard.players = new ArrayList<>(players);
newBoard.won = won;
newBoard.grid = copy();
newBoard.playersTurn = playersTurn;
return newBoard;
}
}

View File

@ -0,0 +1,24 @@
package inf101.v18.connectfour.events.random;
import inf101.v18.connectfour.board.IVerticalBoard;
import inf101.v18.connectfour.objects.Empty;
import inf101.v18.connectfour.objects.Immovable;
/**
* Blocks a random tile on the board
*/
public class Block extends RandomEvent {
@Override
void act(IVerticalBoard b) {
int i;
do {
i = random.nextInt(b.getWidth());
} while (!b.isFull() && !(b.get(i, b.getHeight()-1) instanceof Empty));
b.drop(i, new Immovable());
}
@Override
public String announcement() {
return "An immovable object was placed.";
}
}

View File

@ -1,16 +1,19 @@
package events;
package inf101.v18.connectfour.events.random;
import board.IBoard;
import objects.Empty;
import objects.Token;
import inf101.v18.connectfour.board.IBoard;
import inf101.v18.connectfour.board.IVerticalBoard;
import inf101.v18.connectfour.objects.Empty;
import inf101.v18.connectfour.objects.IPlayerObject;
import java.util.ArrayList;
/**
* An event which removes objects from the board.
*/
public class Bomb extends RandomEvent {
@Override
public void act(IBoard b) {
void act(IVerticalBoard b) {
int x = random.nextInt(b.getWidth());
int y = b.getTopObject(x);
int y = b.getTopObject(x, IPlayerObject.class);
for (ArrayList<Integer> list : generateSquare(b, x, y, random.nextInt(2) + 1)) {
b.set(list.get(0), list.get(1), new Empty());
}
@ -29,7 +32,7 @@ public class Bomb extends RandomEvent {
ArrayList<ArrayList<Integer>> locations = new ArrayList<>();
for (int i = x - range; i <= x + range; i++) {
for (int j = y - range; j <= y + range; j++) {
if (i >= 0 && i < b.getWidth() && j >= 0 && j < b.getHeight() && b.get(i, j) instanceof Token) {
if (i >= 0 && i < b.getWidth() && j >= 0 && j < b.getHeight()) {
ArrayList<Integer> coords = new ArrayList<>();
coords.add(i);
coords.add(j);

View File

@ -0,0 +1,32 @@
package inf101.v18.connectfour.events.random;
import inf101.v18.connectfour.board.IVerticalBoard;
/**
* A random event which can be run on an IVerticalBoard.
*/
public interface IRandomEvent {
/**
* Rolls n die, and acts if all die got a "6".
*
* @param b The board to act on
* @param n The number of die
* @return True if something happened. False otherwise
*/
boolean rollDie(IVerticalBoard b, int n);
/**
* Flips a coin, and acts if it comes out as head.
*
* @param b The board to act on
* @return True if somehting happened. False otherwise
*/
boolean flipCoin(IVerticalBoard b);
/**
* An announcement describing what just happened.
*
* @return An announcement string
*/
String announcement();
}

View File

@ -1,6 +1,6 @@
package events;
package inf101.v18.connectfour.events.random;
import board.IBoard;
import inf101.v18.connectfour.board.IVerticalBoard;
import java.util.Random;
@ -10,13 +10,8 @@ import java.util.Random;
public abstract class RandomEvent implements IRandomEvent {
final Random random = new Random();
/**
* Flips a coin, and acts if it comes out as head.
*
* @param b The board to act on
* @return True if somehting happened. False otherwise
*/
public boolean flipCoin(IBoard b) {
@Override
public boolean flipCoin(IVerticalBoard b) {
if (random.nextInt(2) > 0) {
act(b);
return true;
@ -24,14 +19,8 @@ public abstract class RandomEvent implements IRandomEvent {
return false;
}
/**
* Rolls n die, and acts if all die got a "6".
*
* @param b The board to act on
* @param n The number of die
* @return True if something happened. False otherwise
*/
public boolean rollDie(IBoard b, int n) {
@Override
public boolean rollDie(IVerticalBoard b, int n) {
if (random.nextInt((int)Math.pow(6, n)) < 1) {
act(b);
return true;
@ -39,5 +28,10 @@ public abstract class RandomEvent implements IRandomEvent {
return false;
}
public abstract void act(IBoard b);
/**
* Does something depending on the event.
*
* @param b The board to act on
*/
abstract void act(IVerticalBoard b);
}

View File

@ -0,0 +1,13 @@
package inf101.v18.connectfour.events.winning;
import inf101.v18.connectfour.board.IBoard;
/**
* A win condition used to check if a placed IPlayerObject caused four in a row.
*/
public class FourInARow extends InARow {
@Override
public boolean hasWon(int x, int y, IBoard board) {
return hasWon(x, y, board, 3);
}
}

View File

@ -1,6 +1,6 @@
package events;
package inf101.v18.connectfour.events.winning;
import board.IBoard;
import inf101.v18.connectfour.board.IBoard;
/**
* A condition for winning the game.

View File

@ -1,6 +1,6 @@
package events;
package inf101.v18.connectfour.events.winning;
import board.IBoard;
import inf101.v18.connectfour.board.IBoard;
import java.awt.*;

View File

@ -0,0 +1,13 @@
package inf101.v18.connectfour.events.winning;
import inf101.v18.connectfour.board.IBoard;
/**
* A win condition used to check if a placed IPlayerObject caused three in a row.
*/
public class ThreeInARow extends InARow {
@Override
public boolean hasWon(int x, int y, IBoard board) {
return hasWon(x, y, board, 2);
}
}

View File

@ -0,0 +1,147 @@
package inf101.v18.connectfour.interfaces;
import inf101.v18.connectfour.board.IBoard;
import inf101.v18.connectfour.board.IVerticalBoard;
import inf101.v18.connectfour.player.IAI;
import inf101.v18.connectfour.player.IPlayer;
import java.awt.*;
import java.util.InputMismatchException;
import java.util.Scanner;
/**
* An abstract class implementing methods common for all CLI interfaces.
*/
public abstract class CLI implements IGameInterface {
private final String ANSI_RESET = "\u001B[0m";
private final String ANSI_GREEN = "\u001B[92m";
private final String ANSI_RED = "\u001B[91m";
private final String ANSI_BLUE = "\u001B[94m";
private final String ANSI_YELLOW = "\u001B[93m";
private final String ANSI_MAGENTA = "\u001B[95m";
private final String ANSI_CYAN = "\u001B[96m";
IBoard board;
IPlayer currentPlayer;
private final Scanner in = new Scanner(System.in);
/**
* Converts a Color to an ansi colored string because CLI doesn't support RGB.
*
* @param color The color to convert
* @return An ansi colored O or an empty string
*/
String colorToAnsi(Color color, char symbol) {
if (color == Color.green) {
return ANSI_GREEN + symbol + ANSI_RESET;
} else if (color == Color.red) {
return ANSI_RED + symbol + ANSI_RESET;
} else if (color == Color.blue) {
return ANSI_BLUE + symbol + ANSI_RESET;
} else if (color == Color.yellow) {
return ANSI_YELLOW + symbol + ANSI_RESET;
} else if (color == Color.magenta) {
return ANSI_MAGENTA + symbol + ANSI_RESET;
} else if (color == Color.cyan) {
return ANSI_CYAN + symbol + ANSI_RESET;
} else {
return ".";
}
}
/**
* Reads a name from the commandline.
*
* @param i An index representing the user being named
* @return A non-empty name
*/
String readName(int i) {
String name;
do {
displayMessage("Name of player " + i + ":");
name = in.nextLine();
} while (name.equals(""));
return name;
}
/**
* Asks the user if they want to play alone against an AI, or with a friend.
*
* @return True if the player's input contains y
*/
boolean playingAlone() {
displayMessage("Will you be playing alone? (y/n):");
return in.nextLine().toLowerCase().contains("y");
}
/**
* Gets the correct symbol for a player color
* @param color <p>A color</p>
* @return <p>A symbol</p>
*/
char getSymbol(Color color) {
if (color == Color.red) {
return 'X';
} else if (color == Color.green) {
return 'O';
}
return 'O';
}
@Override
public void doTurn() {
boolean placed = false;
System.out.printf("%nIt's %s's turn (%s)%n", currentPlayer.getName(), colorToAnsi(currentPlayer.getColor(),
getSymbol(currentPlayer.getColor())));
if (currentPlayer instanceof IAI) {
if (board instanceof IVerticalBoard) {
placed = ((IVerticalGameInterface) this).dropObject(((IAI)currentPlayer).getColumn((IVerticalBoard) board));
} else {
int[] choice = ((IAI) currentPlayer).getXY(board);
try {
placed = placePlayerObject(choice[0], choice[1]);
} catch (IllegalAccessException e) {
e.printStackTrace(); //If this happens, the AI is broken.
}
}
} else {
if (board instanceof IVerticalBoard) {
displayMessage("Drop token in (i):");
try {
int i = in.nextInt();
placed = ((IVerticalGameInterface)this).dropObject(i - 1);
} catch (InputMismatchException | IllegalArgumentException ignored) {
} finally {
in.nextLine();
}
} else {
displayMessage("Place token at (x,y):");
try {
String s = in.nextLine();
if (s.contains(",")) {
String[] numbers = s.split(",");
placed = placePlayerObject(Integer.parseInt(numbers[0]) - 1, Integer.parseInt(numbers[1]) - 1);
}
} catch (InputMismatchException | IllegalArgumentException | IllegalAccessException ignored) {}
}
}
if (!placed) {
displayMessage("Invalid input. Please try again.");
}
}
@Override
public boolean hasDrawn() {
return board.isFull();
}
@Override
public boolean hasWon() {
return board.won();
}
@Override
public void displayMessage(String s) {
System.out.println(s);
}
}

View File

@ -0,0 +1,69 @@
package inf101.v18.connectfour.interfaces;
import inf101.v18.connectfour.board.IVerticalBoard;
import inf101.v18.connectfour.board.VerticalBoard;
import inf101.v18.connectfour.events.winning.FourInARow;
import inf101.v18.connectfour.events.winning.IWinCondition;
import inf101.v18.connectfour.objects.Empty;
import inf101.v18.connectfour.objects.IGameObject;
import inf101.v18.connectfour.objects.Token;
import inf101.v18.connectfour.player.AI;
import inf101.v18.connectfour.player.Player;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
/**
* A CLI implementation of connect four.
*/
public class ConnectFourCLI extends CLI implements IVerticalGameInterface {
public ConnectFourCLI(int width, int height) {
List<IWinCondition> con = new ArrayList<>();
con.add(new FourInARow());
board = new VerticalBoard(width, height, new Empty(), con);
board.addPlayer(new Player(readName(1), Color.green));
if (playingAlone()) {
board.addPlayer(new AI("AI", Color.red));
} else {
board.addPlayer(new Player(readName(2), Color.red));
}
currentPlayer = board.nextPlayer();
}
@Override
public void displayBoard() {
for (int i = 1; i <= board.getWidth(); i++) {
System.out.print(i + " ");
}
System.out.println();
for (int i = board.getHeight() - 1; i > -1; i--) {
for (int j = 0; j < board.getWidth(); j++) {
IGameObject obj = board.get(j,i);
System.out.print(colorToAnsi(obj.getColor(), 'O') + " ");
}
if (i > 0) {
System.out.println();
}
}
}
@Override
public boolean placePlayerObject(int x, int y) {
return false;
}
@Override
public boolean dropObject(int i) {
boolean hasDropped = ((IVerticalBoard)board).drop(i, new Token(currentPlayer));
if (!hasDropped) {
return false;
}
if (board.won()) {
displayBoard();
displayMessage("\n" + currentPlayer.getName() + " won");
}
currentPlayer = board.nextPlayer();
return true;
}
}

View File

@ -1,9 +1,14 @@
package interfaces;
package inf101.v18.connectfour.interfaces;
import board.IBoard;
import objects.GameObject;
import objects.Player;
import objects.Token;
import inf101.v18.connectfour.board.IVerticalBoard;
import inf101.v18.connectfour.board.VerticalBoard;
import inf101.v18.connectfour.events.winning.FourInARow;
import inf101.v18.connectfour.events.winning.IWinCondition;
import inf101.v18.connectfour.objects.Empty;
import inf101.v18.connectfour.objects.IGameObject;
import inf101.v18.connectfour.objects.IPlayerObject;
import inf101.v18.connectfour.player.IPlayer;
import inf101.v18.connectfour.player.Player;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
@ -11,17 +16,20 @@ import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
public class FourInARowGUI extends GUI {
public FourInARowGUI(int width, int height, IBoard board) {
if (width < 3 || height < 3 || height > 50 || width > 50) {
/**
* A GUI implementation of connect four.
* This implementation tries to closely replicate the "original" version.
*/
public class ConnectFourClassicGUI extends ConnectGUI {
public ConnectFourClassicGUI(int width, int height) {
if (width < 3 || height < 3) {
throw new IllegalArgumentException("Invalid game size.");
}
if (width != board.getWidth() || height != board.getHeight()) {
throw new IllegalArgumentException("The board does not match the gui.");
}
this.board = board;
ArrayList<IWinCondition> con = new ArrayList<>();
con.add(new FourInARow());
this.board = new VerticalBoard(width, height, new Empty(), con);
for (int i = 0; i < 2; i++) {
String playerName = "";
while (playerName.equals("")) {
@ -32,9 +40,9 @@ public class FourInARowGUI extends GUI {
}
Color color;
if (i == 0) {
color = Color.green;
color = new Color(0, 200, 0);
} else {
color = Color.red;
color = new Color(200, 0, 0);
}
Player player = new Player(playerName, color);
if (!this.board.addPlayer(player)) {
@ -45,6 +53,12 @@ public class FourInARowGUI extends GUI {
initialize(width, height);
}
/**
* Initialized the gui.
*
* @param width The width of the game grid
* @param height The height of the game grid
*/
private void initialize(int width, int height) {
JFrame frame = new JFrame("Four in a row");
frame.addWindowListener(new WindowAdapter() {
@ -66,8 +80,8 @@ public class FourInARowGUI extends GUI {
this.resetButton = new JButton("Reset");
this.resetButton.addActionListener(this);
panel3.add(this.resetButton);
for (Player player : board.getPlayers()) {
JLabel playerLabel = new JLabel(player.toString());
for (IPlayer player : board.getPlayers()) {
JLabel playerLabel = new JLabel(player.getName());
playerLabel.setForeground(player.getColor());
panel3.add(playerLabel);
}
@ -75,17 +89,11 @@ public class FourInARowGUI extends GUI {
JPanel panel2 = new JPanel();
this.mainPanel = panel2;
panel2.setBackground(Color.BLUE);
panel2.setBackground(new Color(0, 0, 200));
panel2.setLayout(new FlowLayout(FlowLayout.CENTER, HEIGHT_GAP, WIDTH_GAP));
panel2.setPreferredSize(new Dimension(fullWidth, fullHeight));
dropButtons = new JButton[width];
for (int i = 0; i < width; i++) {
JButton button = new JButton();
button.addActionListener(this);
button.setPreferredSize(new Dimension(BUTTON_WIDTH, BUTTON_HEIGHT));
dropButtons[i] = button;
panel2.add(button, BorderLayout.PAGE_START);
}
initializeDropButtons(width, panel2);
tiles = new JTextPane[width * height];
readTiles();
container.add(panel2);
@ -98,33 +106,28 @@ public class FourInARowGUI extends GUI {
frame.validate();
frame.pack();
frame.setVisible(true);
this.currentPlayer = board.nextPlayer();
displayMessage("It's " + this.currentPlayer + "'s turn.");
currentPlayer = board.nextPlayer();
displayMessage("It's " + currentPlayer.getName() + "'s turn.");
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == this.resetButton) {
this.board.flush();
displayBoard();
this.currentPlayer = this.board.nextPlayer();
displayMessage("It's " + this.currentPlayer + "'s turn.");
}
if (!(this.board.won() || this.board.isFull())) {
for (int i = 0; i < this.dropButtons.length; i++) {
if (e.getSource() == this.dropButtons[i]) {
reset(e);
if (!(board.won() || board.isFull())) {
for (int i = 0; i < dropButtons.length; i++) {
if (e.getSource() == dropButtons[i]) {
if (dropObject(i)) {
displayBoard();
if (!this.board.won()) {
if (!board.won()) {
if (board.isFull()) {
displayMessage("It's a draw");
} else {
this.currentPlayer = board.nextPlayer();
displayMessage("It's " + this.currentPlayer + "'s turn.");
currentPlayer = board.nextPlayer();
displayMessage("It's " + currentPlayer.getName() + "'s turn.");
}
} else {
GameObject winnerObject = this.board.get(i, this.board.getTopObject(i));
displayMessage(((Token)winnerObject).getOwner() + " won.");
IGameObject winnerObject = board.get(i, ((IVerticalBoard)board).getTopObject(i, IPlayerObject.class));
displayMessage(((IPlayerObject)winnerObject).getOwner().getName() + " won.");
}
} else {
displayMessage("That column is full.");
@ -133,4 +136,9 @@ public class FourInARowGUI extends GUI {
}
}
}
@Override
public boolean placePlayerObject(int x, int y) throws IllegalAccessException {
throw new IllegalAccessException("Method not allowed in a vertical board.");
}
}

View File

@ -0,0 +1,247 @@
package inf101.v18.connectfour.interfaces;
import inf101.v18.connectfour.board.IVerticalBoard;
import inf101.v18.connectfour.board.VerticalBoard;
import inf101.v18.connectfour.events.random.Block;
import inf101.v18.connectfour.events.random.Bomb;
import inf101.v18.connectfour.events.random.IRandomEvent;
import inf101.v18.connectfour.events.winning.FourInARow;
import inf101.v18.connectfour.events.winning.IWinCondition;
import inf101.v18.connectfour.objects.Empty;
import inf101.v18.connectfour.objects.IGameObject;
import inf101.v18.connectfour.objects.IPlayerObject;
import inf101.v18.connectfour.player.AI;
import inf101.v18.connectfour.player.IPlayer;
import inf101.v18.connectfour.player.Player;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
/**
* A custom version of connect four.
*/
public class ConnectFourGUI extends ConnectGUI {
private final IRandomEvent bomb = new Bomb();
private final IRandomEvent block = new Block();
private JLabel eventLabel;
@Override
public boolean placePlayerObject(int x, int y) {
return false;
}
//A list of colors which can be used without conflict.
private enum Colors {GREEN, RED, BLUE, ORANGE, YELLOW, PINK, BROWN, TEAL}
public ConnectFourGUI(int width, int height) {
if (width < 3 || height < 3 || height > 50 || width > 50) {
throw new IllegalArgumentException("Invalid game size.");
}
ArrayList<IWinCondition> con = new ArrayList<>();
con.add(new FourInARow());
this.board = new VerticalBoard(width, height, new Empty(), con);
int playerNum = 0;
Colors[] colorValues = Colors.values();
while (playerNum == 0) {
try {
String num = JOptionPane.showInputDialog("Number of players: ");
if (num == null) {
System.exit(0);
}
playerNum = Integer.parseInt(num);
if (playerNum > colorValues.length) {
JOptionPane.showMessageDialog(null, "Number of players can't be higher than " + colorValues.length, "Error", JOptionPane.ERROR_MESSAGE);
playerNum = 0;
}
} catch (NumberFormatException e) {
JOptionPane.showMessageDialog(null, "Invalid input.", "Error", JOptionPane.ERROR_MESSAGE);
}
}
int aiNum = -1;
while (aiNum == -1) {
try {
String num = JOptionPane.showInputDialog("Number of ai: ");
if (num == null) {
System.exit(0);
}
aiNum = Integer.parseInt(num);
if (aiNum + playerNum > colorValues.length) {
JOptionPane.showMessageDialog(null, "Number of ai + players can't be higher than " + colorValues.length, "Error", JOptionPane.ERROR_MESSAGE);
aiNum = 0;
}
} catch (NumberFormatException e) {
JOptionPane.showMessageDialog(null, "Invalid input.", "Error", JOptionPane.ERROR_MESSAGE);
}
}
for (int i = 0; i < playerNum; i++) {
String playerName = "";
while (playerName.equals("")) {
playerName = JOptionPane.showInputDialog(String.format("Name of player %d: ", i + 1));
if (playerName == null) {
System.exit(0);
}
}
Color color = colorsToColor(Colors.values()[i]);
Player player = new Player(playerName, color);
if (!this.board.addPlayer(player)) {
JOptionPane.showMessageDialog(null, "Player is not unique. Please try again.", "Error", JOptionPane.ERROR_MESSAGE);
i--;
}
}
for (int i = 0; i < aiNum; i++) {
if (!this.board.addPlayer(new AI("AI" + (i + 1), colorsToColor(colorValues[playerNum + i])))) {
i--;
}
}
initialize(width, height);
}
/**
* Converts values in the Colors enum to actual colors.
*
* @param color An enum value
* @return An awt Color
*/
private Color colorsToColor(Colors color) {
switch (color) {
case RED:
return new Color(209,17,65);
case GREEN:
return new Color(0,177,89);
case BLUE:
return new Color(0, 174, 219);
case ORANGE:
return new Color(243, 119, 53);
case YELLOW:
return new Color(255, 196, 37);
case PINK:
return new Color(255,0,101);
case BROWN:
return new Color(224,168,153);
case TEAL:
return new Color(102,178,178);
default:
return null;
}
}
/**
* Initializes the GUI.
*
* @param width The width of the board
* @param height The height of the board
*/
private void initialize(int width, int height) {
JFrame frame = new JFrame("Four in a row");
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
int fullWidth = BUTTON_WIDTH * width + (WIDTH_GAP + 1) * width;
int fullHeight = BUTTON_HEIGHT + (TILE_HEIGHT * height) + HEIGHT_GAP * (height) + TILE_HEIGHT;
this.frame = frame;
JPanel container = new JPanel();
container.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
container.setPreferredSize(new Dimension(fullWidth, fullHeight + TILE_HEIGHT + 2 * BUTTON_HEIGHT));
frame.add(container);
JPanel panel3 = new JPanel();
this.resetButton = new JButton("Reset");
this.resetButton.addActionListener(this);
panel3.add(this.resetButton);
for (IPlayer player : board.getPlayers()) {
JLabel playerLabel = new JLabel(player.getName());
playerLabel.setForeground(player.getColor());
panel3.add(playerLabel);
}
container.add(panel3);
JPanel panel2 = new JPanel();
this.mainPanel = panel2;
panel2.setBackground(new Color(0, 0, 200));
panel2.setLayout(new FlowLayout(FlowLayout.CENTER, HEIGHT_GAP, WIDTH_GAP));
panel2.setPreferredSize(new Dimension(fullWidth, fullHeight));
dropButtons = new JButton[width];
initializeDropButtons(width, panel2);
tiles = new JTextPane[width * height];
readTiles();
container.add(panel2);
infoLabel = new JLabel();
infoLabel.setPreferredSize(new Dimension(fullWidth, BUTTON_HEIGHT));
infoLabel.setBorder(new EmptyBorder(10, 10, 10, 10));
container.add(infoLabel);
eventLabel = new JLabel();
eventLabel.setPreferredSize(new Dimension(fullWidth, BUTTON_HEIGHT));
eventLabel.setBorder(new EmptyBorder(10, 10, 10, 10));
container.add(eventLabel);
frame.validate();
frame.pack();
frame.setVisible(true);
currentPlayer = board.nextPlayer();
displayMessage("It's " + currentPlayer.getName() + "'s turn.");
}
@Override
public void actionPerformed(ActionEvent e) {
reset(e);
if (!(board.won() || board.isFull())) {
for (int i = 0; i < dropButtons.length; i++) {
if (e.getSource() == dropButtons[i]) {
if (dropObject(i)) {
displayBoard();
if (board.won()) {
IGameObject winnerObject = board.get(i, ((IVerticalBoard)board).getTopObject(i, IPlayerObject.class));
displayMessage(((IPlayerObject)winnerObject).getOwner().getName() + " won.");
} else {
if (!board.isFull()) {
triggerEvents();
}
//This might look weird, but an event can fill up the board.
if (board.isFull()) {
displayMessage("It's a draw");
} else {
currentPlayer = board.nextPlayer();
displayMessage("It's " + currentPlayer.getName() + "'s turn.");
if (currentPlayer instanceof AI) {
dropButtons[((AI)currentPlayer).getColumn((VerticalBoard)board)].doClick();
}
}
}
} else {
displayMessage("That column is full.");
}
}
}
}
}
/**
* Triggers a set of events after each player's turn.
*/
private void triggerEvents() {
if (bomb.rollDie(((IVerticalBoard)board), 2)) {
eventLabel.setText(bomb.announcement());
} else if (block.rollDie(((IVerticalBoard)board), 1)) {
eventLabel.setText(block.announcement());
} else {
eventLabel.setText("");
}
while (((IVerticalBoard)board).floatingObjects() > 0) {
((IVerticalBoard)board).gravitate();
}
displayBoard();
}
}

View File

@ -0,0 +1,66 @@
package inf101.v18.connectfour.interfaces;
import inf101.v18.connectfour.board.IVerticalBoard;
import inf101.v18.connectfour.objects.IGameObject;
import inf101.v18.connectfour.objects.Token;
import javax.swing.*;
import java.awt.*;
/**
* Implements some methods specific for connect four.
*/
public abstract class ConnectGUI extends GUI implements IVerticalGameInterface {
JButton[] dropButtons;
JTextPane[] tiles;
@Override
public void displayBoard() {
for (JTextPane pane : this.tiles) {
this.mainPanel.remove(pane);
}
this.tiles = new JTextPane[this.board.getWidth() * this.board.getHeight()];
readTiles();
this.frame.validate();
}
/**
* Reads the values from board, and creates corresponding text panes.
*/
void readTiles() {
int k = 0;
for (int i = board.getHeight() - 1; i > -1; i--) {
for (int j = 0; j < board.getWidth(); j++) {
JTextPane textPane = new JTextPane();
this.tiles[k++] = textPane;
textPane.setEditable(false);
textPane.setBackground(new Color(0, 0, 200));
IGameObject obj = board.get(j,i);
textPane.setBorder(BorderFactory.createLineBorder(obj.getColor(), 50, true));
textPane.setPreferredSize(new Dimension(BUTTON_WIDTH, TILE_HEIGHT));
mainPanel.add(textPane, BorderLayout.PAGE_START);
}
}
}
/**
* Creates a bunch of buttons.
*
* @param width The width of the game grid
* @param panel2 The panel the buttons should be attached to
*/
void initializeDropButtons(int width, JPanel panel2) {
for (int i = 0; i < width; i++) {
JButton button = new JButton();
button.addActionListener(this);
button.setPreferredSize(new Dimension(BUTTON_WIDTH, BUTTON_HEIGHT));
dropButtons[i] = button;
panel2.add(button, BorderLayout.PAGE_START);
}
}
@Override
public boolean dropObject(int i) {
return ((IVerticalBoard)board).drop(i, new Token(currentPlayer));
}
}

View File

@ -0,0 +1,54 @@
package inf101.v18.connectfour.interfaces;
import inf101.v18.connectfour.board.IBoard;
import inf101.v18.connectfour.player.IPlayer;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* Implements common variables and methods for any GUI interface.
*/
public abstract class GUI implements ActionListener, IGameInterface {
final int BUTTON_HEIGHT = 25;
final int BUTTON_WIDTH = 50;
final int TILE_HEIGHT = 50;
final int HEIGHT_GAP = 10;
final int WIDTH_GAP = 10;
JButton resetButton;
JLabel infoLabel;
JPanel mainPanel;
JFrame frame;
IBoard board;
IPlayer currentPlayer;
void reset(ActionEvent e) {
if (e.getSource() == resetButton) {
board.flush();
displayBoard();
currentPlayer = board.nextPlayer();
displayMessage("It's " + currentPlayer.getName() + "'s turn.");
}
}
@Override
public boolean hasDrawn() {
return board.isFull();
}
@Override
public boolean hasWon() {
return board.won();
}
@Override
public void displayMessage(String s) {
infoLabel.setText(s);
}
@Override
public void doTurn() {}
}

View File

@ -0,0 +1,47 @@
package inf101.v18.connectfour.interfaces;
/**
* Provides an interface a user interface.
*/
public interface IGameInterface {
/**
* Displays a message to the player(s).
*
* @param s The massage to display
*/
void displayMessage(String s);
/**
* Displays an updated board.
*/
void displayBoard();
/**
* Checks if the game ended in someone winning.
*
* @return True if the game is over
*/
boolean hasWon();
/**
* Checks if the game ended in a draw.
*
* @return True if the game is over
*/
boolean hasDrawn();
/**
* Makes the interface place an object corresponding to the current player.
*
* @param x The x-coordinate of the object to place
* @param y The y-coordinate of the object to place
* @return True if the object was placed
* @throws IllegalAccessException If method is used from a vertical interface
*/
boolean placePlayerObject(int x, int y) throws IllegalAccessException;
/**
* Makes the interface do whatever it wants to do for this turn.
*/
void doTurn();
}

View File

@ -0,0 +1,26 @@
package inf101.v18.connectfour.interfaces;
/**
* An IVerticalGameInterface replaces IGameInterface's place method with a drop method.
*/
interface IVerticalGameInterface extends IGameInterface {
/**
* Drops an object onto the board at column i,
*
* @param i Target column
* @return True if the column had available space
*/
boolean dropObject(int i);
/**
* Prevents manually placing a game piece, unless method is overwritten.
*
* @param x An integer which doesn't matter
* @param y An integer which doesn't matter
* @return Nothing
* @throws IllegalAccessException When method is used from a Vertical game
*/
default boolean placePlayerObject(int x, int y) throws IllegalAccessException {
throw new IllegalAccessException("Method not allowed in a vertical board.");
}
}

View File

@ -0,0 +1,67 @@
package inf101.v18.connectfour.interfaces;
import inf101.v18.connectfour.board.Board;
import inf101.v18.connectfour.events.winning.IWinCondition;
import inf101.v18.connectfour.events.winning.ThreeInARow;
import inf101.v18.connectfour.objects.Empty;
import inf101.v18.connectfour.objects.IGameObject;
import inf101.v18.connectfour.objects.IPlayerObject;
import inf101.v18.connectfour.objects.Token;
import inf101.v18.connectfour.player.AI;
import inf101.v18.connectfour.player.Player;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
/**
* A Tic-Tac-Toe implementation using a Command Line Interface.
*/
public class TicTacToeCLI extends CLI {
public TicTacToeCLI() {
List<IWinCondition> con = new ArrayList<>();
con.add(new ThreeInARow());
board = new Board(3, 3, new Empty(), con);
board.addPlayer(new Player(readName(1), Color.green));
if (playingAlone()) {
board.addPlayer(new AI("AI", Color.red));
} else {
board.addPlayer(new Player(readName(2), Color.red));
}
currentPlayer = board.nextPlayer();
}
@Override
public void displayBoard() {
for (int i = board.getHeight() - 1; i >= 0; i--) {
System.out.print((i + 1) + " ");
for (int j = 0; j < board.getWidth(); j++) {
IGameObject obj = board.get(j,i);
System.out.print(colorToAnsi(obj.getColor(), getSymbol(obj.getColor())) + " ");
}
System.out.println();
}
System.out.print(" ");
for (int i = 0; i < board.getWidth(); i++) {
System.out.print(" " + (i + 1));
}
}
@Override
public boolean placePlayerObject(int x, int y) {
if (board.get(x, y) instanceof IPlayerObject) {
return false;
}
try {
board.set(x, y, new Token(currentPlayer));
if (board.won()) {
displayBoard();
displayMessage("\n" + currentPlayer.getName() + " won");
}
currentPlayer = board.nextPlayer();
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}

View File

@ -1,12 +1,12 @@
package interfaces;
package inf101.v18.connectfour.interfaces;
import board.Board;
import events.IWinCondition;
import events.ThreeInARow;
import objects.Empty;
import objects.GameObject;
import objects.Player;
import objects.Token;
import inf101.v18.connectfour.board.Board;
import inf101.v18.connectfour.events.winning.IWinCondition;
import inf101.v18.connectfour.events.winning.ThreeInARow;
import inf101.v18.connectfour.objects.Empty;
import inf101.v18.connectfour.objects.IGameObject;
import inf101.v18.connectfour.objects.Token;
import inf101.v18.connectfour.player.Player;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
@ -17,6 +17,9 @@ import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.List;
/**
* A Tic-Tac-Toe implementation using a Graphical User Interface.
*/
public class TicTacToeGUI extends GUI {
private JButton[] gameGrid = new JButton[9];
@ -51,6 +54,9 @@ public class TicTacToeGUI extends GUI {
initialize();
}
/**
* Initializes the GUI.
*/
private void initialize() {
JFrame frame = new JFrame("Four in a row");
frame.addWindowListener(new WindowAdapter() {
@ -69,7 +75,7 @@ public class TicTacToeGUI extends GUI {
frame.add(container);
JPanel panel2 = new JPanel();
this.mainPanel = panel2;
mainPanel = panel2;
panel2.setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0));
panel2.setPreferredSize(new Dimension(fullWidth, fullHeight));
readTiles();
@ -79,73 +85,106 @@ public class TicTacToeGUI extends GUI {
infoLabel.setPreferredSize(new Dimension(fullWidth, 15));
infoLabel.setBorder(new EmptyBorder(10, 10, 15, 10));
container.add(infoLabel);
this.resetButton = new JButton("Reset");
this.resetButton.addActionListener(this);
container.add(this.resetButton);
resetButton = new JButton("Reset");
resetButton.addActionListener(this);
container.add(resetButton);
frame.validate();
frame.pack();
frame.setVisible(true);
currentPlayer = board.nextPlayer();
displayMessage("It's " + currentPlayer + "'s turn.");
displayMessage("It's " + currentPlayer.getName() + "'s turn.");
}
@Override
public void displayBoard() {
for (JButton button : this.gameGrid) {
this.mainPanel.remove(button);
for (JButton button : gameGrid) {
mainPanel.remove(button);
}
this.gameGrid = new JButton[this.board.getWidth() * this.board.getHeight()];
gameGrid = new JButton[board.getWidth() * board.getHeight()];
readTiles();
this.frame.validate();
frame.validate();
}
void readTiles() {
@Override
public boolean hasWon() {
return board.won();
}
@Override
public boolean placePlayerObject(int x, int y) {
if (!board.isFull()) {
try {
board.set(x, y, new Token(currentPlayer));
return true;
} catch (IllegalArgumentException ignored) {}
}
return false;
}
/**
* Converts the data contained in the board to buttons.
*/
private void readTiles() {
int k = 0;
for (int i = 0; i < board.getHeight(); i++) {
for (int j = 0; j < board.getWidth(); j++) {
JButton button = new JButton();
button.addActionListener(this);
GameObject obj = board.get(j,i);
if (obj.getColor() == Color.black) {
IGameObject obj = board.get(j,i);
if (obj.getColor() == Color.black) { //This is a bit hacky, but prevents unnecessary changes,
button.setText("X");
} else if (obj.getColor() == Color.white) {
button.setText("O");
}
button.setPreferredSize(new Dimension(BUTTON_WIDTH, TILE_HEIGHT));
this.gameGrid[k++] = button;
gameGrid[k++] = button;
mainPanel.add(button, BorderLayout.PAGE_START);
}
}
}
/**
* Handles all player input and output.
*
* @param e An event handler
*/
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == this.resetButton) {
if (e.getSource() == resetButton) {
this.board.flush();
displayBoard();
this.currentPlayer = this.board.nextPlayer();
displayMessage("It's " + this.currentPlayer + "'s turn.");
this.currentPlayer = board.nextPlayer();
displayMessage("It's " + currentPlayer.getName() + "'s turn.");
}
if (!(board.won() || board.isFull())) {
for (int i = 0; i < this.gameGrid.length; i++) {
if (e.getSource() == this.gameGrid[i]) {
if (this.gameGrid[i].getText().equals("")) {
int x = i % 3;
int y = i / 3;
this.board.set(x, y, new Token(currentPlayer));
displayBoard();
if (!board.won()) {
currentPlayer = board.nextPlayer();
displayMessage("It's " + currentPlayer + "'s turn.");
} else {
GameObject winnerObject = board.get(x, board.getTopObject(x));
displayMessage(((Token)winnerObject).getOwner() + " won.");
}
if (board.isFull()) {
displayMessage("It's a draw");
}
}
for (int i = 0; i < gameGrid.length; i++) {
if (e.getSource() == gameGrid[i] && gameGrid[i].getText().equals("")) {
doTurn(i);
}
}
}
}
/**
* Places a token, and prepares the board and itself for the next event.
*
* @param i The index of the button pushed
*/
private void doTurn(int i) {
int x = i % 3;
int y = i / 3;
board.set(x, y, new Token(currentPlayer));
displayBoard();
if (board.won()) {
IGameObject winnerObject = board.get(x, y);
displayMessage(((Token)winnerObject).getOwner().getName() + " won.");
} else {
currentPlayer = board.nextPlayer();
displayMessage("It's " + currentPlayer.getName() + "'s turn.");
if (board.isFull()) {
displayMessage("It's a draw");
}
}
}
}

View File

@ -0,0 +1,6 @@
package inf101.v18.connectfour.objects;
/**
* An object representing an empty tile.
*/
public class Empty extends GameObject {}

View File

@ -0,0 +1,15 @@
package inf101.v18.connectfour.objects;
import java.awt.*;
/**
* Implements necessary methods for every IGameObject.
*/
public abstract class GameObject implements IGameObject {
Color color = Color.LIGHT_GRAY;
@Override
public Color getColor() {
return color;
}
}

View File

@ -0,0 +1,16 @@
package inf101.v18.connectfour.objects;
import java.awt.*;
/**
* An object which can be placed on/dropped into a board.
*/
public interface IGameObject {
/**
* Retrieves the color representing the object.
* To prevent conflicts, all colors should be easily extinguishable.
*
* @return A Color
*/
Color getColor();
}

View File

@ -0,0 +1,10 @@
package inf101.v18.connectfour.objects;
import inf101.v18.connectfour.player.IPlayer;
/**
* An object placed by an IPlayer.
*/
public interface IPlayerObject extends IGameObject {
IPlayer getOwner();
}

View File

@ -1,7 +1,10 @@
package objects;
package inf101.v18.connectfour.objects;
import java.awt.*;
/**
* An object representing an obstacle which does not represent any player.
*/
public class Immovable extends GameObject {
public Immovable() {
this.color = Color.BLACK;

View File

@ -1,4 +1,4 @@
package objects;
package inf101.v18.connectfour.objects;
import java.awt.*;
import java.util.Objects;

View File

@ -0,0 +1,24 @@
package inf101.v18.connectfour.objects;
import inf101.v18.connectfour.player.IPlayer;
/**
* A token represents an object a player can place on a board.
*/
public class Token extends GameObject implements IPlayerObject {
private final IPlayer owner;
public Token(IPlayer owner) {
this.owner = owner;
this.color = owner.getColor();
}
/**
* Fetches the player associated with the token.
*
* @return A player
*/
public IPlayer getOwner() {
return this.owner;
}
}

View File

@ -0,0 +1,136 @@
package inf101.v18.connectfour.player;
import inf101.v18.connectfour.board.IBoard;
import inf101.v18.connectfour.board.IVerticalBoard;
import inf101.v18.connectfour.objects.Empty;
import inf101.v18.connectfour.objects.Token;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* An AI is a Player which must be asked for input directly.
*/
public class AI extends Player implements IAI {
private final Random random = new Random();
public AI(String name, Color color) {
super(name, color);
}
@Override
public int getColumn(IVerticalBoard b) {
List<int[]> topPositions = getTopElements(b);
IVerticalBoard copy = b.copyAll();
//Checks if the AI can win
int i = winRow(copy, topPositions, this)[0];
if (i != -1) {
return i;
}
IPlayer nextPlayer = copy.nextPlayer();
//Checks if the AI can prevent a winning move
while (nextPlayer != this) {
i = winRow(copy, topPositions, nextPlayer)[0];
if (i != -1) {
return i;
}
nextPlayer = copy.nextPlayer();
}
do {
i = random.nextInt(b.getWidth());
} while (!b.isFull() && !(b.get(i, b.getHeight()-1) instanceof Empty));
return i;
}
/**
* Retrieves a list of all possible drop locations.
*
* @param b A board
* @return A list of positions
*/
private List<int[]> getTopElements(IVerticalBoard b) {
List<int[]> postitions = new ArrayList<>();
for (int x = 0; x < b.getWidth(); x++) {
int y = b.getTopObject(x);
if (y < b.getHeight() - 1 && b.get(x, y + 1) instanceof Empty) {
postitions.add(new int[]{x, y});
}
}
return postitions;
}
/**
* Retrieves a list of all empty locations.
*
* @param b A board
* @return A list of positions
*/
private List<int[]> getEmpty(IBoard b) {
List<int[]> postitions = new ArrayList<>();
for (int i = 0; i < b.getWidth(); i++) {
for (int j = 0; j < b.getHeight(); j++) {
if (b.get(i, j) instanceof Empty) {
postitions.add(new int[]{i, j});
}
}
}
return postitions;
}
/**
* Tries to simulate all possible moves for a player, until it finds a winning move.
*
* @param b A board
* @param positions A list of positions on the board
* @param player The player to simulate
* @return A column index or -1 if the player can't win.
*/
private int[] winRow(IBoard b, List<int[]> positions, IPlayer player) {
for (int[] coords : positions) {
int x = coords[0];
int y = coords[1];
if (b instanceof IVerticalBoard) {
b.set(x, y + 1, new Token(player));
if (b.won()) {
return new int[]{x};
}
b.set(x, y + 1, new Empty());
} else {
b.set(x, y, new Token(player));
if (b.won()) {
return new int[]{x, y};
}
b.set(x, y, new Empty());
}
}
return new int[]{-1};
}
@Override
public int[] getXY(IBoard b) {
List<int[]> emptyPositions = getEmpty(b);
IBoard copy = b.copyAll();
//Checks if the AI can win
int[] coords = winRow(copy, emptyPositions, this);
if (coords[0] != -1) {
return coords;
}
IPlayer nextPlayer = copy.nextPlayer();
//Checks if the AI can prevent a winning move
while (nextPlayer != this) {
coords = winRow(copy, emptyPositions, nextPlayer);
if (coords[0] != -1) {
return coords;
}
nextPlayer = copy.nextPlayer();
}
int x, y;
do {
x = random.nextInt(b.getWidth());
y = random.nextInt(b.getHeight());
} while (!b.isFull() && !(b.get(x, y) instanceof Empty));
return new int[]{x, y};
}
}

View File

@ -0,0 +1,25 @@
package inf101.v18.connectfour.player;
import inf101.v18.connectfour.board.IBoard;
import inf101.v18.connectfour.board.IVerticalBoard;
/**
* An IAI is an IPlayer with methods for retrieving its next move.
*/
public interface IAI extends IPlayer {
/**
* Gets the column where the AI wants to drop its token.
*
* @param b A board
* @return A valid column
*/
int getColumn(IVerticalBoard b);
/**
* Gets the x and y coordinate of the board position where the AI wants to place its token.
*
* @param b A board
* @return An array of integers with length 2
*/
int[] getXY(IBoard b);
}

View File

@ -0,0 +1,22 @@
package inf101.v18.connectfour.player;
import java.awt.*;
/**
* An IPlayer is a representation of an actual player.
*/
public interface IPlayer {
/**
* Retrieves a color representing the player.
*
* @return A Color
*/
Color getColor();
/**
* Retrieves a string representing the player.
*
* @return A String
*/
String getName();
}

View File

@ -0,0 +1,47 @@
package inf101.v18.connectfour.player;
import java.awt.*;
import java.util.Objects;
/**
* A class representing a player playing a game.
*/
public class Player implements IPlayer {
private final String name;
private final Color color;
public Player(String name, Color color) {
if (name == null || name.equals("")) {
throw new IllegalArgumentException("Player name is invalid.");
}
if (color == null) {
throw new IllegalArgumentException("Player color is invalid");
}
this.name = name;
this.color = color;
}
@Override
public Color getColor() {
return color;
}
@Override
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Player player = (Player) o;
return Objects.equals(name, player.name) ||
Objects.equals(color, player.color);
}
@Override
public int hashCode() {
return Objects.hash(name, color);
}
}

View File

@ -1,3 +0,0 @@
package objects;
public class Empty extends GameObject {}

Some files were not shown because too many files have changed in this diff Show More