Overview

The Library we use to make this game is P5.JS for website and Processing for dekstop as the platform.

Reference :

Desktop Version

If you want to run the game in the desktop (windows), you can download the game below

download link

Project Source

This game is open sourced in our github repo

Program Documentation

Lorem ipsum dolor, sit amet consectetur adipisicing elit. Vero, provident quos magnam fugiat possimus sint, est modi id voluptatem explicabo culpa numquam nulla. Nulla laboriosam natus ut voluptas delectus quis ducimus corporis, maiores assumenda officia?

change documentation

Render.pde

Global Variable

The setup we make using processing :

String page = "MainMenu";
PFont pixelFont;
JSONObject json;
int xCenter;
int startTime, endTime;

setup()

The setup we make using processing :

void setup() {
  reloadBestTime();
  setupAllSound();
  pixelFont = createFont("../font/PressStart2P-Regular.ttf", 50);
  textFont(pixelFont);
  size(1000, 800, P3D);
  xCenter = width / 2;
  smooth();
  imageMode(CENTER);
  themeSound.play();
}

clickToRenderPage()

The setup we make using processing :

void clickToRenderPage(String thePage, boolean changeTheme, float xPos, float yPos,
                       float objHalfWidth, float objHalfHeight) {
  if ((mouseX <= xPos + objHalfWidth && mouseX >= xPos - objHalfWidth) && (mouseY <= yPos +
       objHalfHeight && mouseY >= yPos - objHalfHeight)) {
    page = thePage;
    playTickSound();
    textFont(pixelFont);
    if (changeTheme) {
        renderTheme();
    }
  }
}

mouseClicked()

The setup we make using processing :

void mouseClicked() {
  switch(page) {
  case "MainMenu":
    mainMenuMouseClicked();
    break;
  case "SettingsPage":
    settingsPageMouseClicked();
    break;
  case "PlayGame":
    defaultCamera();
    playGameMouseClicked();
    break;
  case "WinPage":
    winGameMouseClicked();
    break;
  case "RecordPage":
    recordPageMouseClicked();
    break;
  case "GameMode":
    gameModeMouseClicked();
    break;
  default:
    println("page does not exits");
  }
}

setCamera()

The setup we make using processing :

void setCamera() {
  camera(mouseX, mouseY,
        (height/2.0) / tan(PI*30.0 / 180.0), width/2.0, height/2.0, 0, 0, 1, 0);
}

defaultCamera()

The setup we make using processing :

void defaultCamera() {
  camera();
}

draw()

The setup we make using processing :

void draw() {
  endTime = millis();
  switch(page) {
  case "MainMenu":
    setCamera();
    mainMenu();
    break;
  case "SettingsPage":
    setCamera();
    settingsPage();
    break;
  case "PlayGame":
    defaultCamera();
    playGame();
    break;
  case "GameMode":
    setCamera();
    gameMode();
    break;
  case "WinPage":
    setCamera();
    winGame();
    break;
  case "RecordPage":
    setCamera();
    recordPage();
    break;
  default:
    println("page does not exits");
  }
}

Main_Menu.pde

mainMenu()

The setup we make using processing :

void mainMenu() {
  background(bgColor);
  xCenter = width / 2;
  displayText("MATCHUS", 50, 0, textColor, textColor, xCenter, 230);
  rectMode(CENTER);
  displayRect(xCenter, 270, textWidth("MATCHUS"), 10, textColor, -1);
  cursor(ARROW);
  displayText("PLAY", 35, 5, textColor, primaryHoverColor, xCenter, 320);
  displayText("SETTINGS", 35, 5, textColor, primaryHoverColor, xCenter, 370);
  displayText("BEST RECORD", 35, 5, textColor, primaryHoverColor, xCenter, 420);
  displayText("EXIT", 35, 5, textColor, secondaryHoverColor, xCenter, 470);
}

clickToExitGame()

The setup we make using processing :

void clickToExitGame(float yPos) {
  textSize(35);
  float halfExitText = textWidth("EXIT") / 2;
  if ((mouseX <= xCenter + halfExitText && mouseX >= xCenter -  halfExitText) &&
    (mouseY <= yPos + 15 && mouseY >= yPos - 15)) {
    exit();
  }
}

mainMenuMouseClicked()

The setup we make using processing :

void mainMenuMouseClicked() {
  clickToRenderPage("GameMode", false, xCenter,
    320, textWidth("Play") / 2, 15);
  clickToRenderPage("SettingsPage", false, xCenter,
    370, textWidth("Settings") / 2, 15);
  clickToRenderPage("RecordPage", false, xCenter,
    420, textWidth("Best Record") / 2, 15);
  clickToExitGame(470);
}

Game_Mode.pde

Global Variable

The setup we make using processing :

String gameModeName;
int totalCardType;
float xPosStart, yPosStart, xRange, yRange, xLimit, yLimit, halfWidthRect, halfHeightRect;

gameMode()

The setup we make using processing :

void gameMode() {
  background(bgColor);
  displayText("Game Mode", 50, 0, textColor, textColor, xCenter, 230);
  displayText("EASY", 35, 5, textColor, primaryHoverColor, xCenter, 300);
  displayText("NORMAL", 35, 5, textColor, primaryHoverColor, xCenter, 350);
  displayText("HARD", 35, 5, textColor, primaryHoverColor, xCenter, 400);
  displayText("MENU", 35, 5, textColor, secondaryHoverColor, xCenter, 470);
}

gameModeMouseClicked()

The setup we make using processing :

void gameModeMouseClicked() {
  setGameStart(300, "EASY");
  setGameStart(350, "NORMAL");
  setGameStart(400, "HARD");
  clickToRenderPage("PlayGame", false, xCenter,
    300, textWidth("EASY") / 2, 20);
  clickToRenderPage("PlayGame", false, xCenter,
    350, textWidth("NORMAL") / 2, 20);
  clickToRenderPage("PlayGame", false, xCenter,
    400, textWidth("HARD") / 2, 20);
  clickToRenderPage("MainMenu", false, xCenter,
    470, textWidth("MENU") / 2, 20);
}

setGameMode()

The setup we make using processing :

void setGameMode(int totalCard, float xPos, float yPos, float xRangeCenter, float yRangeCenter, float
                 xLimitRange, float yLimitRange, float halfWidth, float halfHeight) {
  xPosStart = xPos;
  yPosStart = yPos;
  xRange = xRangeCenter;
  yRange = yRangeCenter;
  xLimit = xLimitRange;
  yLimit = yLimitRange;
  halfWidthRect = halfWidth;
  halfHeightRect = halfHeight;
  totalCardType = totalCard;
}

Settings.pde

settingsPage()

The setup we make using processing :

void settingsPage() {
  background(bgColor);
  displayText("Settings", 50, 0, textColor, textColor, xCenter, 230);
  displayText("THEME:" + theme, 35, 5, textColor, primaryHoverColor, xCenter, 300);
  displayText("BACK", 35, 5, textColor, secondaryHoverColor, xCenter, 350);
}

settingsPageMouseClicked()

The setup we make using processing :

void settingsPageMouseClicked() {
  clickToRenderPage("SettingsPage", true, xCenter,
    300, textWidth("Theme:" + theme) / 2, 15);
  clickToRenderPage("MainMenu", false, xCenter,
    360, textWidth("back") / 2, 15);
}

Record_Page.pde

Global Variable

The setup we make using processing :

String theme = "DARK";
int bgColor = #0d0401;
int textColor = #f2f2f2;
int primaryHoverColor = #87f49a;
int secondaryHoverColor = #fcba03;

darkTheme(), lightTheme(), colorfullTheme()

The setup we make using processing :

void darkTheme() {
  theme = "DARK";
  bgColor = 30;
  textColor = #f2f2f2;
  primaryHoverColor = #87f49a;
  secondaryHoverColor = #fcba03;
}
void lightTheme() {
  theme = "LIGHT";
  bgColor = #d9d9d9;
  textColor = #0d0401;
  primaryHoverColor = #40c27a;
  secondaryHoverColor = #fcba03;
}
void colorfullTheme() {
  theme = "COLORFULL";
  bgColor = #4195f0;
  textColor = #542a91;
  primaryHoverColor = #b2f007;
  secondaryHoverColor = #e637c0;
}

renderTheme()

The setup we make using processing :

void renderTheme() {
  switch(theme) {
  case "DARK":
    lightTheme();
    break;
  case "LIGHT":
    colorfullTheme();
    break;
  case "COLORFULL":
    darkTheme();
    break;
  default:
    println("unknown color theme");
  }
}

Play_Game.pde

Global Variable

The setup we make using processing :

boolean rotateHorizontal = true;
int win = 0;
boolean sameCard;
int rotateInterval = 10;
boolean[][] rotateStatus;
float[][] rectAngle;
int cardUp = -1;
PImage[] checkCard = new PImage[2];
int[] rowCard = new int[2];
int[] columnCard = new int[2];
boolean[][] isAbleToRotate;
boolean[][] cardFaceUp;
int[][] zPos;

playGame()

The setup we make using processing :

void playGame() {
  background(bgColor);
  waitToStartTheGame();
  displayText("Match the card", 25, 0, textColor, textColor, xCenter, 75);
  cursor(ARROW);
  displayText("MENU", 25, 5, textColor, secondaryHoverColor, 75, 75);
  rectMode(CENTER);
  setRect();
  waitToRotate();
  waitAfterWin();
  getTime();
}

shuffleCard()

The setup we make using processing :

void shuffleCard(PImage[][] array) {
  int Cols = array.length;
  int Rows = array[0].length;
  for (int col = 0; col 
    for (int row = 0; row 
      int randC = int(random(Cols));
      int randR = int(random(Rows));
      PImage imgTemp = array[col][row];
      array[col][row] = array[randC][randR];
      array[randC][randR] = imgTemp;
    }
  }
}

setRect()

The setup we make using processing :

void setRect() {
  int k = 0, l = 0;
  for (float i = yPosStart; i <= yLimit; i = i + yRange) {
    l = 0;
    for (float j = xPosStart; j <= xLimit; j = j + xRange) {
      pushMatrix();
      translate(j, i, zPos[k][l]);
      if (rotateHorizontal) {
        rotateY(radians(rectAngle[k][l]));
      } else {
        rotateX(radians(rectAngle[k][l]));
      }
      rotateHorizontal = !rotateHorizontal;
      image(cardIdentity[k][l], 0, 0, 80, 80);
      translate(0, 0, -zPos[k][l]);
      displayRect(0, 0, halfWidthRect * 2, halfHeightRect * 2, textColor, #ffffff);
      popMatrix();
      rotateRect(k, l);
      l++;
    }
    k++;
  }
}

rotateRect()

The setup we make using processing :

void rotateRect(int rowIndex, int columnIndex) {
  if (rotateStatus[rowIndex][columnIndex]) {
    rectAngle[rowIndex][columnIndex] += rotateInterval;
    if (rectAngle[rowIndex][columnIndex] == 180 || rectAngle[rowIndex][columnIndex] == 0) {
      rotateStatus[rowIndex][columnIndex] = false;
    }
    if (rectAngle[rowIndex][columnIndex] == 180 && !isAbleToRotate[rowIndex][columnIndex]) {
      isAbleToRotate[rowIndex][columnIndex] = true;
      cardUp = -1;
    }
  }
}

playGameMouseClicked()

The setup we make using processing :

void playGameMouseClicked() {
  clickToRenderPage("MainMenu", false, 75,
    75, textWidth("menu") / 2, 15);
  int k = 0, l = 0;
  for (float i = yPosStart; i <= yLimit; i = i + yRange) {
    l = 0;
    for (float j = xPosStart; j <= xLimit; j = j + xRange) {
      clickToRotateRect(k, l, j, i, halfWidthRect, halfHeightRect);
      l++;
    }
    k++;
  }
}

resetCard()

The setup we make using processing :

void resetCard() {
  for ( int i = 0; i < 2; i++) {
    for (int j = 0; j < totalCardType; j++) {
      rotateStatus[i][j] = false;
      cardFaceUp[i][j] = false;
      isAbleToRotate[i][j] = true;
    }
  }
}

setAllCardStatus()

The setup we make using processing :

void setAllCardStatus() {
  rotateStatus = new boolean[2][totalCardType];
  rectAngle = new float[2][totalCardType];
  isAbleToRotate = new boolean[2][totalCardType];
  cardFaceUp = new boolean[2][totalCardType];
  zPos = new int[2][totalCardType];
  for (int i = 0; i < 2; i++) {
    for (int j = 0; j < totalCardType; j++) {
      rotateStatus[i][j] = false;
      rectAngle[i][j] = 0;
      zPos[i][j] = 1;
      isAbleToRotate[i][j] = false;
      cardFaceUp[i][j] = true;
    }
  }
}

setCard()

The setup we make using processing :

void setCard() {
  for (int i = 0; i < 2; i++) {
    for (int j = 0; j < totalCardType; j++) {
      //bgCard[i][j] = #f2f2f2;
      rotateStatus[i][j] = true;
      isAbleToRotate[i][j] = true;
      cardFaceUp[i][j] = false;
    }
  }
}

changeRotateDirection()

The setup we make using processing :

void changeRotateDirection(int rowIndex, int columnIndex) {
  if (rectAngle[rowIndex][columnIndex] == 0) {
    rotateInterval = 10;
  } else {
    rotateInterval = -10;
  }
}

clickToRotateRect()

The setup we make using processing :

void clickToRotateRect(int rowIndex, int columnIndex, float xPos, float yPos,
                       float objHalfWidth, float objHalfHeight) {
  if ((mouseX <= xPos + objHalfWidth && mouseX >= xPos -  objHalfWidth) &&
     (mouseY <= yPos + objHalfHeight && mouseY >= yPos - objHalfHeight)) {
    if (!rotateStatus[rowIndex][columnIndex] && isAbleToRotate[rowIndex][columnIndex] && cardUp != 1) {
      changeRotateDirection(rowIndex, columnIndex);
      rotateStatus[rowIndex][columnIndex] = true;
      playFlipSound();
    }
    if (!cardFaceUp[rowIndex][columnIndex] && isAbleToRotate[rowIndex][columnIndex] && cardUp != 1) {
      isAbleToRotate[rowIndex][columnIndex] = false;
      cardFaceUp[rowIndex][columnIndex] = true;
      cardUp++;
      checkCard[cardUp] = cardIdentity[rowIndex][columnIndex];
      rowCard[cardUp] = rowIndex;
      columnCard[cardUp] = columnIndex;
      if (cardUp == 1) {
        if (checkCard[0] == checkCard[1]) {
          for (int i = 0; i < 2; i++) {
            isAbleToRotate[rowCard[i]][columnCard[i]] = false;
          }
          cardUp = -1;
          win++;
          sameCard = true;
        } else {
          startTimeToRotate = millis();
          sameCard = false;
        }
      }
    }
    if (win == totalCardType) {
      startTimeToRotate = millis();
      setBestTime();
      playWinSound();
      if (modeBT.equals("NONE") ||
        int(split(timeText, ':')[0]) < int(split(modeBT, ':')[0]) ||
        int(split(timeText, ':')[1]) < int(split(modeBT, ':')[1])) {
        setSaveJSONBT();
        newRecord = true;
      } else {
        newRecord = false;
      }
    }
  }
}

Win_Game.pde

Global Variable

The setup we make using processing :

boolean newRecord;

winGame()

The setup we make using processing :

void winGame() {
  background(bgColor);
  displayText("Congratulation", 50, 0, textColor, textColor, xCenter, 150);
  displayText("On winning", 50, 0, textColor, textColor, xCenter, 200);
  displayRecordTime();
  displayText("PLAY AGAIN", 35, 5, textColor, primaryHoverColor, xCenter, 330);
  displayText("MENU", 35, 5, textColor, primaryHoverColor, xCenter, 380);
}

displayRecordTime()

The setup we make using processing :

void displayRecordTime() {
  if (newRecord) {
    displayText("NEW RECORD TIME - " + timeText, 25, 0, textColor, textColor, xCenter, 265);
  } else {
    displayText("TIME - " + timeText, 25, 0, textColor, textColor, xCenter, 265);
    displayText("BEST TIME - " + modeBT, 25, 0, textColor, textColor, xCenter, 450);
  }
}

winGameMouseClicked()

The setup we make using processing :

void winGameMouseClicked() {
  setGameStart(330, gameModeName);
  
  clickToRenderPage("PlayGame", false, xCenter,
    330, textWidth("PLAY AGAIN") / 2, 20);
  clickToRenderPage("MainMenu", false, xCenter,
    380, textWidth("MENU") / 2, 20);
}

Connect_JSON.pde

Global Variable

The setup we make using processing :

String modeBT;

setBestTime()

The setup we make using processing :

void setBestTime() {
  switch(gameModeName) {
  case "EASY":
    modeBT = easyBT;
    break;
  case "NORMAL":
    modeBT = normalBT;
    break;
  case "HARD":
    modeBT = hardBT;
    break;
  default:
    println("Unknown Game Mode");
  }
}

setSaveJSONBT()

The setup we make using processing :

void setSaveJSONBT() {
  json = new JSONObject();
  switch(gameModeName) {
  case "EASY":
    json.setString("easy", timeText);
    json.setString("normal", normalBT);
    json.setString("hard", hardBT);
    break;
  case "NORMAL":
    json.setString("easy", easyBT);
    json.setString("normal", timeText);
    json.setString("hard", hardBT);
    break;
  case "HARD":
    json.setString("easy", easyBT);
    json.setString("normal", normalBT);
    json.setString("hard", timeText);
    break;
  default:
    println("Unknown Game Mode");
  }
  saveJSONObject(json, "../data/record.json");
}

Record_Page.pde

recordPage()

The setup we make using processing :

void recordPage() {
  background(bgColor);
  displayText("Best Record", 50, 0, textColor, textColor, xCenter, 230);
  reloadBestTime();
  displayText("EASY - " + easyBT, 30, 0, textColor, textColor, xCenter, 300);
  displayText("NORMAL - " + normalBT, 30, 0, textColor, textColor, xCenter, 350);
  displayText("HARD - " + hardBT, 30, 0, textColor, textColor, xCenter, 400);
  displayText("MENU", 35, 5, textColor, secondaryHoverColor, xCenter, 460);
}

recordPageMouseClicked()

The setup we make using processing :

void recordPageMouseClicked() {
  clickToRenderPage("MainMenu", false, xCenter,
    480, textWidth("menu") / 2, 20);
}

Time_Setting.pde

Global Variable

The setup we make using processing :

String easyBT, normalBT, hardBT;
int startTimeToRotate;
int minuteTime = 0;
int secondTime = 0;
String timeText;
boolean gameStart;

reloadBestTime()

The setup we make using processing :

void reloadBestTime() {
  json = loadJSONObject("../data/record.json");
  easyBT = json.getString("easy");
  normalBT = json.getString("normal");
  hardBT = json.getString("hard");
}

setGameStart()

The setup we make using processing :

void setGameStart(float yPos, String gameMode) {
  textSize(35);
  float halfExitText = textWidth("Play") / 2;
  if ((mouseX <= xCenter + halfExitText && mouseX >= xCenter -  halfExitText) &&
      (mouseY <= yPos + 20 && mouseY >= yPos - 20)) {
    startTime = millis();
    minuteTime = 0;
    secondTime = 0;
    gameModeName = gameMode;
    gameType();
    loadAssets();
    shuffleCard(cardIdentity);
    setAllCardStatus();
    gameStart = true;
    rotateInterval = 10;
  }
}

gameType()

The setup we make using processing :

void gameType() {
  switch(gameModeName) {
  case "EASY":
    setGameMode(4, 140, 300, 240, 290, 860, 590, 100, 125);
    break;
  case "NORMAL":
    setGameMode(5, 108, 300, 196, 246, 892, 546, 88, 113);
    break;
  case "HARD":
    setGameMode(6, 87.5, 300, 165, 215, 912.5, 515, 77.5, 102.5);
    break;
  default:
    println("Unknown Game Mode");
  }
}

timeTicking(), timeWaitToRotateTicking()

The setup we make using processing :

int timeTicking() {
  return endTime - startTime;
}
int timeWaitToRotateTicking() {
  return endTime - startTimeToRotate;
}

getTime()

The setup we make using processing :

void getTime() {
  int interval = 1000;
  if (timeTicking() >= interval) {
    secondTime++;
    startTime = millis();
  }
  if (secondTime >= 59) {
    minuteTime++;
    secondTime = 0;
  }
  timeText = minuteTime + ":" + secondTime;
  displayText(timeText, 25, 0, textColor, textColor, 915, 75);
}

waitToStartTheGame(), waitToRotate(), waitAfterWin()

The setup we make using processing :

void waitToStartTheGame() {
  int interval = 3;
  if (secondTime >= interval && gameStart) {
    setCard();
    gameStart = false;
  }
}
void waitToRotate() {
  int interval = 700;
  if (timeWaitToRotateTicking() >= interval && cardUp == 1 && !sameCard) {
    for (int i = 0; i < 2; i++) {
      changeRotateDirection(rowCard[i], columnCard[i]);
      rotateStatus[rowCard[i]][columnCard[i]] = true;
      cardFaceUp[rowCard[i]][columnCard[i]] = false;
    }
    sameCard = true;
  }
}
void waitAfterWin() {
  int interval = 500;
  if (win == totalCardType && timeWaitToRotateTicking() >= interval) {
    resetCard();
    page = "WinPage";
    win = 0;
  }
}

display_rect.pde

displayRect()

The setup we make using processing :

void displayRect(float xPos, float yPos, float widthSize, float heightSize,
                int colorFill, int colorStroke) {
  noFill();
  noStroke();
  if (colorFill != -1) {
    fill(colorFill);
  }
  if (colorStroke != -1) {
    stroke(colorStroke);
  }
  rect(xPos, yPos, widthSize, heightSize);
}

display_text.pde

displayText()

The setup we make using processing :

void displayText(String text, int textSize, int scaleSize, int regularColor,
                int colorOnHover, float xPos, float yPos) {
  textAlign(CENTER, CENTER);
  textSize(textSize);
  float halfTextWidth = textWidth(text) / 2;
  float halfTextHeight = textSize / 2;
  fill(regularColor);
  if ((mouseX <= xPos + halfTextWidth &&
    mouseX >= xPos -  halfTextWidth) &&
    (mouseY <= yPos + halfTextHeight && mouseY >= yPos - halfTextHeight)) {
    textSize(textSize + scaleSize);
    if (scaleSize != 0) {
      cursor(HAND);
    }
    fill(colorOnHover);
  }
  text(text, xPos, yPos);
}

Play_Sound.pde

Global Variable

The setup we make using processing :

import processing.sound.*;
SoundFile tick, winSound, flip, themeSound;

setupAllSound()

The setup we make using processing :

void setupAllSound() {
  tick = new SoundFile(this, "../sound/tick.mp3");
  winSound = new SoundFile(this, "../sound/win.mp3");
  flip = new SoundFile(this, "../sound/flip.mp3");
  themeSound = new SoundFile(this, "../sound/themeSound.mp3");
}

playFlipSound(), playTickSound(), playWinSound()

The setup we make using processing :

void playFlipSound() {
  flip.play();
}
void playTickSound() {
  tick.play();
}
void playWinSound() {
  winSound.play();
}