Midterm – Adventures of Kathakali and Theyyam

The game that I made for the midterm project was definitely a product of a lot of ideas and concepts that were discussed in the class – not just pertaining to the environment of processing itself. As a CS major, I had done a bit of Processing for my Intro to CS class, but this time around, I approached making a game on Processing differently – and the biggest difference was I was actively thinking about whether the game was designed to make sense to a player – whereas back then I was concentrating on achieving functionality.

My game, in a nutshell, can be thought of as snake and ladders but instead of the snakes and ladders, there are cards that describe the number of steps you move back and forth. The theme I chose was my state in India – Kerala and it can be seen in the tokens – Theyyam and Kathakali (both of which are art forms in Kerala) and the cards. The cards are things that are related to Kerala. Here are the cards:

Implementation

 

I took a step-by-step approach and broke down the game into smaller units and then tied them together. In the last week, I worked on the rolling die, the grid, and the tokens. This week, I started on improving the design of the grid. The one I had before was a grid with 121 squares starting on the top left and ending on the bottom right. I wasn’t satisfied with these specifications – the idea of reaching a 100 had more of a victorious effect than 121. Also, traditionally, most of these games begin at the bottom left and end on the top left. I tried playing through a game with the original specifications, but knowledge of previous games made it hard to play, so I decided to match the specifications. Moreover, the numbers were displayed in n order, so when the next line comes, it starts from the left again, but with the movement of the tokens, this path is very abrupt and cut off.  Making these changes was very confusing, especially alternating between going from left to right and then vice-versa, but finally, I used the (%2==0) condition and alternated to get the left to right and right to left display. Also, I changed the colors of the squares from just black and alternated between shades of black, to provide more definition to the individual squares. 

Then, I worked on picking random squares on each row to have a colored square that would represent a card. This was very easy to implement, and I made an array of colors and randomized which color to pick for each game from this array, and the position of the squares is random for each game as well.

After this, I worked on moving the tokens and recording what the die returned on rolling. Further, I had to add the functionality of moving when arriving on a colored square and then showing the card when the token arrives at the colored square. This was definitely the hardest part to figure out – implementation-wise and design-wise. I went through a lot of ways to implement this and figuring out how the token should move, when should it move, should there be triggers, etc. I picked the version that seemed the simplest of the bunch.  The player rolls the die and the token moves according to the die number, and this is automatic. If they happen to arrive at a colored square, the extra steps are also accounted for in the movement of the token, and the card is displayed on the side to make sense of the movement. In order to understand whose turn it is, the token is displayed on the side of whose turn it is. 

Rolling of the die and coordinating that with the turns of the user was confusing. I had a global variable to keep track of the turns. Figuring out the logic behind it took some time though. Also, I found out that the number of steps the tokens take the first time around is always -1 the number of steps they are supposed to take, so I made an if condition to check if it is the first time they are moving and take that into account as well. The explanation I have for it is that the number displayed starts at 1 and the internal number starts at 0, and I created the program handling both, so I changed that. Finally, the player gets to move, only when a 1 or 6 comes up, so I had a global variable to indicate whether they are on the board yet. 

Then, I designed the beginning and end slides of the game which was fairly easy to implement. 

Welcome page + Instructions
Exit Page

The functionality of the restart button took some time to figure out, the core of the issue was that many of my global variables needed to be reinitialized too, not just those in the setup(). But once I figured that out, all was done. The final addition was the sound! I added an evil laugh effect every time the player enters a colored square that makes them move back and a “woohoo” effect for when they progressed. I added some instructions on the game board as well, just in case the player chooses to skip the instructions on the welcome slide, they’ll not be completely confused during the game. A lot of the things I incorporated in the game design-wise were from feedback from family and friends playing the game. They noticed things that seemed very obvious to me (for example, you have to double click on the dice to stop it). So, yeah, it was a cool learning experience. 

Here’s a demo of the game

 

//sound
import processing.sound.*;
SoundFile[] soundFiles = new SoundFile[2]; 

//images
PImage kathakali, kToken, kathakaliWon;
PImage theyyam, tToken, theyyamWon;
PImage power, filter, daagini, mosquito, dosa, coconut, houseboat, mahabali, traffic, sadya;
PImage welcome;


Rect rect[];
int size = 82;
int diceX = (100*width/8)+25;
int diceY =(60*height/8)+10;
int diceSize = 90;
boolean toggleRun=false;
boolean gameMode = false;
int yOffset = 35;
int xOffset = 40;

//color palette
color c1=#ffc097;
color c2=#ffee93;
color c3=#fcf5c7;
color c4=#aeced9;
color c5=#adf7b6;
int die = 0;
color[] palette = {c1, c2, c3, c4, c5};


//game logisitics
int turn=-1;
int token=0;
Token token1, token2;
boolean card=false;
int cardButton=0;
int start1;
int start2;
boolean celebrate = false;
boolean start=false;

void setup() {
  fullScreen();

  soundFiles[0] = new SoundFile(this, "evil.wav");
  soundFiles[1] = new SoundFile(this, "woohoo.wav");
  theyyam = loadImage("theyyam.png");
  tToken = loadImage("theyyam.png");
  theyyam.resize(220, 260);
  tToken.resize(88, 104);
  theyyamWon = loadImage("theyyamWon.jpg");


  kathakali = loadImage("kathakali.png");
  kToken  = loadImage("kathakali.png");
  kathakali.resize(180, 224);
  kToken.resize(72, 90);
  kathakaliWon = loadImage("kathakaliWon.jpg");
  
  
  
  

  power = loadImage("power.jpg");
  filter = loadImage("filter.jpg");
  daagini = loadImage("daagini.jpg");
  mosquito = loadImage("mosquito.jpg");
  dosa = loadImage("dosa.jpg");
  coconut = loadImage("coconut.jpg");
  houseboat = loadImage("houseboat.jpg");
  mahabali = loadImage("mahabali.jpg");
  traffic = loadImage("traffic.jpg");
  sadya = loadImage("sadya.jpg");

  welcome = loadImage("WELCOME.jpg");

  background(0);
  image(welcome, 0, 40);
}

void draw() {



  if (start)
  {
    
    //when restart, show image of dice, so that player can click on it to begin game
    if (celebrate)
    {
      fill(#FFF3D6);
      rectMode(CENTER);
      rect(diceX, diceY, diceSize, diceSize, diceSize/5);

      //dots
      fill(50);


      ellipse(diceX, diceY, diceSize/5, diceSize/5);
      rectMode(CORNER);
      celebrate=false;
    }


    if (toggleRun)
    {
      die=dice();
    }


    for (int j=0; j<rect.length; j++) {
      rect[j].display();
    }



    if (token1.mode)
    {
      token1.display();
    }
    if (token2.mode)
    {
      token2.display();
    }






    if (((die==1) || (die ==6)) && ((!toggleRun)))
    {
      

      if (token==0 && !token1.mode)
      {

        token1.start();
      }
      if (token==1 && !token2.mode)
      {
        token2.start();
      }
    }




    fill(0);
    noStroke();
    rect(0, 0, 285, 800);
    fill(0);
    noStroke();
    rect(diceX-diceSize, diceY-diceSize, 200, 30);
    fill(255);

    text("Double click on the dice to roll it", diceX-diceSize, diceY-diceSize+20);

    text("Goal: Reach Square 100", 60, 600);
    text("Roll die and move accordingly, movement", 20, 620);
    text(" changes if there's a card involved on the square ", 0, 640);
    text("you land.", 100, 660);
    text("Each player needs to roll a one or a six", 30, 300);
    text("to begin their game ", 80, 320);

    if (turn==-1)
      image(kathakali, 35, height/2-100);
    else
    {
      if (token==0)
        image(theyyam, 25, height/2-100);
      else
        image(kathakali, 35, height/2-100);
    }

    if (token1.num>=99)
    {

      token1.celebrate();
      celebrate = true;
    }

    if (token2.num>=99)
    {

      token2.celebrate();
      celebrate = true;
    }
  }
}

//grid print
void grid() {
  int xlen = (width)/size ;
  int ylen = (height)/size;
  rect = new Rect[(xlen-7)*(ylen)];

  int i=99;
  color clr;


  int card;
  int cardNum=0;
  for (int y=0; y < ylen; y++) {
    cardNum++;
    int rand = int(random(4, xlen-5));
    if (y%2==0)
    {

      for (int x =3; x < xlen-4; x++) {
        card=0;
        PVector p = new PVector(x*size+xOffset, y*size+yOffset);
        int j = y%2;
        if ((i+j)%2==0)
          clr=color(40);
        else
          clr=color(0);
        if (x==rand)
        {
          clr=palette[int(random(5))];

          //clr=color(247,221,123);
          card=1;
        }
        rect[i] = new Rect(p, size, i, clr, card, cardNum);

        i--;
      }
    } else
    {
      for (int x =xlen-4; x >3; x--) {
        card=0;
        PVector p = new PVector(x*size+xOffset-size, y*size+yOffset);
        int j = y%2;
        if ((i+j)%2!=0)
          clr=color(40);
        else
          clr=color(0);
        if (x==rand)
        {

          clr=palette[int(random(5))];

          //clr=color(247,221,123);
          card=1;
        }
        rect[i] = new Rect(p, size, i, clr, card, cardNum);
        i--;
      }
    }
  }
}






int dice() {

  fill(255);

  fill(#FFF3D6);
  rectMode(CENTER);
  rect(diceX, diceY, diceSize, diceSize, diceSize/5);

  //dots
  fill(50);
  int side = int(random(1, 7));
  if (side == 1 || side == 3 || side == 5)
    ellipse(diceX, diceY, diceSize/5, diceSize/5);
  if (side == 2 || side == 3 || side == 4 || side == 5 || side == 6) {
    ellipse(diceX - diceSize/4, diceY - diceSize/4, diceSize/5, diceSize/5);
    ellipse(diceX+ diceSize/4, diceY + diceSize/4, diceSize/5, diceSize/5);
  }
  if (side == 4 || side == 5 || side == 6) {

    ellipse(diceX - diceSize/4, diceY + diceSize/4, diceSize/5, diceSize/5);
    ellipse(diceX + diceSize/4, diceY- diceSize/4, diceSize/5, diceSize/5);
  }
  if (side == 6) {
    ellipse(diceX, diceY- diceSize/4, diceSize/5, diceSize/5);
    ellipse(diceX, diceY + diceSize/4, diceSize/5, diceSize/5);
  }
  rectMode(CORNER);




  return side;
}

void mousePressed() {
  color red = color(255, 0, 0);
  color yellow = color(255, 255, 0);
  if (start) {
    if (start1!=-1||start2!=-1)
    {
      fill(0);
      rect(1160, 450, 250, 70, 50 );
    }
    PVector p = new PVector(0, 0);
    if (mouseX>(diceX)-(diceSize/2) && mouseX<(diceX)+(diceSize/2) && mouseY>(diceY)-(diceSize/2) && mouseY<(diceY)+(diceSize/2 ))
    {

      toggleRun=!toggleRun;
      turn+=1;
      fill(0);
      rect(1160, 100, 281, 450);
      if ((turn%2==0))
      {
        token=1-token;
      } else
      {
        if (token==1 && token2.mode)
        {
          if (token2.num>99) {
            token2.num=99;
          }
          if (token2.num<0) {
            token2.num=0;
          }
          token2.num +=die;
          for (int i=0; i<rect.length; i++)
          {
            if (rect[i].num==token2.num)
            {
              if (start1<0)
              {
                p = rect[i-1].position;
                token2.move(p);

                token2.num=rect[i-1].num;
                if (rect[i-1].card==1)
                {
                  showCards(rect[i-1].cardNum, yellow, token2);
                  cardButton=2;
                }
                break;
              }
              p = rect[i].position;
              token2.move(p);

              if (rect[i].card==1)
              {
                showCards(rect[i].cardNum, yellow, token2);
                cardButton=2;
              }

              break;
            }
          }


          start1++;
        }


        if (token==0 && token1.mode)
        {

          token1.num +=die;
          if (token1.num>99) {
            token1.num=99;
          }
          if (token1.num<0) {
            token1.num=0;
          }

          for (int i=0; i<rect.length; i++)
          {
            if (rect[i].num==token1.num)
            {
              if (start2<0)
              {
                p = rect[i-1].position;
                token1.move(p);
                token1.num=rect[i-1].num;
                if (rect[i-1].card==1)
                {

                  showCards(rect[i-1].cardNum, red, token1);
                  cardButton=1;
                }

                break;
              }
              p = rect[i].position;
              token1.move(p);
              if (rect[i].card==1)
              {
                showCards(rect[i-1].cardNum, red, token1);
                cardButton = 1;
              }

              break;
            }
          }

          start2++;
        }
      }
    }

    if ((token2.num>=99||token1.num>=99) && celebrate)
    {
      //reset
      if (mouseX>360 && mouseX<660 && mouseY>459 && mouseY<542)
      {
        restart();
      }

      //exit
      if (mouseX>719 && mouseX<982 && mouseY>459 && mouseY<542)
      {
        exit();
      }
    }
  }
  if (!start)
  {
    if (mouseX>121 && mouseX<1319 && mouseY>742 && mouseY<782)
    {
      start=true;
      restart();
    }
  }
}



void showCards(int cardNum, color c, Token token)
{

  PVector p = new PVector(0, 0);
  switch(cardNum)
  {
  case 1:
    fill(c);
    token.num -=5;
    soundFiles[0].play();
    image(power, 1160, 100);
    break;
  case 2:
    fill(c);
    token.num +=8;
    soundFiles[1].play();
    image(filter, 1160, 100);
    break;
  case 3:
    fill(c);
    token.num -=8;
    image(daagini, 1160, 100);
    soundFiles[0].play();
    break;
  case 4:
    fill(c);
    token.num -=3;
    image(mosquito, 1160, 100);
    soundFiles[0].play();
    break;
  case 5:
    fill(c);
    token.num +=10;
    soundFiles[1].play();

    image(dosa, 1160, 100);
    break;
  case 6:
    fill(c);
    token.num -=10;
    image(coconut, 1160, 100);
    soundFiles[0].play();
    break;
  case 7:
    fill(c);
    token.num +=12;
    soundFiles[1].play();
    image(houseboat, 1160, 100);
    break;
  case 8:
    fill(c);
    token.num +=3;
    soundFiles[1].play();

    image(mahabali, 1160, 100);
    break;
  case 9:
    fill(c);
    token.num -=3;
    image(traffic, 1160, 100);
    soundFiles[0].play();
    break;
  case 10:
    token.num +=5;
    soundFiles[1].play();

    image(sadya, 1160, 100);
    break;
  }


  if (token.num>99) {
    token.num=99;
  }
  if (token.num<0) {
    token.num=0;
  }


  for (int i=0; i<rect.length; i++)
  {
    if (rect[i].num==token.num)
    {

      token.position = rect[i].position;


      break;
    }
  }


 

  
}

void restart()
{
  background(0);



  toggleRun=false;
  gameMode = false;

  die = 0;
  turn=-1;
  token=0;
  card=false;
  cardButton=0;
  start1=-1;
  start2=-1;

  token1 = new Token(tToken, theyyam, theyyamWon);
  token2 = new Token(kToken, kathakali, kathakaliWon);

  grid();

  dice();


}

Disclaimer: The components of the art used in this game are done by other artists and some of them by me, I worked on them on illustrator for the game. 

Credits:

Kathakali – Sonali Meshram

Theyyam – Sinu Rajendran

Comics (Maayavi) – Pradeep Sathe, M. Mohandas, Thyagarajan

Leave a Reply