Space Invaders [Testo del progetto di Programmazione II per Febbraio 2013]
Indice
Descrizione
Il progetto consiste nell'implementare in Java un'applicazione che simuli, con le dovute semplificazioni, l'esecuzione di una partita al famoso arcade Space Invaders (1978).Space Invaders
Il Gioco
Nella versione originale del gioco, il giocatore controlla gli spostamenti di un cannoncino laser che può muoversi esclusivamente da destra a sinistra e viceversa nella parte bassa dello schermo mentre dall'alto scendono schiere di alieni desiderosi di conquistare la Terra. Compito del giocatore è proprio quello di distruggere le armate nemiche senza farsi distruggere o lasciare conquistare la Terra.
L'intera armata di alieni si muove sistematicamente da destra verso sinistra fino a raggiungere il bordo estremo quindi scende di una riga e si muove nella direzione opposta e così via fino a toccare terra. Durante i loro spostamenti, gli alieni sparano, in modo asincrono, dei proiettili diretti verso terra. Il cannoncino può sparare alla volta degli alieni e può proteggersi nascondendosi dietro delle casematte (3 per la precisione). Le casematte se colpite un numero adeguato di volte vengono distrutte, mentre basta un singolo colpo a distruggere un alieno o il cannoncino. Gli spari del cannoncino e quelli degli alieni si elidono quando si scontrano. Si hanno a disposizione tre cannoncini mentre gli alieni eliminati non vengono rimpiazzati; se si distrugge l'intera armata un'altra la sostituisce passando al quadro successivo.
Parallelamente al gioco, una navicella aliena appare di tanto in tanto in cima allo schermo muovendosi da sinistra a destra; arrivata al bordo opposto dello schermo
sparisce. Ovviamente può essere colpita e rappresenta un bonus.
Ovviamente non è possibile, nell'ambito del progetto, implementare interamente il gioco stesso ma dovrete realizzarne una simulazione che permetta il passaggio da
una configurazione ad un'altra. In particolare, imporremo le seguenti limitazioni/varianti:
È obbligatorio realizzare in Java il programma descritto nelle sezioni precedenti utilizzando le classi sotto indicate. Si noti che è richiesto di:
Notare la peculiare presenza di alcuni attributi nella classe astratta; vale sempre il principio che lo stato degli oggetti deve essere nascosto ma in questo
caso deve poter essere usato dalle classi concrete che estenderanno la classe astratta.
La classe La classe La classe Le istanze della classe Le istanze della classe La configurazione è codificata in una stringa come segue:
La Figura mostra una possibile configurazione per un campo di gioco di 7 x 10 caselle.
In particolare sono presenti 18 elementi: 1 navicella, 1 cannoncino, 3 casematte e 13 alieni. La corrispondente stringa sarà:
Notare che nell'esempio non ci sono spari in gioco e gli alieni sono tutti trattati allo stesso modo (ad eccezione della navicella rossa).
Attenzione che i vari metodi potrebbero sollevare delle eccezioni queste non sono state specificate nel testo ma dovranno essere gestite cum grano salis.
Le classi A parte quanto espressamente richiesto, è lasciata piena libertà sull'implementazione delle singole classi e sull'eventuale introduzione di altre classi, a patto di
seguire le regole del paradigma ad oggetti ed i principi di buona programmazione.
Non è richiesto e non verrà valutato l'utilizzo di particolari modalità grafiche di visualizzazione: è sufficiente una qualunque modalità di visualizzazione basata sull'uso dei caratteri.
È invece espressamente richiesto di non utilizzare package non standard di Java (si possono quindi utilizzare La classe Semplificazioni e varianti
Lo scopo a questo punto diventa quello di far muovere le varie entità in gioco e di salvare (a richiesta) la configurazione corrente.
Valutazione
Il vostro programma sarà sottoposto a un test automatico che attribuirà un punteggio, da 0 a 30, sulla base di quanti dei metodi e costruttori
elencati risulti implementato correttamente. Un punteggio inferiore a 15 comporterà un'esclusione dalla valutazione successiva.
La valutazione del progetto verrà poi effettuata, per i soli progetti che abbiano superato la prima fase, tenendo conto dei seguenti criteri:
Consegna
Per consegnare:
Vi ricordo che la documentazione delle classi standard è disponibile
puntando il browser su file:///usr/share/javadoc/java/index.html
/home/LABDID/pboldi00/consegna *.java
Classi da Realizzare
BattleFieldElement
BattleFieldElement è una classe astratta usata per descrivere il comportamento, in astratto, dei vari personaggi in gioco.
Attenzione! Questa classe e tutte quelle che la estendono tranne Gun e Shot hanno un costruttore che riceve la posizione (x, y) dell'elemento
e le dimensioni (righe, colonne) del campo di battaglia.
Attributi
int x e int y: variabili di istanza che memorizzano la posizione corrente dell'elemento sul campo di gioco.
int rows e int cols: variabili di istanza che memorizzano il numero di righe e di colonne del campo di gioco; le colonne sono
numerate da 0 (la più a sinistra) a cols-1; le righe sono numerate da 0 (la più in alto) a rows-1.
Costanti
Nella classe sono definite quattro costanti (campi statici finali) intere che verranno usate nel progetto per rappresentare le quattro direzioni: SU,
GIU, DESTRA, SINISTRA. I loro valori sono arbitrari.
Metodi
public void setPosition(int x, int y): sposta la posizione corrente del corrispondente elemento al punto specificato dai valori passatigli. Solleva una IllegalPositionException se i valori passati sono illegali.
public int getX() e public int getY(): restituiscono la posizione corrente;
public String toString(): metodo dall'ovvio significato ereditato da Object e ridefinito in modo da garantire la rappresentazione di ogni singolo elemento come spiegato descrivendo la configurazione del campo da gioco.
abstract public boolean canMove(): serve per stabilire se questo elemento può spostarsi, allo stato attuale delle cose; ad esempio, se l'elemento
si muove in verticale e si sta spostando verso l'alto, il metodo restituirà true se e solo se l'elemento si trova su una riga successiva alla prima.
Notate che il risultato del metodo dipende ovviamente non solo dalla posizione corrente ma anche dal tipo di elemento in questione, ed è per questo motivo che il
metodo è astratto.
abstract public int nextX() e abstract public int nextY(): servono per stabilire, rispettivamente, in quale posizione x e y si troverà
questo elemento al prossimo passo: questo dipende ovviamente non solo dalla posizione corrente ma anche dal tipo di elemento in questione. I valori restituiti
sono irrilevanti se il metodo canMove() restituisce false.
public void moveIfYouCan(): questo metodo non ha nessun effetto se il pezzo non si può muovere; se invece si può muovere, posiziona il pezzo
nella nuova posizione prevista (invocando i metodi astratti elencati sopra). Notate che è possibile che alcuni pezzi debbano anche aggiornare, oltre alla loro posizione,
altri campi (per esempio, il pezzo Gun si muove alternativamente da destra a sinistra e da sinistra a destra: per far questo tiene un campo che dice
in quale direzione si sta spostando e quando si trova ai bordi del campo di battaglia deve anche cambiare il valore della direzione).
BattleFieldElement dovrà essere estesa da diverse altri classi che rappresenteranno i vari elementi del gioco. Nel seguito descriveremo le
singole classi senza entrare nel merito dell'implementazione dei metodi ereditati ma solo del comportamento delle sue istanze.
Alien
Alien classe astratta derivata da BattleFieldElement, rappresenta la classe progenitrice di tutte le entità aliene presenti nel gioco.
Attributi
int direction: indica in che direzione si muoverà l'alieno.
Metodi
public void invertDirection(): inverte la direzione di movimento di questo alieno (se era SU diventa GIU, ecc.).
RedSpacecraft
RedSpacecraft, classe concreta che estende Alien, rappresenta la navicella bonus; questa si muove sempre e solo da destra verso sinistra di una casella alla volta sulla riga più in alto dello schermo (sollevare l'eccezione IllegalPositionException se si tenta di posizionarla in una riga diversa). Arrivata in fondo all'estrema sinistra sparisce dal campo di gioco.
OneStepAlien
OneStepAlien, classe concreta che estende Alien, rappresenta gli alieni; tutti gli alieni di questo tipo si muovono come se fossero
un'unica entità da destra a sinistra e viceversa (non scendono o salgono mai, partono muovendosi da destra verso sinistra). Quando un alieno raggiunge il bordo
l'intera armata cambia direzione. L'alieno, in quanto tale, si comporta come se non si potesse più muovere quando raggiunge il bordo.
Shot
Shot è una classe astratta derivata da BattleFieldElement che rappresenta la classe progenitrice di tutti i possibili spari, sia degli alieni (istanze della classe AlienShot)
che del cannoncino (istanze della classe GunShot). Due spari che collidono si elidono come pure si elidono lo sparo e l'elemento (alieno, casamatta o cannoncino) che colpisce. Gli spari degli alieni vanno dall'alto verso il basso mentre quelli del cannoncino dal basso verso l'alto. Raggiunti gli estremi del campo di gioco escono dal gioco.
Attributi
int direction: indica in che direzione si muove lo sparo; la classe contiene due costanti (campi statici finali) che contengono i
valori corrispondenti alle due direzioni (ad esempio, SU potrebbe avere valore 0 e GIU valore 1).
Empty
Empty estende BattleFieldElement e rappresenta le caselle vuote che ovviamente non si spostano (o meglio non ha senso che si
spostino).
Gun
Gun (che estende BattleFieldElement) rappresentano il cannoncino.
Quando un cannoncino viene distrutto (colpito da uno sparo o scontrandosi con un alieno) viene rimpiazzato da un altro cannoncino
la cui posizione iniziale sarà il centro della linea più in basso del campo di gioco.
Ad ogni mossa, il cannoncino procede, una casella alla volta, nella direzione in cui si stava muovendo fino a raggiungere l'estremo del campo di gioco a questo punto
inverte la direzione e continua la sua marcia (inizia muovendosi da sinistra a destra).
I cannoncini possono essere solo sull'ultima riga altrimenti si deve sollevare l'eccezione IllegalPositionException.
Questa classe è l'unica, fra quelle che estendono BattleFieldElement ad avere un costruttore che prende solo tre argomenti: non viene infatti
passata nel costruttore la coordinate y poiché quest'ultima è fissa e non può cambiare.
Casemate
Casemate (che estende BattleFieldElement) rappresentano le casematte. Sono ferme nella loro posizione e ce ne possono essere un po' ovunque tranne in quella più in basso riservata al cannoncino (nell'ipotesi si deve sollevare l'eccezione IllegalPositionException).
BattleField
BattleField è la classe che descrive il campo di gioco (lo schermo del videogioco per intendersi) durante il gioco stesso. Sostanzialmente può essere schematizzato come una matrice di n x m elementi, con n e m non definiti a priori.
Ogni elemento o sarà vuoto o conterrà un alieno, un cannoncino, uno sparo o una casamatta. In ogni istante un'istanza della classe BattleField fornirà uno snapshot della situazione (configurazione) corrente del campo di gioco.
Esempio di configurazione
'R', 'A', 'G', 'C', 's', 'S', ' '}
e rappresenta l'elemento in questione (usiamo, qui e nel seguito, ' ' per indicare uno spazio);
notare che avere n in {1,…,9} non limita a 9 la lunghezza delle righe in quanto si possono avere due o più coppie
contigue dello stesso elemento, ad es. 9 7 rappresenta una sequenza di 16 caselle vuote;
'R' |= navicella rossa (il bonus nel gioco originale);
'A' |= alieno;
'G' |= cannoncino;
'C' |= casamatta;
's' |= sparo del cannoncino;
'S' |= sparo degli alieni;
' ' |= casella vuota.
Attributi
BattleFieldElement[][] battlefield: variabile d'istanza che memorizza la configurazione corrente del campo di gioco. Ogni elemento della matrice conterrà un elemento diverso da null.
int rows e int columns: numero totale delle righe e delle colonne che compongono il campo di gioco.
String filename: variabile di istanza contenente il nome del file su cui si salvano le configurazioni e che viene usato per il ripristino della configurazione. Implementare anche i corrispondenti metodi publici String getFilename() e void setFilename(String) dall'ovvio significato.
Metodi e costruttori
public BattleField(String filename): costruttore che configura il campo di gioco, filename è il nome del file su disco contenente una serie di configurazioni, una per riga, l'ultima configurazione letta diventerà la configurazione corrente.
public BattleField(int rows, int columns): costruttore che crea un campo di gioco con le dimensioni date e completamente vuoto; se viene usato questo costruttore,
filename risulta uguale a null. L'intero campo è pieno di caselle vuote (Empty).
public int rows() e public int cols(): restituiscono rispettivamente il numero di righe e di colonne del campo di battaglia.
public void setFilename(String filename): metodo che modifica l'attributo filename ponendolo uguale alla stringa passata come argomento.
public String getFilename(): metodo che restituisce l'attributo filename.
void setBattleFieldElement(int, int, BattleFieldElement) e
BattleFieldElement getBattleFieldElement(int, int):
metodi accessori per la gestione del singolo elemento del campo di gioco;
public void setBattleField(String): metodo che permette di inizializzare il campo di gioco alla configurazione passata come parametro, la configurazione è codificata nella stringa secondo le stesse convenzioni descritte precedentemente;
può sollevare varie eccezioni, fra cui IllegalPositionException (se qualche pezzo non è posizionato nel modo corretto), IllegalElementException (se
si tenta di creare più di un cannoncino), IllegalCharacterException (se il file contiene qualche carattere non legale).
public String getBattleField(): metodo che ritorna la configurazione corrente codificata secondo le convenzioni descritte precedentemente; per questo metodo
potreste trovare utile il metodo getClass: questo metodo, ereditato da Object (e quindi disponibile per tutti gli oggetti), fornisce la
classe di cui un oggetto è istanza; in questo modo è possibile sapere se due oggetti arbitrari sono istanze della stessa classe;
public void write(): metodo che appende la configurazione corrente del campo di gioco al file corrispondente;
public void reload(): metodo che rilegge dal file indicato dall'attributo filename la configurazione del campo di gioco;
public void backup(String): metodo che effettua una copia di backup della configurazione del campo di gioco, salvandola in un file il cui nome è specificato nella stringa passata come argomento, il file verrà creato ex-novo;
public String toString(): metodo che crea e ritorna una stringa rappresentante la configurazione corrente della campo di gioco. A differenza del metodo
getBattleField, il metodo toString deve restituire una stringa stampabile così formata:
cols+2 puntini;
cols+2 puntini.
public boolean allAlienCanMove(): questo metodo analizza tutti i OneStepAlien in campo e stabilisce se tutti si possono spostare (in tal caso
restituisce true) oppure no; in quest'ultima circostanza (che si verifica quando almeno un alieno si trova presso il bordo del campo) deve restituire false.
public void allAlienInvert(): questo metodo invoca invertDirection su tutti i OneStepAlien in campo.
public void move(): metodo che fa avanzare la configurazione di un passo; partendo dall'angolo in alto a sinistra e procedendo da sinistra a destra.
Il metodo deve procedere come sotto indicato:
BattleFieldElement di dimensioni identiche a quella di gioco;
battlefield, partendo da quello in alto a sinistra, e procedento da sinistra a
destra e dall'alto in basso; per ogni elemento esaminato, opera come segue:
Empty, non fa niente, e prosegue passando all'elemento successivo;
canMove()) viene spostato (*);
RedSpacecraft giunta al bordo: in tal caso non si fa niente, e si passa all'elemento successivo (l'astronave viene così fatta scomparire);
Shot giunto al bordo: in tal caso non si fa niente, e si passa all'elemento successivo (lo sparo viene così fatto scomparire);
Casemate: in tal caso l'elemento non viene spostato, ma non si passa all'elemento successivo (*);
OneStepAlien e Gun si possono necessariamente muovere
(i primi perché si è proceduto a cambiare direzione se necessario all'inizio, e il secondo perché una Gun si può sempre muovere);
getX() e getY() per averne la nuova posizione;
in questa fase si tiene conto (mediante un flag) se è stato piazzato un null);
Gun;
Gun, ne viene creato uno (nella posizione centrale della riga più in basso); se la posizione
è già occupata, ciò che contiene viene buttato via;
null vengono sostituiti
con un'istanza di Empty;
battlefield viene sostituito con quello nuovo.
Eccezioni
IllegalElementException, IllegalPositionException e IllegalMovementException, da definire in modo opportuno,
rappresentano le eccezioni da lanciare (con le modalità indicate) quando si verificano le condizioni di errore descritte nei punti precedenti.
Tutte le altre eccezioni previste dall'uso di metodi Java devono filtrare ed essere gestite nel metodo main() anche se non espressamente indicato dalla
segnatura dei metodi introdotti in questo documento.
java.util,
java.io e così via), con l'unica eccezione del package prog.io incluso nel libro di testo per gestire l'input da tastiera e l'output a video e di rispettare l'interfaccia fornita.
Esempio di programma di test ed esecuzione
TestSpaceInvaders riportata di seguito è un esempio di possibile main() contro il quale potrebbe essere provato il vostro programma.
public class TestSpaceInvaders {
public static void main(String[] args) throws Exception {
BattleField bf = new BattleField( "es-in.txt" );
System.out.println( "at the beginning :- " + bf.getBattleField() + bf.toString() );
bf.move();
System.out.println( "1st step :- " + bf.getBattleField() + bf.toString() );
bf.move();
System.out.println( "2nd step :- " + bf.getBattleField() + bf.toString() );
bf.move();
System.out.println( "3rd step :- " + bf.getBattleField() + bf.toString() );
bf.setBattleFieldElement( 5, 3, new GunShot( 5, 3, bf.rows(), bf.cols() ) );
bf.setBattleFieldElement( 2, 1, new AlienShot( 2, 1, bf.rows(), bf.cols() ) );
System.out.println( "new entries :- " + bf.getBattleField() + bf.toString() );
bf.move();
System.out.println( "4th step :- " + bf.getBattleField() + bf.toString() );
bf.move();
System.out.println( "5th step :- " + bf.getBattleField() + bf.toString() );
bf.move();
bf.backup( "es-out.txt" );
}
}
Supponete che il file es-in.txt sia:
5|5|1 1B3 $5 $2 1C2 $5 $2 1A2 $
5|5|1 1R3 $5 $1C1 1C2 $5 $2 1A2 $
7|10|8 1R1 $4 3A1 1A1 $4 2C1 1A2 $4 2A4 $2 1C4A3 $2 1A7 $1 1G8 $
7|10|1 1R6 1R1 $4 3A1 1A1 $4 2C1 1A2 $4 2A4 $2 1C4A3 $2 1A7 $1 1G8 $
L'esecuzione del programma di cui sopra produrrà su standard output quanto segue:
at the beginning :- 7|10|1 1R6 1R1 $4 3A1 1A1 $4 2C1 1A2 $4 2A4 $2 1C4A3 $2 1A7 $1 1G8 $
............
. R R .
. AAA A .
. CC A .
. AA .
. CAAAA .
. A .
. G .
............
1st step :- 7|10|1R6 1R2 $3 3A1 1A2 $4 2C1A3 $3 2A5 $3 3A4 $1 1A8 $2 1G7 $
............
.R R .
. AAA A .
. CCA .
. AA .
. AAA .
. A .
. G .
............
2nd step :- 7|10|6 1R3 $2 3A1 1A3 $4 1C5 $2 2A6 $2 3A5 $1A9 $3 1G6 $
............
. R .
. AAA A .
. C .
. AA .
. AAA .
.A .
. G .
............
3rd step :- 7|10|5 1R4 $3 3A1 1A2 $4 1C5 $3 2A5 $3 3A4 $1 1A8 $4 1G5 $
............
. R .
. AAA A .
. C .
. AA .
. AAA .
. A .
. G .
............
new entries :- 7|10|5 1R4 $2 1S3A1 1A2 $4 1C5 $3 2A1s4 $3 3A4 $1 1A8 $4 1G5 $
............
. R .
. SAAA A .
. C .
. AAs .
. AAA .
. A .
. G .
............
4th step :- 7|10|4 1R5 $4 3A1 1A1 $2 1S1 1C1s4 $4 2A4 $4 3A3 $2 1A7 $5 1G4 $
............
. R .
. AAA A .
. S Cs .
. AA .
. AAA .
. A .
. G .
............
5th step :- 7|10|3 1R6 $6 2A1 1A$4 1C5 $2 1S2 2A3 $5 3A2 $3 1A6 $6 1G3 $
............
. R .
. AA A.
. C .
. S AA .
. AAA .
. A .
. G .
............
Il contenuto del file es-out.txt alla fine sarà:
7|10|2 1R7 $5 2A1 1A1 $4 1C5 $4 2A4 $2 1S1 3A3 $2 1A7 $7 1G2 $