MokaByte 79 - 9mbre 2003 
Robocode
II parte: Advanced Robocode
di
Simone Chiaretta
Entriamo nel dettaglio di Robocode: il funzionamento del simulatore, e finalmente gli AdvancedRobot

Introduzione
Dopo aver visto lo scorso mese le basi per iniziare a "giocare" con Robocode, cioè come installarlo e come fare il primo "Bot", in questo numero vedremo come funziona il simulatore di battaglia e come costruire un robot più complesso, ma anche più potente, usando come base l'AdvancedRobot. Lungo il percorso avremo occasione di "ripassare" un po' di matematica, necessaria per progettare algoritmi avanzati.

Il simulatore: l'architettura
Il simulatore di robocode è un sofisticato motore disegnato sia per essere efficiente (deve simulare le battaglie a velocità sostenuta), che flessibile (consente la creazione di robot dalla logica complessa con un accoppiamento molto basso). Un ringraziamento va a Mathew Nelson, il creatore di Robocode, per aver pubblicato una breve descrizione del meccanismo interno di funzionamento di Robocode.
Il motore di simulazione è stato creato sfruttando le possibilità offerte dai thread non-preemptive delle ultime VM e le funzioni di rendering offerte dalle librerie 2D incluse in Java.



Figura 1 - L'architettura del simulatore

Come si nota dallo schema, il gestore delle battaglia dirige la simulazione, comandando i thread dei singoli robot e il sottosistema di rendering grafico, basato su AWT e Java 2D.

Per diminuire le possibilità di deadlock nell'accesso alle risorse condivise, è stato necessario ridurre l'accoppiamento tra il thread del gestore e quelli dei robot: questo scopo è stato raggiunto dotando ogni robot della sua coda di eventi. In questa maniera è stato rimossa ogni possibilità di accesso contemporaneo da parte dei vari thread.

 

Il simulatore: la logica
Si può vedere il simulatore come un programma che carica a run-time un insieme di plugin (i robot); ognuno di questi plugin può accedere alle API fornite ereditando dalla classe robocode.Robot.
Ogni robot è un singolo thread e all'interno del metodo run() è contenuto il codice che sarà eseguito durante la simulazione. In ogni istante, un robot può chiamare un metodo fornito dalla classe padre, e questo tipicamente potrebbe fermare l'esecuzione del robot con una Object.wait().

Il gestore delle battaglia è incaricato di gestire il movimento dei robot, dei proiettili e il disegno del campo di battaglia. Il tempo della simulazione è identificato dal numero di frame disegnati sullo schermo. E il frame rate è regolabile dall'utente.

In ogni turno, il gestore "sveglia" i thread di tutti robot e aspetta che questi termino il loro turno chiamando un metodo bloccante. La durata di ogni turno è tipicamente una decina di millisecondi, e anche i robot dalla logica più complessa impiegano 1-2 ms con gli attuali sistemi di calcolo.

Vediamo come funziona un turno di simulazione (in pseudo-codice):

fino a che il round non è finito
  disegna i robot, i proiettili e le esplosioni
  per ogni robot
    sveglia il thread
    aspetta che il robot chiami un funzione bloccante, fino ad un massimo intervallo                                                                           di tempo
  end
  svuota le code degli eventi dei robot
  sposta i proiettili, ed eventualmente genera degli eventi nelle code dei robot
  sposta i robot, ed eventualmente genere gli eventi nelle code dei robot
  finalizza il campo di battaglia e genera eventualmente altri eventi nelle code dei                                                                           robot
  attende per adeguarsi al frame-rate impostato
end do

Da notare che ad ogni turno il gestore non aspetta per un infinito intervallo di tempo che un robot completi le sue elaborazioni, ma, dopo un massimo periodo di tempo viene, terminato il turno del robot e viene generato un evento di tipo SkippedTurnEvent. Questo accade in genere se c'è qualche errore nel codice del robot (es loop infinito).

Il sistema di rendering grafico attuale usa AWT e Java2D e disegna sullo schermo ricevendo le istruzioni dal gestore delle battaglia. E' stato implementato in modo che fosse abbastanza disaccoppiato dal resto del sistema per rendere possibile l'implementazione di sistemi di rendering alternativi (già esiste un renderer 3D basato su OpenGL). Al momento il sistema di rendering viene disattivato quando l'applicazione viene minimizzata, per consentire alla simulazione di girare al frame-rate massimo.


Costruiamo qualcosa di più complesso: AdvancedRobot
Il mese scorso abbiamo realizzato un robot ereditando dalla classe Robot

public class MokaRobot extends Robot

cioè il MokaRobot aveva accesso alle API fornite della classe Robot, tipo la turnLeft() e la ahead().
Una grossa limitazione di queste API è rappresentata dal fatto che le chiamate ai metodi di "movimento" sono bloccanti, cioè non restituiscono il controllo al codice fino a che lo spostamento non è terminato. Siccome queste operazioni potrebbero durare molti turni, siamo impossibilitati dal fare operazioni per tutto questo tempo.
Fortunatamente esiste la classe AdvancedRobot che ci permette di riprendere il controllo. Per fare questo dovremo dichiarare che il nostro nuovo robot eredita da AdvancedRobot

public class AdvancedMokaRobot extends AdvancedRobot

E' importante notare che AdvancedRobot è una sottoclasse di Robot e che il nostro AdvancedMokaRobot è a sua volta una sottoclasse di AdvancedRobot.


Figura 2 - Diagramma delle classi

Ovviamente dall'interno di AdvancedMokaRobot ora avremo a disposizione sia i metodi della classe Robot che quelli della classe AdvancedRobot.
Ora vedremo come utilizzare le nuove possibilità offerteci dalle nuove API.

 

I metodi non bloccanti
Con gli AdvancedRobot abbiamo la possibilità di modificare il comportamento del nostro robot ad ogni turno. Un turno è chiamato tick ed è relazionato con i frame disegnati sul campo di battaglia. Durante ogni turno il nostro codice riprende il controllo e decide cosa fare con il robot: con i PC attuali si possono eseguire molte linee di codice (e quindi prendere decisioni molto complesse) ad ogni tick.

Tutti i metodi che abbiamo incontrato fino ad ora sono bloccanti, ossia restituiscono il controllo solo dopo aver eseguito tutta l'operazione. Ora, poiché AdvancedMokaRobot è una sottoclasse di AdvancedRobot, possiamo usare tutta una serie di metodi non bloccanti, cioè che restituiscono immediatamente il controllo al chiamante.
Esiste un metodo non bloccante per ognuno dei metodi bloccanti visti in precedenza: ad esempio abbiamo setTurnLeft(), setAhead(), setBack(), ecc… Notate che tutti i nomi dei metodi sono composti da "set" e il nome del metodo bloccante. Usando queste chiamate possiamo fare compiere al nostro robot più movimenti contemporaneamente.

Iniziamo con l'analizzare il codice di AdvancedMokaRobot per vedere come questi metodi si usano in pratica.

protected void reverseDirection() {
  if(clockwise) {
    setTurnRightRadians(PI);
    setAhead(2000);
  } else {
    setTurnLeftRadians(PI);
    setBack(2000);
  }
  setTurnGunRightRadians(PI);
  clockwise=!clockwise;
}

In questa funzione, noi comandiamo al robot di girare di 360°, di andare avanti di 2000px e di ruotare la torretta a destra di 360°, tutto nello stesso turno.
Tutti questi metodi restituiscono il controllo al programma immediatamente dopo essere stati chiamati e il nostro robot non farà nulla fino a che il controllo non sarà restituito al gestore di battaglie. E questo avviene non appena viene chiamata un funzione bloccante (cioè tutte quelle senza "set", oppure fire) o quando viene chiamato il metodo execute().
Execute() ritorna il controllo a Robocode per esattamente un tick, cioè è come se io dicessi a Robocode che ho deciso cosa fare per questo turno e lui muove il robot.
Un'altra alternativa è quella di usare il metodo waitFor() che non restituisce il controllo al codice fino a che non avviene una determinata condizione.

while(true) {
  //attendo di aver completato la rotazione
  waitFor(new TurnCompleteCondition(this));
  //inverto la direzione di rotazione
  reverseDirection();
}

Nel nostro caso attendiamo che il robot abbia finito di girare. E siccome si muove e gira allo stesso tempo, il robot percorrerà una traiettoria circolare.
Nel codice viene usata anche la condizione MoveCompleteCondition, per attendere il completamento di tutto il movimento, rotazione e spostamento.

 

Eventi customizzati e classi anonime
Il mese scorso abbiamo visto come gestire gli eventi estendendo i metodi per la loro gestione.
Ora analizziamo una nuova funzionalità fornita dagli AdvancedRobot: i customEvents.
Questi verificano l'esistenza di una certa condizione, e in caso affermativo, alzano l'evento

addCustomEvent(new Condition("NearWall") {
  public boolean test() {
    return (getX()<allowedWallDistance ||
            getY()<allowedWallDistance ||
            getX()>getBattleFieldWidth()-allowedWallDistance ||
            getY()>getBattleFieldHeight()-allowedWallDistance);
    }
  });

Nel robot d'esempio abbiamo creato una condizione usando il meccanismo delle classi anonime.
All'interno del metodo addCustomEvent ho creato una nuova classe che eredita da Condition e esteso il metodo test(). La classe è chiamata "anonima" perché non ha nome.
La nostra condizione solleva l'evento quando il robot si trova sul bordo del campo di battaglia

Per eseguire del codice in seguito ad un customEvent bisogna estendere il metodo onCustomEvent() e verificare al suo interno quale evento è stato sollevato.

public void onCustomEvent(CustomEvent ev) {
  Condition cd = ev.getCondition();
  System.out.println("event with " + cd.getName());
  if (cd.getName().equals("NearWall")) {
    centerRobot();
  }
}

 

RoboMath: la matematica usata in Robocode
Nel robot d'esempio abbiamo avuto a che fare con le coordinate del campo di battaglia e con i vari heading e bearing .
Chi avesse conoscenze di orientamento e topografia sono rispettivamente direzione e "rilevamento".
E questo paragone è anche abbastanza azzeccato perché lo zero degli angoli è verso l'alto, come se fosse una bussola, e non verso destra come il solito cerchio usato in trigonometria.

 


Figura 3
- Coordinate del campo

Nel file d'esempio ho incluso verso la fine un set di metodi che è possibile riutilizzare per eseguire le più comuni operazioni necessarie per realizzare i calcoli matematici necessari per funzioni di puntamento.
Per la spiegazione del codice rimando ai commenti inclusi, ma volevo far notare che tutte le operazioni con gli angoli sono fatte in radianti e non i gradi: Robocode fornisce metodi per lavorare sia con gradi che con i radianti, ma nel suo funzionamento interno usa i radianti, quindi le operazioni con i radianti sono più precise. Inoltre le funzioni trigonometriche fornite da Java lavorano in radianti.

 

La fisica in robocode
I robot nel campo di gioco si spostano rispettando alcune regole fisiche. Conoscere quali siano queste regole è necessario per programmare correttamente il nostro robot. Ora ne vedremo alcune, ma vi rimando alle FAQ su Robocode accessibili dal menu Help->FAQ o direttamente alla pagina [7] indicata nella webliografia.

  • la massima velocità di avanzamento è di 8 px/tick
  • la rotazione di un robot è inversamente proporzionale la velocità di avanzamento e varia da 4°/tick a 9°/tick
  • la torretta gira a 20°/tick e il radar a 45°/tick, e queste velocità si sommano (se ruoto entrambi uno sull'altro avrò 65°/tick)
  • i proiettili viaggiano a 20 - 3*potenza px/tick

 

Conclusione
In questo articolo abbiamo visto la classe AdvancedRobot, necessaria per realizzare dei robot competitivi al pari di quelli che competono nelle varie leghe su internet. Inoltre abbiamo iniziato a lavorare con gli angoli e abbiamo visto la pseudo-fisica che sta alla base del movimento dei robot.
Ora che avete tutti gli strumenti per sviluppare i vostri robot, il prossimo mese racconterò la storia di Robocode, vedremo l'attuale stato dell'arte e come costruire un team di robot che combattono insieme contro un altro team di avversari.

 

Link e risorse
[1] http://www.alphaworks.ibm.com/tech/robocode - Sito ufficiale di Robocode
[2] http://www.ecs.soton.ac.uk/~awo101/robocode.html - SnippetBot tutorial: tutti i primi robocoder hanno iniziato da qui
[3] http://robowiki.dyndns.org/perl/robowiki - Un wiki su Robocode, il punto di ritrovo attuale di tutti i robocoders
[4] http://www.robocoderepository.com - Il primo sito su robocode, e ancora ora repository generale per tutti i robot sviluppati e resi disponibili
[5] http://www.eternal-rumble.com - Sito che gestisce una ranking list generale
[6] http://robowiki.dyndns.org/perl/robowiki?Robocode_3D - HomePage per Robocode 3D
[7] http://robocode.alphaworks.ibm.com/help/physics/physics.html - Tutte le costanti fisiche del gioco

[8] Gli esempi descritti in questo articolo

 
MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it