
/*

  SNAKE

  by matty riek.

*/

global showMemoryUsage = false;

CURSOR(0,0);

global g_sx = 80;
global g_sy = 24;
global g_border =

"\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205"
"\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205"
"\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205\205";

global LocFromXY = function(a_x, a_y) { return a_y * g_sx + a_x; };
global XFromLoc = function(a_loc) { return a_loc % g_sx; };
global YFromLoc = function(a_loc) { return a_loc / g_sx; };
global background =  CA.B_BLUE;

global fade0 = table(  CA.F_RED|CA.F_BLUE|CA.F_GREEN | CA.F_INTENSITY | background,
                CA.F_GREEN | CA.F_INTENSITY | CA.F_INTENSITY | background,
                CA.F_BLUE | background,
                CA.F_GREEN | CA.F_INTENSITY | background,
                CA.F_RED|CA.F_GREEN | CA.F_INTENSITY | background,
                CA.F_RED|CA.F_BLUE|CA.F_GREEN | CA.F_INTENSITY | background,
                CA.F_BLUE | CA.F_INTENSITY | background,
                CA.F_BLUE | CA.F_INTENSITY | background,
                CA.F_RED|CA.F_BLUE|CA.F_GREEN | background,
                CA.F_BLUE | background,
                CA.F_BLUE | CA.F_INTENSITY | background,
                CA.F_RED|CA.F_GREEN | CA.F_INTENSITY | background  );

global fade1 = table( CA.F_RED|CA.F_BLUE|CA.F_GREEN | CA.F_INTENSITY | background,
                CA.F_RED|CA.F_BLUE|CA.F_GREEN | CA.F_INTENSITY | background,
                CA.F_RED|CA.F_BLUE|CA.F_GREEN | CA.F_INTENSITY | background,
                CA.F_RED|CA.F_GREEN | CA.F_INTENSITY | background,
                CA.F_RED | CA.F_INTENSITY | background,
                CA.F_RED | background,
                CA.F_RED | CA.F_INTENSITY | background,
                CA.F_RED|CA.F_GREEN | CA.F_INTENSITY  | background );

global fadeRed = table( CA.F_RED|CA.F_INTENSITY | background , CA.F_RED | background);

///////////////////////////////////////////////////////////////////////////////////////////////////
//
// CreateGemType
//

global CreateGemType = function(a_char, a_time, a_score, a_chance, a_length, a_cycle, a_pickup, a_mover)
{
  gem = table(m_char = a_char, m_time = a_time * 1000, m_score = a_score, m_chance = a_chance, m_length = a_length, m_cycle = a_cycle, m_mover = a_mover);

  gem.Pickup = function()
  {
  };

  if(a_pickup)
  {
    gem.Pickup = a_pickup;
  }

  gem.i = 0;
  gem.Draw = function(x, y)
  {
    .i = .i + 1;
    if(.i >= tableCount(.m_cycle))
    {
      .i = 0;
    }
    CATTRIB(.m_cycle[.i]);
    XYTEXT(x, y, .m_char);
    CATTRIB(background | CA.F_GREEN | CA.F_INTENSITY);
  };
  return gem;
};

///////////////////////////////////////////////////////////////////////////////////////////////////
//
// DrawScreen
//
global DrawScreen = function()
{
  //
  CATTRIB(background | CA.F_GREEN | CA.F_INTENSITY);

  // clear screen
  CLS();

  // draw border
  XYTEXT(1, 0, g_border);
  XYTEXT(1, g_sy - 1, g_border);

  ey = g_sy - 1;
  for(i = 1; i < ey; i = i + 1)
  {
    XYTEXT(0, i, "\186");
    XYTEXT(g_sx - 1, i, "\186");
  }
  XYTEXT(0, 0, "\201");
  XYTEXT(g_sx - 1, 0, "\187");
  XYTEXT(0, ey, "\200");
  XYTEXT(g_sx - 1, ey, "\188");

};

///////////////////////////////////////////////////////////////////////////////////////////////////
//
// EmitGems
//
EmitGems = function(a_game)
{
  // add a gem
  for(;;)
  {
    // choose a random sleep time
    sleep( randfloat(
          a_game.m_levels[a_game.m_level][1] / a_game.m_gameSpeed,
          a_game.m_levels[a_game.m_level][2] / a_game.m_gameSpeed)
          );

    // pick a random location
    x = randint(1, g_sx - 2);
    y = randint(1, g_sy - 2);
    loc = LocFromXY(x, y);

    // test if the location is on a existing gem or snake
    if(a_game.m_gems[loc])
    {
      continue;
    }
    onsnake = false;
    foreach(snake in a_game.m_snakes)
    {
      if(snake.IsAt(loc))
      {
        onsnake = true;
        break;
      }
    }
    if(onsnake) { continue; }

    // choose a gem
    found = false;
    while(!found)
    {
      foreach(gem in a_game.m_gemTypes)
      {
        if(randint(0, 100) < gem.m_chance)
        {
          found = true;
          break;
        }
      }
    }

    // add the gem
    gemInstance = table(m_gem = gem, m_expire = sysTime() + (gem.m_time / a_game.m_gameSpeed));
    a_game.m_gems[loc] = gemInstance;
    if(gem.m_mover)
    {
      gemInstance.m_dirX = randint(-1, 2);
      gemInstance.m_dirY = randint(-1, 2);
    }

    // draw the gem
    gem.Draw(x, y);
  }
};

///////////////////////////////////////////////////////////////////////////////////////////////////
//
// ReclaimGems
//
ReclaimGems = function(a_game)
{
  // add a gem
  for(;;)
  {
    time = sysTime();

    foreach(key and gem in a_game.m_gems)
    {
      if(time > gem.m_expire)
      {
        // remove from screen
        XYTEXT(XFromLoc(key), YFromLoc(key), " ");
        a_game.m_gems[key] = null;
        b = true;
        break;
      }
    }

    yield();
  }
};

///////////////////////////////////////////////////////////////////////////////////////////////////
//
// DrawGems
//
DrawGems = function(a_game)
{
  for(;;)
  {
    foreach(key and gem in a_game.m_gems)
    {
      x = XFromLoc(key);
      y = YFromLoc(key);
      gem.m_gem.Draw(x, y);
    }
    sleep(1 / 30.);
  }
};

///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Move mover gems
//
MoveMovers = function(a_game)
{
  for(;;)
  {
    movers = table();
    foreach(key and gem in a_game.m_gems)
    {
      if(gem.m_gem.m_mover)
      {
        x = XFromLoc(key);
        y = YFromLoc(key);
        
        XYTEXT(x, y, " ");

        x = x + gem.m_dirX;
        y = y + gem.m_dirY;

        if(x <= 1 or x >= g_sx - 2) { gem.m_dirX = -gem.m_dirX;  }
        if(y <= 1 or y >= g_sy - 2) { gem.m_dirY = -gem.m_dirY;  }
        gem.m_newLoc = LocFromXY(x, y);
        movers[key] = gem;
      }
    }
    
    foreach(key and gem in movers)
    {
      a_game.m_gems[key] = null;
      a_game.m_gems[gem.m_newLoc] = gem;
    }
    sleep(1 / 4.);
  }
};


///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Snake
//
Snake = function(a_id, a_name, a_score, a_scoreX, a_scoreY, a_headChar, a_tailChar, a_startX, a_startY, a_startDir, a_keys)
{
  snake = table(
    m_id = a_id,
    m_grow = 2,
    m_credits = 3,
    m_startX = a_startX,
    m_startY = a_startY,
    m_startDir = a_startDir,
    m_name = a_name,
    m_score = a_score,
    m_scoreX = a_scoreX,
    m_scoreY = a_scoreY,
    m_body = array(50),
    m_length = 0,
    m_dir = a_startDir, // 0 right, 1 up, 2 left, 3 down
    m_head = table(
      m_headChar = a_headChar,
      m_tailChar = a_tailChar,
      m_x = a_startX,
      m_y = a_startY
    ),
    m_keys = a_keys
  );

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  //
  // Die
  //
  snake.Die = function() // return true if snake has no credits
  {
    // flash a death thinggy

    i = 10;
    while(i)
    {
      if(i & 1) {
        XYTEXT(.m_scoreX, .m_scoreY, format(" ***  FATALITY ***  ", .m_name));
      }
      else {
        XYTEXT(.m_scoreX, .m_scoreY, format("                    ", .m_name));
      }
      sleep(0.2f);
      i = i - 1;
    }

    .UpdateScore();

    // clear the snake
    for(i = 0; i < .m_length; i = i + 1)
    {
      loc = .m_body[i];
      x = XFromLoc(loc);
      y = YFromLoc(loc);
      XYTEXT(x, y, " ");
    }
    XYTEXT(.m_head.m_x, .m_head.m_y, " ");

    // update the credits
    .m_credits = .m_credits - 1;
    if(.m_credits)
    {
      .m_head.m_x = .m_startX;
      .m_head.m_y = .m_startY;
      .m_dir = .m_startDir;
      .m_grow = 2;
      .m_length = 0;
      return false;
    }
    return true;
  };

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  //
  // UpdateScore
  //
  snake.UpdateScore = function()
  {
    XYTEXT(.m_scoreX, .m_scoreY, format(" %d UP %s SCORE %d  ", .m_credits, .m_name, .m_score));
  };

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  //
  // Draw
  //
  snake.Draw = function(a_oldX, a_oldY)
  {
    // clear the tail.
    if(.m_length > 0 and .grow == null)
    {
      loc = .m_body[.m_length - 1];
      y = YFromLoc(loc);
      x = XFromLoc(loc);
      XYTEXT(x, y, " ");
    }

    // grow the snake
    if(.grow)
    {
      .m_length = .m_length + 1;
      if(.m_length >= .m_body.Size())
      {
        .m_body.Resize(.m_body.Size() * 2);
      }
      .grow = false;
    }

    // move the body along.
    for(i = .m_length - 1; i > 0; i = i - 1)
    {
      .m_body[i] = .m_body[i - 1];
    }

    // put the new tail piece in
    if(.m_length)
    {
      .m_body[0] = LocFromXY(a_oldX, a_oldY);
      XYTEXT(a_oldX, a_oldY, .m_head.m_tailChar);
    }
    else
    {
      XYTEXT(a_oldX, a_oldY, " ");
    }

    // draw the new head
    XYTEXT(.m_head.m_x, .m_head.m_y, .m_head.m_headChar);
  };

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  //
  // IsAt
  //
  snake.IsAt = function(a_loc)
  {
    for(i = 0; i < .m_length; i = i + 1)
    {
      if(a_loc == .m_body[i])
      {
        return true;
      }
    }
    if(a_loc == LocFromXY(.m_head.m_x, .m_head.m_y))
    {
      return true;
    }
    return false;
  };

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  //
  // Update
  //
  snake.Update = function(a_game)
  {
    member UpdateScore;

    UpdateScore();
    for(;;)
    {
      wait = 1.0f / (a_game.m_snakeMoveRate * a_game.m_gameSpeed);
      sleep(wait);

      x = .m_head.m_x;
      y = .m_head.m_y;

      if(.m_dir == 0) { x = x + 1; }
      else if(.m_dir == 1) { y = y - 1; }
      else if(.m_dir == 2) { x = x - 1; }
      else if(.m_dir == 3) { y = y + 1; }

      // did we collect a gem?
      loc = LocFromXY(x, y);
      gem = a_game.m_gems[loc];
      if(gem)
      {
        // update score
        .m_score = .m_score + gem.m_gem.m_score;
        .m_grow = gem.m_gem.m_length;
        pickup = gem.m_gem.Pickup;
        this:pickup();
        UpdateScore();

        // remove the gem
        a_game.m_gems[loc] = null;
      }

      // did we run into a wall??
      dead = false;
      if(x <= 0 or x >= g_sx - 1 or y <= 0 or y >= g_sy - 1)
      {
        dead = true;
      }
      
      // did we run into ourselves?
      if(!dead and .IsAt(loc))
      {
        dead = true;
      }

      // update position
      oldX = .m_head.m_x;
      oldY = .m_head.m_y;
      .m_head.m_x = x;
      .m_head.m_y = y;

      // did we run into another snake
      if(!dead)
      {
        foreach(snake in a_game.m_snakes)
        {
          if(snake != this and snake.IsAt(loc))
          {
            dead = true;
            break;
          }
        }
      }

      // did we die?
      if(dead)
      {
        .m_head.m_x = oldX;
        .m_head.m_y = oldY;
        die = .Die();
        .UpdateScore();
        if(die)
        {
          a_game.m_snakes[.m_id] = null;
          threadKill(.kht);
          exit();
        }
      }
      .Draw(oldX, oldY);
      
      if(.m_grow)
      {
        .grow = true;
        .m_grow = .m_grow - 1;
      }
    }
  };
  
  ///////////////////////////////////////////////////////////////////////////////////////////////////
  //
  // KeyHandler
  //
  snake.KeyHandler = function(a_game)
  {
    wait = 1.0f / a_game.m_keyUpdateRate;
    for(;;)
    {
      for(i = 0; i < 4; i = i + 1)
      {
        if(ISPRESSED(.m_keys[i]))
        {
          .m_dir = i;
          break;
        }
      }
      sleep(wait);
    }
  };

  return snake;
};

///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Game
//
g_game = table(

  // goodies
  m_gems = table(),
  
  m_levels = table(
  
  // level is speed, lower spawn time, upper spawn time, next level score
    table(1.0f, 10, 20, 500),
    table(2.0f, 8, 18, 1500),
    table(2.5f, 5, 16, 2700),
    table(3.0f, 4, 13, 3800),
    table(3.5f, 3, 10, 5000)
  ),

  m_gemTypes = table(

    CreateGemType("\4", 22, 50, 40, 3, fade0), // standard 1
    CreateGemType("\5", 17, 100, 30, 4, fade0), // standard 2
    CreateGemType("\6", 15, 200, 20, 5, fade1), // standard 3
    CreateGemType("\3", 30, 20, 15, 0, fadeRed, function() { .m_credits = .m_credits + 1; }, true),  // 1up
    CreateGemType("\15", 45, 0, 15, 50, fade1) // bomb
    ),

  // snakes
  m_snakes = table(
    Snake(0, "lefty", 0, 8, 23, "\1", "\176", 1, 12, 0, table('D', 'W', 'A', 'S')),
    Snake(1, "righty", 0, 50, 23, "\2", "\177", 78, 12, 2, table('L', 'I', 'J', 'K'))
    ),

  // game
  m_level = 0,
  m_over = false,
  m_totalScore = 0,

  // timing
  m_speedup = 2.0f, // overall speed multiplier
  m_gameSpeed = 1.0f, // current game speed
  m_keyUpdateRate = 30.0f, // times per second
  m_snakeMoveRate = 4.0f // times per second
);

///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Main
//
ScoreMonitor = function()
{
  .m_gameSpeed = .m_speedup * .m_levels[.m_level][0];

  for(;;)
  {
    // update scores and levels.
    .m_totalScore = 0;
    foreach(snake in .m_snakes)
    {
      .m_totalScore = .m_totalScore + snake.m_score;
    }

    // are they at the next level
    if(.m_totalScore >= .m_levels[.m_level][3])
    {
      // level up.... have we finished
      .m_level = .m_level + 1;
      if(.m_level >= tableCount(.m_levels))
      {
        // you win....
        XYTEXT(0,0,"YOUWIN");
        sleep(5);
        .gameover = true;
      }
      else
      {
        // next level
        .m_gameSpeed = .m_speedup * .m_levels[.m_level][0];
      }
    }

    XYTEXT(8, 0, format("LEVEL %d, SCORE %d, NEXT LEVEL TARGET %d", .m_level, .m_totalScore, .m_levels[.m_level][3]));
    if(showMemoryUsage)
    {
      XYTEXT(8, 1, format("mem %d Desired H: %d S: %d F: %d I: %d W: %d",
                           sysGetMemoryUsage(), 
                           sysGetDesiredMemoryUsageHard(), 
                           sysGetDesiredMemoryUsageSoft(),
                           sysGetStatsGCNumFullCollects(),
                           sysGetStatsGCNumIncCollects(),
                           sysGetStatsGCNumWarnings() ));
    }
    sleep(0.1);
  }
};

///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Title Screen
//

// todo


///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Main
//

DrawScreen();

// start the snakes
foreach(snake in g_game.m_snakes)
{
  snake.kht = snake:thread(snake.KeyHandler, g_game);
  snake.ut = snake:thread(snake.Update, g_game);
}

// start the gems
egt = thread(EmitGems, g_game);
rgt = thread(ReclaimGems, g_game);
dgt = thread(DrawGems, g_game);
mmt = thread(MoveMovers, g_game);
smt = g_game:thread(ScoreMonitor);

for(;;)
{
  // test for escape key
  if(g_game.gameover or ISPRESSED(27))
  {
    // kill threads
    threadKill(egt);
    threadKill(rgt);
    threadKill(dgt);
    threadKill(smt);
    threadKill(mmt);

    foreach(snake in g_game.m_snakes)
    {
      threadKill(snake.kht);
      threadKill(snake.ut);
    }
    exit();
  }
  yield();
}




















