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

/* Červík se pohybuje přes obrazovku a uživatel musí
   klepnout (stisknout) na jeho hlavu.

   Pokud ji uživatel mine, přidá se na obrazovku modrá
   kostička (pokud neklepnul na tělo červíka).

   Červík se přes tuto kostičku nemůže pohybovat, takže 
   přidaná překážka může usnadnit lov.

   Červík začíná na nulové délce a roste až do své
   maximální délky, která mu zůstane.

   Na konci se na obrazovce zobrazí dosažený počet bodů,
   vypočítáný z počtu kostiček a uplynulého času. Méně kostiček
   a kratší čas znamená vyšší skóre.

   -------------

   Používá výhradní celoobrazovkový režim, aktivní vykreslování a
   double buffering/page flipping.   

   Na obrazovce jsou tlačítka pro pozastavení a ukončení.

   Používá časovač z knihovny Java 3D: J3DTimer.getValue()
     *  nanosekundy míst milisekund

   Průměrné hodntoy FPS / UPS
		     20		 50	    80	        100
   Win 98:         20/20       50/50       81/83       84/100
   Win 2000:       20/20       50/50       60/83       60/100
   Win XP (1):     20/20       50/50       74/83       76/100
   Win XP (2):     20/20       50/50       83/83       85/100

   Umístěn v adresáři /WormFSEM

   Aktualizováno: 12. Února 2004
     * na konec metody setDisplayMode() přidán spánek navíc 

     * volání createBufferStrategy() přesunuto do zvláštní metody
       setBufferStrategy() s dalšími doplňky
   ----
*/

import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import java.text.DecimalFormat;
import java.awt.image.BufferStrategy;

import com.sun.j3d.utils.timer.J3DTimer;


public class WormChase extends JFrame implements Runnable
{
  private static final int NUM_BUFFERS = 2;    // pro page flipping

  private static int DEFAULT_FPS = 100;

  private static long MAX_STATS_INTERVAL = 1000000000L;
    // (zhruba) každou sekundu zaznamenává statistiky

  private static final int NO_DELAYS_PER_YIELD = 16;
  /* Počet snímků s prodlevou 0 ms před tím, než animační vlákno předá
     běh ostatním vláknům. */

  private static int MAX_FRAME_SKIPS = 5;   // was 2;
    // počet snímků, které mohou být v jakémkoliv animačním cyklu přeskočeny
    // tj. aktualizuje se stav hry, ale nedojde k jejímu vykreslení

  private static int NUM_FPS = 10;    
     // počet FPS hodnot uložených pro získání průměru


  private int pWidth, pHeight;     // rozměry panelu

  // pro statistické údaje
  private long statsInterval = 0L;    // v ns
  private long prevStatsTime;   
  private long totalElapsedTime = 0L;
  private long gameStartTime;
  private int timeSpentInGame = 0;    // v sekundách

  private long frameCount = 0;
  private double fpsStore[];
  private long statsCount = 0;
  private double averageFPS = 0.0;

  private long framesSkipped = 0L;
  private long totalFramesSkipped = 0L;
  private double upsStore[];
  private double averageUPS = 0.0;


  private DecimalFormat df = new DecimalFormat("0.##");  // 2 dm
  private DecimalFormat timedf = new DecimalFormat("0.####");  // 4 dm


  private Thread animator;            // vlákno provádející animaci
  private volatile boolean running = false;    // pro pozastavení animacního vlákna

  private long period;                // interval mezi vykresleními v _nanosekundách_

  private Worm fred;       // červík
  private Obstacles obs;   // překážky
  private int boxesUsed = 0;


  // pro ukončení hry
  private volatile boolean gameOver = false;
  private int score = 0;
  private Font font;
  private FontMetrics metrics;
  private boolean finishedOff = false;

  // pro tlačítko 'konec'
  private volatile boolean isOverQuitButton = false;
  private Rectangle quitArea;

  // pro tlačítko 'pozastavit'
  private volatile boolean isOverPauseButton = false;
  private Rectangle pauseArea;
  private volatile boolean isPaused = false;

  
  // pro výhradní celoobrazovkový režim
  private GraphicsDevice gd;
  private Graphics gScr;
  private BufferStrategy bufferStrategy;


  public WormChase(long period) 
  {
    super("Worm Chase");

    this.period = period;
    initFullScreen();

	readyForTermination();

    // vytvoř herní komponenty
    obs = new Obstacles(this);
    fred = new Worm(pWidth, pHeight, obs);

    addMouseListener( new MouseAdapter() {
      public void mousePressed(MouseEvent e)
      { testPress(e.getX(), e.getY()); }
    });

    addMouseMotionListener( new MouseMotionAdapter() {
      public void mouseMoved(MouseEvent e)
      { testMove(e.getX(), e.getY()); }
    });

    // nastav font pro zprávy
    font = new Font("SansSerif", Font.BOLD, 24);
    metrics = this.getFontMetrics(font);

    // urči oblasti na obrazovce pro tlačítka
    pauseArea = new Rectangle(pWidth-200, pHeight-100, 170, 40);
    quitArea = new Rectangle(pWidth-200, pHeight-50, 170, 40);

    // inicializuje časovací prvky
    fpsStore = new double[NUM_FPS];
    upsStore = new double[NUM_FPS];
    for (int i=0; i < NUM_FPS; i++) {
      fpsStore[i] = 0.0;
      upsStore[i] = 0.0;
    }

    gameStart();
  }  // konec metody: WormChase()


  private void initFullScreen()
  {
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    gd = ge.getDefaultScreenDevice();

    setUndecorated(true);    // bez lišty s menu, okrajů a prvků knihovny Swing
    setIgnoreRepaint(true);  // vypni všechny kreslící událost, protože používáme aktivní vykreslování    setResizable(false);

    if (!gd.isFullScreenSupported()) {
      System.out.println("Výhradní celoobrazovkový režim není podmorován");
      System.exit(0);
    }
    gd.setFullScreenWindow(this); // zapni výhradní celoobrazovkový režim

    // nyní můžeme upravit režim obrazovky, pokud chceme
    showCurrentMode();

    // setDisplayMode(800, 600, 8);   // nebo můžeme vyzkoušet toto 8 bits
    // setDisplayMode(1280, 1024, 32);

    // reportCapabilities();

    pWidth = getBounds().width;
    pHeight = getBounds().height;

    setBufferStrategy();
  }  // konec metody: initFullScreen()



  private void reportCapabilities()
  {
    GraphicsConfiguration gc = gd.getDefaultConfiguration();

    // Možnosti obrázku
    ImageCapabilities imageCaps = gc.getImageCapabilities();
    System.out.println("Image Caps. isAccelerated: " + imageCaps.isAccelerated() );
    System.out.println("Image Caps. isTrueVolatile: " + imageCaps.isTrueVolatile());

    // Možnosti bufferu
    BufferCapabilities bufferCaps = gc.getBufferCapabilities();
    System.out.println("Buffer Caps. isPageFlipping: " + bufferCaps.isPageFlipping());
    System.out.println("Buffer Caps. Flip Contents: " + 
                            getFlipText(bufferCaps.getFlipContents()));
    System.out.println("Buffer Caps. Full-screen Required: " + 
                            bufferCaps.isFullScreenRequired());
    System.out.println("Buffer Caps. MultiBuffers: " + bufferCaps.isMultiBufferAvailable());
  } // konec metody: reportCapabilities()


  private String getFlipText(BufferCapabilities.FlipContents flip) 
  {
    if (flip == null)
      return "false";
    else if (flip == BufferCapabilities.FlipContents.UNDEFINED)
      return "Undefined";
    else if (flip == BufferCapabilities.FlipContents.BACKGROUND)
      return "Background";
    else if (flip == BufferCapabilities.FlipContents.PRIOR)
      return "Prior";
    else // if (flip == BufferCapabilities.FlipContents.COPIED)
      return "Copied";
  } // konec metody: getFlipTest()


  private void setBufferStrategy()
  /* Zapme page flipping: NUM_BUFFERS == 2, takže
     bude k dispozici primární povrch a zadní buffer.

     Použití metody invokeAndWait() je kvůli možnému uváznutí
     s vláknem dispečera událostí. Mělo by být opraveno v J2SE 1.5.

     Metoda createBufferStrategy() je asynchronní operací, takže
     jsme přidali trochu spánku, aby volání metody getBufferStrateg()
     obdrželo správné údaje.
  */
  { try {
      EventQueue.invokeAndWait( new Runnable() {
        public void run() 
        { createBufferStrategy(NUM_BUFFERS);  }
      });
    }
    catch (Exception e) {  
      System.out.println("Chyba při tvorbě zobrazovací strategie");  
      System.exit(0);
    }

    try {  // spánek poskytující čas pro provedení strategie bufferu
      Thread.sleep(500);  // 0.5 sec
    }
    catch(InterruptedException ex){}

    bufferStrategy = getBufferStrategy();  // ulož na později
  }  // konec metody: setBufferStrategy()


  private void readyForTermination()
  {
	addKeyListener( new KeyAdapter() {
	// pro snadné ukončení z celoobrazovkové konfigurace
	// naslouchej klávesám esc, q, end, ctrl-c na herní obrazovce
       public void keyPressed(KeyEvent e)
       { int keyCode = e.getKeyCode();
         if ((keyCode == KeyEvent.VK_ESCAPE) || (keyCode == KeyEvent.VK_Q) ||
             (keyCode == KeyEvent.VK_END) ||
             ((keyCode == KeyEvent.VK_C) && e.isControlDown()) ) {
           running = false;
         }
       }
     });

     // pro činnosti spojené s ukončením
     // ukončení nemusí přijít jen z tohoto programu
     Runtime.getRuntime().addShutdownHook(new Thread() {
       public void run()
       { running = false;   
         finishOff();
       }
     });
  }  // konec metody: readyForTermination()



  private void gameStart()
  // inicializuj a spusť vlákno
  { 
    if (animator == null || !running) {
      animator = new Thread(this);
	  animator.start();
    }
  } // konec metody: gameStart()
    

  private void testPress(int x, int y)
  /* Řeší tlačítka ukončení a pozastavení.
     Dále, je (x,y) blízko hlavy, or nebo by měla být přidána překážka?
  */
  { if (isOverPauseButton)
      isPaused = !isPaused;     // přepni stav pozastavení hry
    else if (isOverQuitButton)
      running = false;
    else {
      if (!isPaused && !gameOver) {
        if (fred.nearHead(x,y)) {   // došlo ke stisku myši poblíž hlavy?
          gameOver = true;
          score =  (40 - timeSpentInGame) + (40 - boxesUsed);    
             // dej dohromady skóre
        }
        else {   // přidej překážku, je-li to možné
          if (!fred.touchedAt(x,y))   // červíkovo tělo nebylo zasaženo?
            obs.add(x,y);
        }
      }
    }
  }  // konec metody: testPress()



  private void testMove(int x, int y)
  // je bod (x, y) nad tlačítkem "Pozastav" nebo "Konec"?
  { 
    if (running) {   // potlačí problémy s rychlým pohybem after pressing 'quit'
      isOverPauseButton = pauseArea.contains(x,y) ? true : false;
      isOverQuitButton = quitArea.contains(x,y) ? true : false;
    }
  }


  public void setBoxNumber(int no)
  // volá se z objektu typu Obstacle
  {  boxesUsed = no;  }



  public void run()
  /* Snímky animace vykreslujeme v cyklu while. */
  {
    long beforeTime, afterTime, timeDiff, sleepTime;
    long overSleepTime = 0L;
    int noDelays = 0;
    long excess = 0L;

    gameStartTime = J3DTimer.getValue();
    prevStatsTime = gameStartTime;
    beforeTime = gameStartTime;

	running = true;

	while(running) {
	  gameUpdate();     
      screenUpdate();

      afterTime = J3DTimer.getValue();
      timeDiff = afterTime - beforeTime;
      sleepTime = (period - timeDiff) - overSleepTime;  

      if (sleepTime > 0) {   // v cyklu ještě nějaký čas zůstal
        try {
          Thread.sleep(sleepTime/1000000L);  // nanosekundy -> milisekundy
        }
        catch(InterruptedException ex){}
        overSleepTime = (J3DTimer.getValue() - afterTime) - sleepTime;
      }
      else {    // sleepTime <= 0; snímek trval déle než-li interval
        excess -= sleepTime;  // ulož množství zbylého času
        overSleepTime = 0L;

        if (++noDelays >= NO_DELAYS_PER_YIELD) {
          Thread.yield();   // dej možnost běhu ostatním vláknům
          noDelays = 0;
        }
      }

      beforeTime = J3DTimer.getValue();

      /* Trvá-li animace příliš dlouho, aktualizuj stav hry bez vykreslení,   
       abychom dostali UPS blíže k požadované hodnotě FPS */
      int skips = 0;
      while((excess > period) && (skips < MAX_FRAME_SKIPS)) {
        excess -= period;
	    gameUpdate();    // aktualizuj stav, ale nevykresluj
        skips++;
      }
      framesSkipped += skips;

      storeStats();
	}
    finishOff();
  } // konec metody: metody: run()

  
  private void gameUpdate() 
  { if (!isPaused && !gameOver)
      fred.move();
  }  // konec metody: gameUpdate()


  private void screenUpdate()
  // použije aktivní vykreslování
  { try {
      gScr = bufferStrategy.getDrawGraphics();
      gameRender(gScr);
      gScr.dispose();
      if (!bufferStrategy.contentsLost())
        bufferStrategy.show();
      else
        System.out.println("Obsah bufferu byl ztracen");
      // v některých systémech musíme použít synchronizaci obrazovky.
      // (na Linuxu takto opravíme problémy s frontou událostí)
      Toolkit.getDefaultToolkit().sync();
    }
    catch (Exception e) 
    { e.printStackTrace();  
      running = false; 
    } 
  }  // konec metody: screenUpdate()



  private void gameRender(Graphics gScr)
  {
    // vyčisti pozadí
    gScr.setColor(Color.white);
    gScr.fillRect (0, 0, pWidth, pHeight);

	gScr.setColor(Color.blue);
    gScr.setFont(font);

    // vypiš počet snímků a prům. FPS a UPS do levého horního rohu
	// gScr.drawString("Frame Count " + frameCount, 10, 25);
	gScr.drawString("FPS/UPS průměrně: " + df.format(averageFPS) + "/" +
                                df.format(averageUPS), 20, 25);  // was (10,55)

    // ve spodní levé části vytiskni uplynulý čas a počet spotřeb. krabiček
    gScr.drawString("Celkový čas: " + timeSpentInGame + " sekund", 10, pHeight-15);
    gScr.drawString("Použitých krabiček: " + boxesUsed, 300, pHeight-15);

    // nakresli tlačítka pozastavení a ukončení
    drawButtons(gScr);

	gScr.setColor(Color.black);

    // nakresli prvky hry: překážky a červíka
    obs.draw(gScr);
    fred.draw(gScr);

    if (gameOver)
      gameOverMessage(gScr);
  }  // konec metody: metody: gameRender()


  private void drawButtons(Graphics g)
  {
    g.setColor(Color.black);

    // nakresli tlačítko pozastavení
    if (isOverPauseButton)
      g.setColor(Color.green);

    g.drawOval( pauseArea.x, pauseArea.y, pauseArea.width, pauseArea.height);
    if (isPaused)
      g.drawString("Pozastaveno", pauseArea.x+10, pauseArea.y+25);
    else
      g.drawString("Pozastav", pauseArea.x+30, pauseArea.y+25);

    if (isOverPauseButton)
      g.setColor(Color.black);


    // nakresli tlačítko ukončení
    if (isOverQuitButton)
      g.setColor(Color.green);

    g.drawOval(quitArea.x, quitArea.y, quitArea.width, quitArea.height);
    g.drawString("Konec", quitArea.x+50, quitArea.y+25);

    if (isOverQuitButton)
      g.setColor(Color.black);
  }  // drawButtons()


  private void gameOverMessage(Graphics g)
  // V panelu vycentruj zprávu o ukončení hry.
  {
    String msg = "Hra skončila. Dosažený počet bodů: " + score;
	int x = (pWidth - metrics.stringWidth(msg))/2; 
	int y = (pHeight - metrics.getHeight())/2;
	g.setColor(Color.red);
    g.setFont(font);
	g.drawString(msg, x, y);
  }  // konec metody: gameOverMessage()


  private void storeStats()
  /* Statistické údaje:
       - součet dob trvání všech dosud provedených iterací,
         skutečná doba trvání tohoto periodu,
         diff (odchylka) těchto dvou čísel

       - celkový počet volání metody paintComponent();
   
       - Počet snímků za sekundu v tomto mezidobí a průměrná
         hodnota FPS za posledních NUM_FPS periodů.

       - FPS (snímků za sekudnu) a UPS (aktualizací za sekundu) pro tento interval, 
         průměrné hodnoty FPS & UPS za posledních NUM_FPSs intervalů.

     Tyto informace ukládáme každých MAX_STATS_INTERVAL/1000 sekund.
  */
  { 
    frameCount++;
    statsInterval += period;

    // zaznamenej statistiky každých MAX_STATS_INTERVAL intervalů
    if (statsInterval >= MAX_STATS_INTERVAL) {    
      long timeNow = J3DTimer.getValue();
      timeSpentInGame = (int) ((timeNow - gameStartTime)/1000000000L);  // ns --> sekund

      long realElapsedTime = timeNow - prevStatsTime;   // čas od posledního shromažďování statistik
      totalElapsedTime += realElapsedTime;

      double timingError = 
         ((double)(realElapsedTime - statsInterval) / statsInterval) * 100.0;

      totalFramesSkipped += framesSkipped;

      double actualFPS = 0;     // vypočti nejnovější FPS u UPS
      double actualUPS = 0;
      if (totalElapsedTime > 0) {
        actualFPS = (((double)frameCount / totalElapsedTime) * 1000000000L);
        actualUPS = (((double)(frameCount + totalFramesSkipped) / totalElapsedTime) 
                                                             * 1000000000L);
      }

      // store the latest FPS and UPS
      fpsStore[ (int)statsCount%NUM_FPS ] = actualFPS;
      upsStore[ (int)statsCount%NUM_FPS ] = actualUPS;
      statsCount = statsCount+1;

      double totalFPS = 0.0;     // celkem uložených hodnot FPS a UPS
      double totalUPS = 0.0;
      for (int i=0; i < NUM_FPS; i++) {
        totalFPS += fpsStore[i];
        totalUPS += upsStore[i];
      }

      if (statsCount < NUM_FPS) { // vypočti průměrné hodnoty FPS a UPS
        averageFPS = totalFPS/statsCount;
        averageUPS = totalUPS/statsCount;
      }
      else {
        averageFPS = totalFPS/NUM_FPS;
        averageUPS = totalUPS/NUM_FPS;
      }
/*
      System.out.println(timedf.format( (double) statsInterval/1000000000L) + " " + 
                    timedf.format((double) realElapsedTime/1000000000L) + "s " + 
			        df.format(timingError) + "% " + 
                    frameCount + "v " +
                    framesSkipped + "/" + totalFramesSkipped + " vynecháno; " +
                    df.format(actualFPS) + " " + df.format(averageFPS) + " prům. fps; " + 
                    df.format(actualUPS) + " " + df.format(averageUPS) + " prům. ups" );
*/
      framesSkipped = 0;
      prevStatsTime = timeNow;
      statsInterval = 0L;   // vynuluj
    }
  }  // konec metody: metody: storeStats()


  private void finishOff()
  /* Činnosti, které je nutné provést před ukončením. Volá se na konci
     metody run() a přes zachycení pro ukončení v metodě readyForTermination().

     Volání na konci metody run() není úplně nutné, ale kdo ví.
     Příznak zastaví případné volání podruhé.
  */
  { // System.out.println("finishOff");
    if (!finishedOff) {
      finishedOff = true;
      printStats();
      restoreScreen();
      System.exit(0);
    }
  } // konec metody: finishedOff()


  private void printStats()
  {
    System.out.println("Snímků celkem/vynecháno: " + frameCount + " / " + totalFramesSkipped);
	System.out.println("FPS průměrně: " + df.format(averageFPS));
	System.out.println("UPS průměrně: " + df.format(averageUPS));
    System.out.println("Celkový čas: " + timeSpentInGame + " sekund");
    System.out.println("Použitých krabiček: " + boxesUsed);
  }  // konec metody: metody: printStats()



  private void restoreScreen()
  /* vypne celoobrazovkový režim. Dále také resetuje obrazový režim,
     pokud byl změněn.
  */
  { Window w = gd.getFullScreenWindow();
    if (w != null)
      w.dispose();
    gd.setFullScreenWindow(null);
  } // konec metody: restoreScreen()



  // ------------------ metody pro práci s obrazovým režimem -------------------


  private void setDisplayMode(int width, int height, int bitDepth)
  // pokusí se zobrazit zobrazovací režim dané šířky, výšky a bitové hloubky
  {
    if (!gd.isDisplayChangeSupported()) {
      System.out.println("Změna módu obrazovky není podmorována");
      return;
    }

    if (!isDisplayModeAvailable(width, height, bitDepth)) {
      System.out.println("Obrazový mód ("  + width + "," +
                              height + "," + bitDepth + ") není dostupný");
      return;
    }

    DisplayMode dm = new DisplayMode(width, height, bitDepth, 
                              DisplayMode.REFRESH_RATE_UNKNOWN);   // jakákoli obnov. frekv.
    try {
      gd.setDisplayMode(dm);
      System.out.println("Obrazový mód nastaven na: (" + width + "," +
                                         height + "," + bitDepth + ")");
    }
    catch (IllegalArgumentException e) 
    {  System.out.println("Error setting Obrazový mód ("  + width + "," +
                                              height + "," + bitDepth + ")");  }

    try {  // pospíme, abychom obrazovce poskytli trochu času na změnu módu
      Thread.sleep(1000);  // 1 sekunda
    }
    catch(InterruptedException ex){}
  }  // konec metody: setDisplayMode()



  private boolean isDisplayModeAvailable(int width, int height, int bitDepth)
  /* Ověří, že obrazový režim s šířkou width, výškou height a bitovou hloubkou
     bitDepth jsou k dispozici.
     O obnovovací frekvenci se zde nestaráme, se kterou se stejně asi
     pracuje přes REFRESH_RATE_UNKNOWN.
  */
  { DisplayMode[] modes = gd.getDisplayModes();
    showModes(modes);

    for(int i = 0; i < modes.length; i++) {
      if (width == modes[i].getWidth() && height == modes[i].getHeight() &&
          bitDepth == modes[i].getBitDepth())
        return true;
    }
    return false;
  }  // konec metody: isDisplayModeAvailable()


  private void showModes(DisplayMode[] modes)
  // pěkně vypíše údaje o obrazových režimech v poli modes
  {
    System.out.println("Dostupné obrazové módy");
    for(int i = 0; i < modes.length; i++) {
      System.out.print("(" + modes[i].getWidth() + "," +
                               modes[i].getHeight() + "," +
                               modes[i].getBitDepth() + "," +
                               modes[i].getRefreshRate() + ")  " );
      if ((i+1)%4 == 0)
        System.out.println();
    }
    System.out.println();
  }   // konec metody: showModes()


  private void showCurrentMode()
  // Zobrazí údaje o obrazovém režimu pro tuto grafickou kartu
  {
    DisplayMode dm = gd.getDisplayMode();
    System.out.println("Aktuální obrazový mód: (" + 
                           dm.getWidth() + "," + dm.getHeight() + "," +
                           dm.getBitDepth() + "," + dm.getRefreshRate() + ")  " );
  }


  // -----------------------------------------

  public static void main(String args[])
  {  
    int fps = DEFAULT_FPS;
    if (args.length != 0)
      fps = Integer.parseInt(args[0]);

    long period = (long) 1000.0/fps;
    System.out.println("fps: " + fps + "; period: " + period + " ms");
    new WormChase(period*1000000L);    // ms --> nanosekundy
  }  // konec metody: main()

}  // konec třídy: WormChase

