
// Worm.java
// Andrew Davison, April 2005, ad@fivedots.coe.psu.ac.th

/* Obsahuje interní datové struktury červíka (kruhový buffer)
   a kód pro rozhodování o pozici a směru následujícího pohybu.
*/

import java.awt.*;
import java.awt.geom.*;


public class Worm
{
  // velikost a počet teček v červíkovi
  private static final int DOTSIZE = 12;
  private static final int RADIUS = DOTSIZE/2;
  private static final int MAXPOINTS = 40;

  // konstanty směru/orientace
  private static final int NUM_DIRS = 8;
  private static final int N = 0;  // sever, atd.,ve směru hodinových ručiček
  private static final int NE = 1;
  private static final int E = 2;
  private static final int SE = 3;
  private static final int S = 4;
  private static final int SW = 5;
  private static final int W = 6;
  private static final int NW = 7;

  private int currCompass;  // uchovává aktuální směr

  // uchovává přírůstky v každém směru
  // přírůstek se přidá ke staré pozici hlavy pro získání pozice nové
  Point2D.Double incrs[];

  // pravděpodobnost volby směru
  private static final int NUM_PROBS = 9;
  private int probsForOffset[];

  // pole cells[] uchovává tečky tvořící červa
  // chová se jako kruhový buffer
  private Point cells[];
  private int nPoints;
  private int tailPosn, headmosn;   // čelo a konec bufferu

  private int pWidth, pHeight;   // rozměry panelu
  private long startTime;        // v ms
  private Obstacles obs;


  public Worm(int pW, int pH, Obstacles os)
  {
    pWidth = pW; pHeight = pH;
    obs = os;
    cells = new Point[MAXPOINTS];   // vytvoř pole
    nPoints = 0;
    headmosn = -1;  tailPosn = -1;

    // přírůstky pro každý směr
    incrs = new Point2D.Double[NUM_DIRS];
    incrs[N] = new Point2D.Double(0.0, -1.0);
    incrs[NE] = new Point2D.Double(0.7, -0.7);
    incrs[E] = new Point2D.Double(1.0, 0.0);
    incrs[SE] = new Point2D.Double(0.7, 0.7);
    incrs[S] = new Point2D.Double(0.0, 1.0);
    incrs[SW] = new Point2D.Double(-0.7, 0.7);
    incrs[W] = new Point2D.Double(-1.0, 0.0);
    incrs[NW] = new Point2D.Double(-0.7, -0.7);

    // údaje o pravděpodobnosti volby daného směru
    //    0 = beze změmy, -1 znamená 1 krok proti směru hod. ruč.,
    //    1 znamená 1 krok ve směru hod. ruč., atp.
    /* Pole znamená, že červík obvykle pokračuje stejným směrem,
       ale může lehce zatáčet vlevo či vpravo. */
    probsForOffset = new int[NUM_PROBS];
    probsForOffset[0] = 0;  probsForOffset[1] = 0;
    probsForOffset[2] = 0;  probsForOffset[3] = 1;
    probsForOffset[4] = 1;  probsForOffset[5] = 2;
    probsForOffset[6] = -1;  probsForOffset[7] = -1;
    probsForOffset[8] = -2;

  } // konec konstruktoru: Worm()


  public boolean nearHead(int x, int y)
  // je souřadnice (x, y) poblíž červíkovy hlavičky?
  { if (nPoints > 0) {
      if( (Math.abs( cells[headmosn].x + RADIUS - x) <= DOTSIZE) &&
           (Math.abs( cells[headmosn].y + RADIUS - y) <= DOTSIZE) )
        return true;
    }
    return false;
  } // konec metody: nearHead()


  public boolean touchedAt(int x, int y)
  // je souřadnice (x, y) poblíž některého článku červíkova tělíčka?
  {
    int i = tailPosn;
    while (i != headmosn) {
      if( (Math.abs( cells[i].x + RADIUS - x) <= RADIUS) &&
          (Math.abs( cells[i].y + RADIUS - y) <= RADIUS) )
        return true;
      i = (i+1) % MAXPOINTS;
    }
    return false;
  }  // konec metody: touchedAt()


  public void move()
  /* Pohyb způsobující přidání nové tečky k přední části červíka,
     která se stane novou hlavou. Pozice a směr tečky je odvozen
     od pozice a směru staré hlavy.

     Metoda move() je složitější, protože musí řešit 3 případy:
       * když je červík poprvé vytvořen
       * když červík roste
       * když červík doroste do délky MAXPOINTS (poté musí být přidání nové hlavy
         vyváženo odebráním koncové tečky)
  */
  {
    int prevPosn = headmosn;  // před vytvořením nové ulož původní pozici hlavičky
    headmosn = (headmosn + 1) % MAXPOINTS;

    if (nPoints == 0) {   // na začátku je pole prázdné
      tailPosn = headmosn;
      currCompass = (int)( Math.random()*NUM_DIRS );  // náhodný směr
      cells[headmosn] = new Point( pWidth/2, pHeight/2 ); // středový bod
      nPoints++;
    }
    else if (nPoints == MAXPOINTS) {    // pole je plné
      tailPosn = (tailPosn + 1) % MAXPOINTS;    // zapomeň poslední ocas
      newHead(prevPosn);
    }
    else {     // v pole cells[] je stále místo
      newHead(prevPosn);
      nPoints++;
    }
  }  // konec metody: metody: move()


  private void newHead(int prevPosn)
  /* Vytvoří novou pozici a směr hlavy.

     Má dvě hlavní části. Zpočátku zkoušíme vygenerovat
     hlavu obměnou pozice/směru staré hlavy. Pokud však 
     nová hlava zasáhne překážku, přesuneme se do druhé fáze.
 
     Ve druhé fázi zkusíme hlavu, která je 90 stupňů ve směru
     hodinových ručiček, 90 stupňů proti směru hod.ruč nebo 180
     stupňů zpět, takže se červík vyhne překážce. Tyto směry 
     jsou uloženy v poli fixedOffs[].
  */
  {
    int fixedOffs[] = {-2, 2, -4};  // posuny pro vyhýbaní se překážkám

    int newBearing = varyBearing();
    Point newPt = nextPoint(prevPosn, newBearing );
      // Vezmi novou pozici na základě polo-náhodné
      // obměny aktuální pozice

    if (obs.hits(newPt, DOTSIZE)) {
      for (int i=0; i < fixedOffs.length; i++) {
        newBearing = calcBearing(fixedOffs[i]);
        newPt = nextPoint(prevPosn, newBearing);
        if (!obs.hits(newPt, DOTSIZE))
          break;     // jeden z pevně daných posunů určitě zabere
      }
    }
    cells[headmosn] = newPt;     // nová pozice hlavičky
    currCompass = newBearing;    // nová orientace dle kompasu
  }  // konec metody: newHead()


  private int varyBearing()
  // pseudonáhodně orientaci pozměň 
  {
    int newOffset = probsForOffset[ (int)( Math.random()*NUM_PROBS )];
    return calcBearing( newOffset );
  }  // konec metody: varyBearing()


  private int calcBearing(int offset)
  // Používá offset pro výpočet nové orientace dle kompasu založený
  // na aktuálním směru
  {
    int turn = currCompass + offset;
    // zajisti, že je otočka mezi N a NW (0 až 7)
    if (turn >= NUM_DIRS)
      turn = turn - NUM_DIRS;
    else if (turn < 0)
      turn = NUM_DIRS + turn;
    return turn;
  }  // konec metody: calcBearing()



  private Point nextPoint(int prevPosn, int bearing)
  /* Vrátí následující souřadnici na základě předchozí pozice
     a směru.

     Převede směr na předem nastavený přírůstek (uložený v poli
     incrs[]). Přidá přírůstky vynásobené číslem DOTSIZE ke staré
     pozici hlavy.
     Řeší také kruhovou návaznost obrazovky.
  */
  { 
    // vezmi přírůstek pro daný směr
    Point2D.Double incr = incrs[bearing];

    int newX = cells[prevPosn].x + (int)(DOTSIZE * incr.x);
    int newY = cells[prevPosn].y + (int)(DOTSIZE * incr.y);

    // uprav newX/newY jsou-li < 0, nebo > pWidth/pHeight; použij kruhovou návaznost obrazovky
    if (newX+DOTSIZE < 0)     // je kruh za levým okrajem obrazovky?
      newX = newX + pWidth;
    else  if (newX > pWidth)  // je kruh za pravým okrajem obrazovky?
      newX = newX - pWidth;  

    if (newY+DOTSIZE < 0)     // je kruh za horním okrajem obrazovky?
      newY = newY + pHeight;
    else  if (newY > pHeight) // je kruh za dolním okrajem obrazovky?
      newY = newY - pHeight;

    return new Point(newX,newY);
  }  // konec metody: nextPoint()


  public void draw(Graphics g)
  // vykreslí černého červíka s červenou hlavičkou
  {
    if (nPoints > 0) {
      g.setColor(Color.black);
      int i = tailPosn;
      while (i != headmosn) {
        g.fillOval(cells[i].x, cells[i].y, DOTSIZE, DOTSIZE);
        i = (i+1) % MAXPOINTS;
      }
      g.setColor(Color.red);
      g.fillOval( cells[headmosn].x, cells[headmosn].y, DOTSIZE, DOTSIZE);
    }
  }  // konec metody: draw()

}  // konec třídy: Worm

