/*
    --------------------------------------------------------------------------------
      SM0L SN3K : A little something by Motleypuss!  Updated 2022.05.20! [315 LINES]
    --------------------------------------------------------------------------------
    Here  is a miniscule SNAKE game. Always  looking for ways to stick  self-written 
    SNAKE games on things, although this is the first instance in my life of getting 
    one  running  on 12 dot matrix devices driven by an 8-bit  microcontroller.  The 
    snake logic is quite interesting, because Arduino has no dynamic arrays.
    --------------------------------------------------------------------------------
    Use  the thumbstick to steer the snake.  It starts out as a teensy little  snake 
    (well,  more of a splotch, really). Running over a tablet (a dot) will  increase 
    the  snake's  length  by two dots. The goal is to win the game  by  achieving  a 
    length of 280 dots without your snake running into itself.
    --------------------------------------------------------------------------------
    Your  current score is printed on a  separate dot matrix unit above the 24 x  24 
    game  area  (576 total pixels). I'm not sure the score really  matters,  because 
    snakes don't care for scores, but it is being printed out for you anyway.
    --------------------------------------------------------------------------------
    The  game uses a 24 x 24 pixel-addressable  dot matrix display rig for the  game 
    area. This rig should be assembled from  nine 8 x 8 dot matrix devices, arranged 
    as  three  rows  of three chained devices. An additional 3 x  1  arrangement  of 
    chained  dot matrix devices allows  the  score to be printed. So the game  needs 
    twelve 8 x 8 dot matrix devices in total, plus a thumbstick.
    --------------------------------------------------------------------------------
*/
#include <MD_MAX72xx.h>
/*
    This is all stuff relating to the display and input hardware. Yes, this is 
    basically a really tiny game console.
*/
int C_PINS[]={13,10,7,4};int D_PINS[]={12,8,5,2};int S_PINS[]={11,9,6,3};
#define DEVICES_PER_ROW        3  /* Number of 8x8s per row. Cols are inferred. */
#define VERT_PIN              A0  /* Thumbstick H pin. */
#define HORZ_PIN              A1  /* Thumbstick V pin. */
#define SEL_PIN                0  /* Thumbstick SEL pin. */
#define JOY_CENTRE_POINT     512  /* Thumbstick centre point. */
#define JOY_DEAD_ZONE        100  /* Thumbstick deadzone, measured in whatevers. */
MD_MAX72XX mx1 = MD_MAX72XX(MD_MAX72XX::PAROLA_HW,D_PINS[0],C_PINS[0],S_PINS[0],DEVICES_PER_ROW);
MD_MAX72XX mx2 = MD_MAX72XX(MD_MAX72XX::PAROLA_HW,D_PINS[1],C_PINS[1],S_PINS[1],DEVICES_PER_ROW);
MD_MAX72XX mx3 = MD_MAX72XX(MD_MAX72XX::PAROLA_HW,D_PINS[2],C_PINS[2],S_PINS[2],DEVICES_PER_ROW);
MD_MAX72XX mx4 = MD_MAX72XX(MD_MAX72XX::PAROLA_HW,D_PINS[3],C_PINS[3],S_PINS[3],DEVICES_PER_ROW);
#define	MAX_PIXEL             ((8*DEVICES_PER_ROW)-1)       /* Max pixel co-ord). */
#define PIXELS                (MAX_PIXEL+1)*(MAX_PIXEL*1)   /* Pixel count. */
/*
    These are variables relating to the game.
*/
#define MAX_TABLETS          max((PIXELS/50),1) /* Max edibles (11). */
#define MAX_SNAKE_SEGS       (PIXELS/2)  /* Max snake segments (288). */
#define GAME_TICK_INTERVAL   100 /* Game timer interval. */
#define JOY_READ_INTERVAL    10  /* Thumbstick read interval. */
#define TABLET_SCORE           2 /* Point value of an edible. */
#define GROWTH                 2 /* How many dots snake grows when it eats. */
int TABLETS[MAX_TABLETS*2] ={0}; /* Edibles. All values are in pairs. */
int SNAKE[MAX_SNAKE_SEGS*2]={0}; /* Snake segments. All values are in pairs. */
int SEGS=0;int DLEN=0;int MX=0;  /* */
int MY=0;int SCORE=0;            /* Snake control vars. */
const int buzzerPin = 1;
String lastscore="";             /* */
/*
    This code is called when the microcontroller resets.
*/
void setup() {
    mx1.begin();mx1.control(MD_MAX72XX::INTENSITY,MAX_INTENSITY/2);mx1.clear();
    mx2.begin();mx2.control(MD_MAX72XX::INTENSITY,MAX_INTENSITY/2);mx2.clear();
    mx3.begin();mx3.control(MD_MAX72XX::INTENSITY,MAX_INTENSITY/2);mx3.clear();
    mx4.begin();mx1.control(MD_MAX72XX::INTENSITY,MAX_INTENSITY/2);mx4.clear();
    pinMode(VERT_PIN,INPUT);pinMode(HORZ_PIN,INPUT);pinMode(SEL_PIN,INPUT_PULLUP);
    pinMode(buzzerPin, OUTPUT);
    GameReset();
}
/*
    Here are clock functions. These control how fast different parts of the game 
    code can runs. Call ClockIncTimeGates() at the start of loop(). Anywhere
    in loop(), you can call ClockTestTimeGate() to see if a particular interval of 
    time has elapsed. The lengths of CTIME and PTIME determine how many time gates 
    can be tested against. SM0L SN3K tests against two time gates.
*/
unsigned long CTIME[8]={0,0}; // space for 2 gates (thumbstick and game)
unsigned long PTIME[8]={0,0};
void ClockReset(){for(int i=0;i<8;i++){CTIME[i]=millis();PTIME[i]=0;}}
void ClockIncTimeGates(){for(int i=0;i<8;i++){CTIME[i]=millis();}}
int ClockTestTimeGate(int gate,int desired_interval){if ((CTIME[gate]-PTIME[gate])
>=desired_interval){PTIME[gate]=CTIME[gate];return true;}return false;}
/*
    Here are some convenience functions for working with four chained dot matrix 
    sections and for handling display wrapping of pixel positions.
*/
void Clear(){mx1.clear();mx2.clear();mx3.clear();mx4.clear();}
void Done(){mx1.update();mx2.update();mx3.update();mx4.update();}
int  Clamp(int n){if(n<0){n=MAX_PIXEL+1+n;}if(n>MAX_PIXEL){n=MAX_PIXEL+1-n;}return n;}
void DrawSnake(){int i=0;while(i<SEGS){Plot(SNAKE[i*2+0],SNAKE[i*2+1],true);i++;}}
/*
    This code plots a pixel to one of the bottom three dot matrix rows. Which 
    device is selected depends on the Y co-ordinate. This is quite expandable.
*/
void Plot(int x,int y,int on){
    if((x<0)||(x>MAX_PIXEL)||(y<0)||(y>MAX_PIXEL)){return;} // prevent bad draws
    if(y>15){mx3.setPoint(y-16,MAX_PIXEL-x,on);}else
    if(y>7) {mx2.setPoint(y-8,MAX_PIXEL-x,on);}else
            {mx1.setPoint(y,MAX_PIXEL-x,on);}               // can be expanded
}
/*
    This code plots a pixel to the top dot matrix row. This row is used exclusively 
    for displaying the score and whatever else I can come up with.
*/
void PSB(int x,int y){
    if((x<0)||(x>MAX_PIXEL)||(y<0)||(y>7)){return;} // prevent bad draws
    mx4.setPoint(y,MAX_PIXEL-x,true);
}
/*
    This code plots a numeric character to the top dot matrix row using the helper 
    function PSB() which takes care of pixel addressing.
*/
int PlotScoreboardCharacter(int x,int y,String c){
  if(c=="0"){PSB(x+1,y+0);PSB(x+0,y+1);PSB(x+2,y+1);PSB(x+0,y+2);PSB(x+2,y+2);
  PSB(x+0,y+3);PSB(x+2,y+3);PSB(x+1,y+4);return 4;}
  if(c=="1"){PSB(x+0,y+0);PSB(x+0,y+1);PSB(x+0,y+2);PSB(x+0,y+3);PSB(x+0,y+4);
  return 2;}
  if(c=="2"){PSB(x+0,y+0);PSB(x+1,y+0);PSB(x+2,y+1);PSB(x+1,y+2);PSB(x+0,y+3);
  PSB(x+0,y+4);PSB(x+1,y+4);PSB(x+2,y+4);return 4;}
  if(c=="3"){PSB(x+0,y+0);PSB(x+1,y+0);PSB(x+2,y+1);PSB(x+1,y+2);PSB(x+2,y+3);
  PSB(x+0,y+4);PSB(x+1,y+4);return 4;}
  if(c=="4"){PSB(x+0,y+0);PSB(x+2,y+0);PSB(x+0,y+1);PSB(x+2,y+1);PSB(x+1,y+2);
  PSB(x+2,y+2);PSB(x+2,y+3);PSB(x+2,y+4);return 4;}
  if(c=="5"){PSB(x+0,y+0);PSB(x+1,y+0);PSB(x+2,y+0);PSB(x+0,y+1);PSB(x+0,y+2);
  PSB(x+1,y+2);PSB(x+2,y+2);PSB(x+2,y+3);PSB(x+0,y+4);PSB(x+1,y+4);PSB(x+2,y+4);
  return 4;}
  if(c=="6"){PSB(x+0,y+0);PSB(x+0,y+1);PSB(x+0,y+2);PSB(x+1,y+2);
  PSB(x+0,y+3);PSB(x+2,y+3);PSB(x+1,y+4);return 4;}
  if(c=="7"){PSB(x+0,y+0);PSB(x+1,y+0);PSB(x+2,y+0);PSB(x+2,y+1);PSB(x+2,y+2);
  PSB(x+2,y+3);PSB(x+2,y+4);return 4;}
  if(c=="8"){PSB(x+1,y+0);PSB(x+0,y+1);PSB(x+2,y+1);PSB(x+1,y+2);PSB(x+0,y+3);
  PSB(x+2,y+3);PSB(x+1,y+4);return 4;}
  if(c=="9"){PSB(x+0,y+0);PSB(x+1,y+0);PSB(x+2,y+0);PSB(x+0,y+1);PSB(x+2,y+1);
  PSB(x+0,y+2);PSB(x+1,y+2);PSB(x+2,y+2);PSB(x+2,y+3);PSB(x+2,y+4);return 4;}
  if(c=="S"){PSB(x+1,y+0);PSB(x+2,y+0);PSB(x+0,y+1);PSB(x+1,y+2);PSB(x+2,y+3);
  PSB(x+0,y+4);PSB(x+1,y+4);return 4;}
  if(c=="M"){PSB(x+0,y+0);PSB(x+2,y+0);PSB(x+0,y+1);PSB(x+1,y+1);PSB(x+2,y+1);
  PSB(x+0,y+2);PSB(x+1,y+2);PSB(x+2,y+2);PSB(x+0,y+3);PSB(x+2,y+3);PSB(x+0,y+4);
  PSB(x+2,y+4);return 4;}
  if(c=="N"){PSB(x+0,y+0);PSB(x+1,y+0);PSB(x+0,y+1);PSB(x+2,y+1);PSB(x+0,y+2);
  PSB(x+2,y+2);PSB(x+0,y+3);PSB(x+2,y+3);PSB(x+0,y+4);PSB(x+2,y+4);return 4;}
  if(c=="L"){PSB(x+0,y+0);PSB(x+0,y+1);PSB(x+0,y+2);PSB(x+0,y+3);PSB(x+0,y+4);
  PSB(x+1,y+4);PSB(x+2,y+4);return 4;}
  if(c=="K"){PSB(x+0,y+0);PSB(x+0,y+1);PSB(x+2,y+1);PSB(x+0,y+2);PSB(x+1,y+2);
  PSB(x+0,y+3);PSB(x+2,y+3);PSB(x+0,y+4);PSB(x+2,y+4);return 4;}
  return 0;
}
/*
    This code plots the score or logo to the top dot matrix row.
*/
void PrintText(String sc){
    if(sc!=lastscore){mx4.clear();}else{return;}
    lastscore=sc;int xpos=1;int ypos=1;
    for(int i=0;i<sc.length();i++){char y=sc.charAt(i);
    xpos+=PlotScoreboardCharacter(xpos,ypos,String(y));}
}
/*
    This code plots tables on the bottom three dot matrix rows.
*/
void DrawTablets(){
    int i=0;int len=sizeof(TABLETS)/sizeof(TABLETS[0])/2;
    while(i<len){
        if((TABLETS[i*2+0]>-1)&&(TABLETS[i*2+1]>-1)){
            Plot(TABLETS[i*2+0],TABLETS[i*2+1],true);
        }
        i++;
    }
}
/*
    This code manages game initialisation and resets.
*/
void GameReset(){
    PrintText("SM0L");delay(500);PrintText("SN3K");delay(500);
    digitalWrite(buzzerPin, HIGH);
    delay(500); // Buzzer menyala selama 0.5 detik
    digitalWrite(buzzerPin, LOW);
    InitSnake();InitTablets();Clear();SCORE=0;ClockReset();
}
void GameLost(){
    delay(1000);GameReset();
}
void GameWon(){
    delay(1000);GameReset();
}
/*
    This code generates MAX_TABLETS tablets for the snake to eat. We try to 
    avoid placing the new tablet either on the snake or on an existing tablet, 
    and there's a sentinel value to prevent an infinite freeze in case where 
    no tablet can be placed.
*/
void InitTablets(){
    randomSeed(analogRead(A3));
    for(int i=0;i<(MAX_TABLETS*2);i++){TABLETS[i]=-1;}
    int tx=0;int ty=0;int tries=0;for(int i=0;i<MAX_TABLETS;i++){
      tx=random(0,MAX_PIXEL);ty=random(0,MAX_PIXEL);tries=0;
      while(CheckTabletOnSnake(tx,ty)||(CheckTabletOnTablet(tx,ty))){
        tx=random(0,MAX_PIXEL);ty=random(0,MAX_PIXEL);
        if(tries>=255){GameLost();return;} // whoops!
      }
      TABLETS[i*2+0]=tx;TABLETS[i*2+1]=ty;
    }
}
/*
    This code initialises the snake with MAX_SNAKE_SEGS total length and 
    an actual length of SEGS. By adjusting DLEN, we can grow the snake.
*/
void InitSnake(){
    for(int i=0;i<(MAX_SNAKE_SEGS*2);i++){SNAKE[i]=-1;}
    SNAKE[0]=11;SNAKE[1]=11;SEGS=1;DLEN=4;MY=-1;MX=0;lastscore=98760978;
}
/*
    This code checks to see if desired tablet position TX,TY would end 
    up hitting the snake.
*/
int CheckTabletOnSnake(int tx,int ty){
 int i=0;while(i<SEGS){
     if((SNAKE[i*2+0]>-1)&&(SNAKE[i*2+1]>-1)){          // ignore NULLs
       if((SNAKE[i*2+0]==tx)&&(SNAKE[i*2+1]==ty)){      // collision?
          return true;                                  // game lose!
       }
     }
     i++;
 }
 return false;
}
/*
    This code checks to see if desired tablet position TX,TY would end 
    up hitting an existing tablet.
*/
int CheckTabletOnTablet(int tx,int ty){
 int i=0;while(i<MAX_TABLETS){
     if((TABLETS[i*2+0]>-1)&&(TABLETS[i*2+1]>-1)){      // ignore NULLs
       if((TABLETS[i*2+0]==tx)&&(TABLETS[i*2+1]==ty)){  // collision?
          return true;                                  // game lose!
       }
     }
     i++;
 }
 return false;
}
/*
    This code checks to see if snake self-collision has occurred. If it has, 
    the game calls GameLost() and resets.
*/
void CheckSnakeSelfCollide(){
 int hX=SNAKE[(SEGS-1)*2+0];int hY=SNAKE[(SEGS-1)*2+1]; // get snake head position
 int i=0;while(i<SEGS-1){
     if((SNAKE[i*2+0]>-1)&&(SNAKE[i*2+1]>-1)){          // ignore NULLs
       if((SNAKE[i*2+0]==hX)&&(SNAKE[i*2+1]==hY)){      // collision?
          GameLost();return;                            // game lose!
       }
     }
     i++;
 }
}
/*
    This code checks to see if a tablet has been eaten by the snake. If one 
    has, a new random tablet is generated. We try to avoid placing the new
    tablet either on the snake or on an existing tablet, and there's a 
    sentinel value to prevent an infinite freeze in case where no tablet 
    can be placed.
*/
void TryEatTablet(){
 int len=sizeof(TABLETS)/sizeof(TABLETS[0])/2;int i=0;int tx;int ty;
 int hX=SNAKE[(SEGS-1)*2+0];int hY=SNAKE[(SEGS-1)*2+1]; // get snake head position
 int tries;
 while(i<len){
     if((TABLETS[i*2+0]>-1)&&(TABLETS[i*2+1]>-1)){                  
       if((TABLETS[i*2+0]==hX)&&(TABLETS[i*2+1]==hY)){ 
         Plot(TABLETS[i*2+0],TABLETS[i*2+1],false);
         TABLETS[i*2+0]=-1;TABLETS[i*2+1]=-1;
         tx=random(0,MAX_PIXEL);ty=random(0,MAX_PIXEL);tries=0;
         while(CheckTabletOnSnake(tx,ty)||(CheckTabletOnTablet(tx,ty))){
             tx=random(0,MAX_PIXEL);ty=random(0,MAX_PIXEL);
             if(tries>=255){GameLost();return;} // whoops!
         }
         TABLETS[i*2+0]=tx;TABLETS[i*2+1]=ty;        
         DLEN+=GROWTH;SCORE+=TABLET_SCORE;
       }
     }
     i++;
 }
}
/*
    This code moves the snake, and also handles snake growth. This routine is a 
    pretty big deviation from my 'real' SNAKE games, as the Arduino doesn't have 
    dynamic lists as such that can be easily used.
*/
void MoveSnake(){
    int hX=SNAKE[(SEGS-1)*2+0];int hY=SNAKE[(SEGS-1)*2+1];// get snake head position
    int tX=SNAKE[0];int tY=SNAKE[1];                      // get snake tail position
    if(DLEN>SEGS){SEGS++;}                                // grow snake?
    if(SEGS>MAX_SNAKE_SEGS){GameWon();return;}            // snake is too big
    Plot(tX,tY,false);                                    // unplot tail pixel
    hX+=MX;hY+=MY;                                        // shift position of head
    int q=0;while(q<SEGS){                                // 
      SNAKE[(q*2)+0]=SNAKE[(q*2)+2];                      //   shift snake segments 
      SNAKE[(q*2)+1]=SNAKE[(q*2)+3];                      //   
      q++;                                                //   
    }                                                     //
    SNAKE[(SEGS-1)*2+0]=Clamp(hX);SNAKE[(SEGS-1)*2+1]=Clamp(hY); // replace head
}
/*
    This code is called every tick. We're using millis() to avoid using delay().
    This gives better thumbstick responsiveness. [2022.05.20]
*/
void loop() {
  ClockIncTimeGates();
  if (ClockTestTimeGate(0,GAME_TICK_INTERVAL)){
    PrintText(String(SCORE));MoveSnake();TryEatTablet();DrawSnake();
    DrawTablets();CheckSnakeSelfCollide();Done();
  }
  if (ClockTestTimeGate(1,JOY_READ_INTERVAL)){
    int horz=analogRead(HORZ_PIN);int vert=analogRead(VERT_PIN);
    if(horz<(JOY_CENTRE_POINT-JOY_DEAD_ZONE)){MX=1;MY=0;}else 
    if(horz>(JOY_CENTRE_POINT+JOY_DEAD_ZONE)){MX=-1;MY=0;}else
    if(vert<(JOY_CENTRE_POINT-JOY_DEAD_ZONE)){MX=0;MY=1;}else 
    if(vert>(JOY_CENTRE_POINT+JOY_DEAD_ZONE)){MX=0;MY=-1;}
  }
}