I am participating in the nuggets Community game creative submission Contest. For details, please see: Game Creative Submission Contest

Hello everyone, I am a bowl week, a front end that does not want to be drunk (inrolled). If I am lucky enough to write an article that you like, I am very lucky

Writing in the front

2048 this game was first released in 2014, the gameplay is relatively simple, is through the direction of the key to slide the page, and then the number of the page to the specified direction, if the number is the same, the number of the merger.

I recently recreated the game using Vue3+TS+Tailwind, demonstrating it as follows:

This game also made a response type, can be used in mobile phone barrier-free, try to play the address 👉 dig gold 2048.

Note: Minerals obtained in the game are for entertainment only! The template used for this project is vue-template.

Why Vue3

First, the game is not very complex and does not require the use of a game engine, such as Cocos; Then I didn’t want to play with the native DOM, so I chose the data-driven framework Vue3; That way I don’t have to worry about how to manipulate DOM elements, just the data.

The setup syntax is also very sweet. Here are two articles of my own:

  • There are seven ways to communicate with Vue3 components, not to mention that you can’t communicate with them
  • Get started with Composition API in 10 minutes

The game is material

All the materials in the game come from articles published by gold Digging account or pictures published by boiling point. Photoshop is used for matting, as shown below:

If you need the PSD source, you can go to GitHub and get it at the end of this article.

The page layout

For this page layout, I used Grid layout, TailwindCSS, and wrote almost no CSS code. The

<! -- App.vue -->
<template>
  <div
    class="warp h-screen lg:w-[480px] md:w-screen m-auto flex flex-col justify-center items-center select-none"
  >
    <div>
      <h4
        class="Font -bold text-2XL MB-5 flex bg-[rgba(30,128,255,.8)] Rounded -full text-white Pt-3 PR-10 Pb-2 PL-6"
      >
        <! -- Ore icon -->
        <OreIcon />: {{with}}</h4>
    </div>
    <div
      class="container bg-[#D6E8FF] w-full lg:h-[680px] md:h-[520px] grid grid-cols-4 grid-rows-4 gap-4 p-4 rounded-lg"
      @touchstart="handleTouchStart"
      @touchend="handleTouchEnd"
    >
      <span v-for="(item, index) in dataArray" :key="index" class="grid-item">
        <div v-for="inx in n2048" :key="inx">
          <img
            v-if="item === inx"
            :src="getImageUrl(item)"
            class="w-full he-full user-drag animate-scale"
          />
        </div>
      </span>
    </div>
    <div class="mt-2 text-left w-full p-4 text-white">
      <p>Note: the ore exchange with the screenshots to find Tony teacher, do not exchange blame me oh ~</p>
      <p>Gameplay: PC side press on the left and right, H5 you on the phone to pull back and forth on good ~</p>
    </div>
  </div>
</template>
<style>
@keyframes scale {
  0% {
    transform: scale(0.5);
  }
  100% {
    transform: scale(1); }}.animate-scale {
  animation: scale 0.2 s ease;
}
</style>
Copy the code

The above code is a simple Grid layout, if I write any questions welcome comment.

The core code

First, we implement the core of the whole game, which is the function of the combined card. First, we only consider one line, and can only slide left. The effect to be achieved is shown in the picture below:

By analyzing the above figure, the following ideas can be sorted out:

  1. By default, an array of length 4 is received, and no data is filled with zeros;
  2. Define an empty array to store processed data and return it;
  3. Iterates through four values in a row. If the current value is 0, the loop is broken.
  4. The next one in the record index, which compares two values to see if they are equal, if they arepushDouble values,i++Skip the next one, because the next one is already added to the current onepushThe current value.
  5. So far the basic functionality has been achieved, but if it is the third case in the figure above, the result will be[2, 4]It’s clear that the outcome is not what we want, the outcome that we need is[2, 4]The reason for this problem is that the null value we use is 0, which is not true at the first judgment, so ifjWhen the value of is 0, we need to putjPoint to the next one until it’s non-zero;
  6. As a final step, we need to return an array of length 4 to replace the previous array.

The implementation code is as follows:

const _2048 = (arr: number[], boo = true) = > {
  const newArr: number[] = []
  for (let i = 0; i < arr.length; i++) {
    // If the current value is 0, jump out of the loop
    if(! arr[i])continue
    // When 32768 appears, the game is over (because there is no bigger graph than 32768)
    if (arr[i] === 32768) {
      // TODO game over win
      gameOverBox()
      return[]}let j = i + 1
    for (; j < arr.length; j++) {
      if(arr[j] ! = =0) break
    }
    // Compare for equality and push the result into the new array
    if (arr[i] === arr[j]) {
      // add extra points
      newArr.push(arr[i] + arr[j])
      i++
    } else {
      newArr.push(arr[i])
    }
  }
  // 补0
  return [0.0.0.0].map((v, i) = > newArr[i] || v)
}
Copy the code

So now we have our core function written, and in fact, it works in all directions, and we just need to pass the arguments in different order.

Let’s take the first row and the first example, left slide is [0, 1, 2, 3], right slide is [3, 2, 1, 0], up slide is [0, 4, 8, 12], slide is [12, 8, 4, 0];

We define the order as a constant that is passed as an argument when used:

const DATA_PROPS = {
  ArrowLeft: [[0.1.2.3],
    [4.5.6.7],
    [8.9.10.11],
    [12.13.14.15]],ArrowUp: [[0.4.8.12],
    [1.5.9.13],
    [2.6.10.14],
    [3.7.11.15]],ArrowRight: [[3.2.1.0],
    [7.6.5.4],
    [11.10.9.8],
    [15.14.13.12]],ArrowDown: [[12.8.4.0],
    [13.9.5.1],
    [14.10.6.2],
    [15.11.7.3]],}Copy the code

At this point you should know that the core of the game is an array of length 16, and the empty space displayed on the page has a value of 0 in the array.

Create a new card

Now let’s write a function to create a new card, which goes like this:

  1. Get the 0 index of an array of length 16 and save it as an array;
  2. Get a random value from this array as our index to add a new card;
  3. Add a random array to the values you want to add.

The implementation code is as follows:

/ / create
const create = (arr: number[]) = > {
  // Find the index that is not 0 in the array and save it to an array
  const val0Arr = findIndexAll(arr, 0)

  const random = Math.floor(Math.random() * val0Arr.length)
  const index = val0Arr[random]
  const newArr = [2.4.8]
  arr[index] = newArr[Math.floor(Math.random() * newArr.length)]
}
// Looks for all occurrences of x in the array and returns an array containing the matching index
const findIndexAll = (arr: number[], x: number) = > {
  const results = [],
    len = arr.length
  let pos = 0
  while (pos < len) {
    pos = arr.indexOf(x, pos)
    if (pos === -1) {
      // Exit the loop to complete the search
      break
    }
    results.push(pos) // Store index when found
    pos += 1 // And start searching from the next location
  }
  return results
}
Copy the code

The game start

Tool function

Before the game starts, we need to define two utility functions, one to update the returned array into the array, and one to pass four index sequences in one direction, each calling the update function. The implementation code is as follows:

const setArrayVal = (arr: number[], index: number[], value: number[]) = > {
  index.forEach((val, index) = > {
    arr[val] = value[index]
  })
}
const ArrComputed = (arr: number[], direction: DirectionType, bool = true) = > {
  DATA_PROPS[direction].forEach(_= > {
    const newArr = _2048([arr[_[0]], arr[_[1]], arr[_[2]], arr[_[3]]], bool)
    setArrayVal(arr, _, newArr)
  })
}
Copy the code

Define the game start function

This function is relatively simple, just need to call the first two functions, again before deciding whether the game is over, example code is as follows:

const run = (direction: DirectionType) = > {
  // over is used to record whether the game is over
  if (over.value) return

  ArrComputed(dataArray.value, direction)
  // TODO:Determine if the game is over
  create(dataArray.value)
}
Copy the code

Listen for PC events

PC speakers simply listen for keyboard events and call our run function, as shown in the following example:

const DirectionArr: DirectionType[] = [
  'ArrowLeft'.'ArrowRight'.'ArrowUp'.'ArrowDown',]document.addEventListener('keyup'.(e: KeyboardEvent) = > {
  if (DirectionArr.find(item= > item === e.key)) {
    run(e.key as unknown as DirectionType)
  }
})
Copy the code

Listen for mobile events

On the mobile side, we need to monitor the touch event and determine the sliding direction. Here, we use TouchStart to record the start position and touchEnd to record the end position.

Example code is as follows:

const moveXY = {
  startX: 0.startY: 0.endX: 0.endY: 0,}const handleTouchStart = (e: TouchEvent) = > {
  e.preventDefault()
  // Get the starting positionmoveXY.startX = e.touches? .0].pageX moveXY.startY = e.touches? .0].pageY
}
const handleTouchEnd = (e: TouchEvent) = > {
  e.preventDefault()
  // Get the end positionmoveXY.endX = e.changedTouches? .0].pageX moveXY.endY = e.changedTouches? .0].pageY
  // Get the sliding distance
  const distanceX = moveXY.endX - moveXY.startX
  const distanceY = moveXY.endY - moveXY.startY
  // Determine the sliding direction
  if (Math.abs(distanceX) > Math.abs(distanceY) && distanceX < 0) {
    run('ArrowLeft')}else if (Math.abs(distanceX) > Math.abs(distanceY) && distanceX > 0) {
    run('ArrowRight')}else if (Math.abs(distanceX) < Math.abs(distanceY) && distanceY < 0) {
    run('ArrowUp')}else if (Math.abs(distanceX) < Math.abs(distanceY) && distanceY > 0) {
    run('ArrowDown')}}Copy the code

At this point, we can see that the game is ready to run and mobile compatible.

Game over

End of the game box component

First we write a game end of the box component, the example code is as follows:

<script setup lang="ts">
import { withDefaults } from 'vue'
import { useVModel } from '@vueuse/core'
import OreIcon from './OreIcon.vue'
interface Props {
  modelValue: boolean
  score: number
}
const props = withDefaults(defineProps<Props>(), {
  modelValue: false.score: 0,})const emits = defineEmits(['update:modelValue'.'restart'])
const show = useVModel(props, 'modelValue', emits)
const handleReStart = () = > {
  emits('restart')}</script>

<template>
  <div v-show="show" class="w-screen h-screen fixed inset-0">
    <div class="mask bg-black opacity-50 absolute inset-0"></div>
    <div
      class="h-60 w-[420px] bg-white rounded-3xl absolute inset-0 m-auto p-6 flex flex-col"
    >
      <div class="text-center text-xl">Game over</div>
      <span class="text-2xl flex justify-center mt-3 flex-grow items-center"
        >Get {{props. Score}}<OreIcon />
      </span>
      <el-button
        type="primary"
        size="large"
        round
        class="w-full mt-2"
        @click="handleReStart"
        ></el-button ></div>
  </div>
</template>

Copy the code

Bidirectional data binding is implemented via Vueuse’s useVModel, and I have to say that this hooks library really works.

How to tell when a game is over

Judge the end of the game here I stole a little lazy, directly through the function to perform sliding, after the execution of the four directions of the value and the current consistency, indicating that can not slide, so the game is over.

It is important to note that the array to be tested must be a deep copy of the original array.

The implementation code is as follows:

// Game over judgment
const gameOver = (arr: number[]) = > {
  const oldArr = JSON.stringify(arr)

  const testArr = JSON.parse(JSON.stringify(arr))
  // Compute four directions
  const _res: string[] = []
  DirectionArr.forEach(item= > {
    ArrComputed(testArr, item, false)
    _res.push(JSON.stringify(testArr))
  })

  if (_res.filter(i= > i === oldArr).length == 4) {
    // Game over
    over.value = true
    // TODO: Opens the end-of-game popbox component}}Copy the code

Now we call this function in the function that started the game, and we can determine the end of the game.

Start all over again

It is easier to start again, just reset the data, as shown in the following code:

// Array reset function
const restart = () = > {
  const arr = Array.apply(null.Array(16)).map(Number.prototype.valueOf, 0)
  const random1 = Math.floor(Math.random() * arr.length)
  const random2 = Math.floor(Math.random() * arr.length)
  arr[random1] = 2
  arr[random2] = 4
  return arr
}

// Start over
const handleReStart = () = > {
  show.value = false
  dataArray.value = restart()
  score.value = 0
  over.value = false
}
Copy the code

End scatter flower ~~~

Write in the last

The source code for this game is open source on GitHub (welcome to ⭐), and if you have a good idea, you can fork out the repository, or even clone the core code, and make it whatever you like.

If this article is helpful to you, please like it and comment on it. Thank you

If there are errors in the code, please correct them. If you have a good idea to put forward, I meet ~