Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 5.3

NB: WORK IN PROGRESS

 

Excerpt

Dette eksemplet handler om ConnectFour-spillet med en tydelig oppdeling i klasser for brikke, brett og spill-logikk.

Oppgavebeskrivelse

For å beregne omkretsen og arealet til en sirkel må objektet ihvertfall vite om radiusen. Hvis objektet også skal kunne vises som grafikk, vil en også trenge posisjon, men dette utelater vi her.

Tilstanden i Circle-objekter blir dasom følger:

  • radius - et desimaltall som angir radiusen til sirkelen. Denne være satt når en sirkel opprettes - en sirkel kan ikke eksistere uten å ha en radius!

Circle-klassen har to metoder, getCircumference() og getArea(), med følgende oppførsel:

  • double getCircumference() - beregner omkretsen til sirkelen, basert på radius og returnerer denne
  • double getArea() - beregner arealet til sirkelen basert på radius og returnerer denne

Begge disse metodene gjør en beregning basert på innholdet i objektet, men endrer ikke objektet.

I tillegg er det greit å lage en passende toString()-metode og et hovedprogram, slik at en kan sjekke at oppførselen stemmer med spesifikasjonen, altså beskrivelsen over.

Eksempelløsning

Klassen lagrer radiusen i et attributt (kalt felt i Java) av typen double. Dette feltet initialiseres av konstruktøren, som tar inn et double-argumentet og setter radius-feltet til denne verdien. Her brukes this.radius for å referere til attributtet (og "hoppe over" argumentet) og kun radius for å referere til argumentet.

Metodene getCircumference() og getArea() bruker radius-attributtet i de vanlige formlene for omkrets og areal. Her er bruken av this i this.radius strengt tatt ikke nødvendig, siden radius alene også vil blir tolket som en referanse til attributtet, da det ikke er argumenter eller lokale variable "i veien".

I spillet ConnectFour skal to spillere legge hver sine brikker i et rutenett på 7x7 ruter og prøve å få fire på rad før brettet er fullt. Oppgaven fokuserer på å realisere dette ved bruk av tre klasser, med en tydelig rollefordering: Piece-klassen inneholder verdien til en brikke på brettet, ConnectFour-klassen håndterer selve brettet og hvem sin tur det er, mens ConnectFourProgram-klassen håndterer tekst-basert interaksjon med spillerne gjennom konsollet. Denne tydelige oppdelingen i logikk og interaksjon (også kalt brukergrensesnitt) vil gjøre det lettere å senere lage et grafisk brukergrensesnitt uten å måtte programmere alt på nytt, siden logikk-klassen vil kunne gjenbrukes. For å gjøre klassene noenlunde uavhengig av hverandre brukes prinsippet om Innkapsling.

Oppgaven er åpen i den forstand at den ikke spesifiserer akkurat hvilke metoder hver klasse skal ha, men sier noe om hvordan det skal ta seg ut for brukeren. Dette gjør det vanskeligere å teste enkeltmetoder, så istedenfor testes teksten som kommer ut, basert på hva brukeren (eller testprogrammet) gir inn.

Det er ofte lurt å løse slike oppgaver i mindre trinn, og derfor har vi nedenfor spesifisert hvilke funksjoner vi tror er lurt å lage i hvert trinn.

  • Trinn 1 - kunne vise frem brettet med og uten brikker. I dette trinnet lager du ConnectFour-klassen med en toString()-metode som viser brettet med de brikkene som er satt (f.eks. av et enkelt test-hovedprogram du lager selv). ConnectFour-klassen skal være ordentlig innkapslet. Du må også lage Piece-klassen for å representere brikker.
  • Trinn 2 - spillerne kan legge brikker. I dette trinnet lages en enkel versjon av ConnectFourProgram-klassen, slik at spillerne etter tur kan legge brikker ved å angi kolonnen de ønsker å slippe en brikke ned i. ConnectFourProgram-klassen skal ta seg av all input og utskrift. For interaksjon med brukeren kan det være lurt å bruker Scanner-klassen.
  • Trinn 3 - et helt fungerende spill, hvor ConnectFour-klassen kan si fra til ConnectFourProgram-klassen hvilken spiller som har turen, om spillet er ferdig og hvilken spiller som evt. har vunnet.

Nedenfor har vi vist en mulig spillsekvens som både illustrerer brett-formatet og dialogen mellom spillet og spillerne. Output til brukeren er i svart, mens input fra brukeren er i grønt. Legg merke til at de er utført mange trekk mellom sekvensen til venstre og høyre.

Image Added

....

Image Added

Eksempelløsning

Piece-klassen trenger et felt for å holde en verdi (' ' for tom, 'x' for spiller x og 'o' for spiller o). Dette feltet er innkapslet og validert; bare brikker med verdi ' ', 'x' eller 'o' kan opprettes og kun brikker med verdi ' ' kan endrestoString()-metoden lager en String med ved å skjøte sammen mange deler med +. En alternativ variant med bruk av String.format er også vist. Den tar inn en String med formatteringsdirektiver som forteller hvor de påfølgende argumentene skal spleises inn i teksten. %.2f brukes som direktiv siden verdiene er desimaltall (f for floating point-verdier) og vi bare vil at tallene skal vises med to desimaler.

Code Block
package connectfour;

public class Piece {

	private char value;

	public Piece(char value) {
		this.value = valuesetValue(value);
	}

	public char getValue() {
		return value;
	}

	public void setValue(char value) {
		if (" xo".indexOf(value) < 0) {
			throw new IllegalArgumentException("Illegal piece!");
		}
		if (this.value != ' ' && this.value != '\0') {
			throw new IllegalStateException("Cannot alter a non-blank piece!");		
		}
		this.value = value;
	}

	public String toString() {
		return "" + getValue();
	}
}

 

ConnectFour-klassen representerer brettet med ArrayLists av Piece-objekter inni en ArrayList. Her svarer den ytterste ArrayListen til radene og de innerste til kolonnene i spillet. I konstruktøren instansieres board-feltet og player-feltet. Videre har klassen metoder for å returnere Piece-objekt på posisjon rad, kolonne i brettet samt å sette en posisjon rad, kolonne til et nytt slikt objekt. Disse er private da de kun skal kunne brukes av klassen selv for å sørge for konsistent spilloppførsel (skal f.eks. ikke være lov å la en brikke "henge i løse luften"). Metoden drop(int) legger en ny brikke på brettet og hasWon() sjekker hvorvidt en spiller har vunnet (oppnådd fire på rad). Metodene getPlayer() og changePlayer() håndterer hvilken spiller som står for tur. toString()-metoden returnerer en streng-representasjon av brettet.something here

Code Block
package connectfour;


import java.util.ArrayList;

public class ConnectFour {
	private ArrayList<ArrayList<Piece>> board;
	private char player;

	public ConnectFour() {
		board = new ArrayList<ArrayList<Piece>>();
		for (int r = 0; r < 7; r++) {
			board.add(new ArrayList<Piece>());
			for (int c = 0; c < 7; c++) {
				board.get(r).add(new Piece(' '));
			}
		}
		player = 'o';
	}


	publicprivate Piece getPiece(int r, int c) {
		return board.get(r).get(c);
	}


	publicprivate void setPiece(int r, int c, Piece piece) {
		board.get(r).set(c, piece);
	}

	public boolean drop(int c) {
		if (getPiece(0, c).getValue() != ' ') {
			return false;
		} else {
			for (int r = 6; r >= 0; r--) {
				if (getPiece(r, c).getValue() == ' ') {
					setPiece(r, c, new Piece(player));
					return true;
				}
			}
			return true;
		}
	}

	public boolean hasWon() {
		for (int r = 0; r < 7; r++) {
			for (int c = 0; c < 7; c++) {
				if (getPiece(r,c).getValue() != ' ' && hasWonFromPosition(r,c)) {
					return true;
				}
			}
		}
		return false;
	}

	private boolean hasWonFromPosition(int r, int c) {
		for (int dr = -1; dr <= 1; dr++) {
			for (int dc = -1; dc <= 1; dc++) {
				if (dr != 0 || dc != 0) {
					if (hasWonFromPositionWithDirection(r,c,getPiece(r,c), dr, dc)) {
						return true;
					}
				}
			}
		}
		return false;
	}

	private boolean hasWonFromPositionWithDirection(int r, int c, Piece piece, int dr, int dc) {
		int counter = 0;
		while (0 <= r && r < 7 && 0 <= c && c < 7 && getPiece(r, c).getValue() == piece.getValue()) {
			r += dr;
			c += dc;
			counter++;
		}
		return counter >= 4;
	}


	public char getPlayer() {
		return player;
	}

	public void changePlayer() {
		if (player == 'o') {
			player = 'x';
		} else {
			player = 'o';
		}
	}

	public String toString() {
		String str = "";
		for (int r = 0; r < 7; r++) {
			str += "| ";
			for (int c = 0; c < 7; c++) {
				str += getPiece(r, c) + " ";
			}
			str += "|\n";
		}
		return str;
	}
}

 

ConnectFourProgram-klassen følger konvensjonen med init() og run()-metoder som invokeres fra main()-metoden. I init() instansieres ConnectFour-objektet. I run() står programmet i løkke så lenge spillet ikke har en vinner. I løkken skrives først strengrepresentasjonen av brettet ut før spilleren blir spurt om neste trekk (hvilken kolonne hun ønsker å slippe sin neste brikke i). Dersom dette trekker et lovlig (i hvilket tilfelle drop(int) returnerer true) byttes neste spiller til å utføre trekk. Når en spiller har vunnet skrives brettet ut og vinneren annonseres.something here

Code Block
package connectfour;


import java.util.Scanner;

public class ConnectFourProgram {

	ConnectFour cf;


	public void init() {
		cf = new ConnectFour();
	}

	public void run() {
		Scanner scanner = new Scanner(System.in);
		while (! cf.hasWon()) {
			System.out.println(cf);
			System.out.println("Player " + cf.getPlayer() + ", enter index of column to drop next piece: ");
			int c = scanner.nextInt();
			if (cf.drop(c) && ! cf.hasWon()) {
				cf.changePlayer();
			}
		}
		System.out.println(cf);
		System.out.println("Congratulations player " + cf.getPlayer() + "! You have won the game.");
	}

	public static void main(String[] args) {
		ConnectFourProgram cfp = new ConnectFourProgram();
		cfp.init();
		cfp.run();
	}
}

 

 

For å prøve ut koden lager vi en hovedprogramklasse kalt CircleProgram. Det vanlige er å ha en init()- og en run()-metode, men vi utelater init() her, siden programmet er så lite og enkelt. Det er tross alt vi som velger hva de heter, basert på hva som er ryddig og bekvemt.

I run()-metoden opprettes to Circle-objekter med new. Disse er illustrert i figuren til høyre (id'ene #1 og #2 er kun med for å illustrere at dette er forskjellige objekter). Disse objektene skrives så ut med System.out.println-metoden. Dette vil implisitt kalle toString()-metoden, siden System.out.println bruker denne internt, for å gjøre om argumentet til en String, før det skrives ut.

Merk at main-metoden må være deklarert akkurat slik for å bli kalt av Java, når klassen som helhet skal utføres. Den lager en instans av programmet og kaller run()-metoden som gjør "jobben".