SOURCE

console 命令行工具 X clear

                    
>
console
let el = document.querySelector('.wrap')
let data = [
	['', '', '', ''],
	['', '', '', ''],
	['', '', '', ''], 
	['', '', '', '']
]
let hasAdd = false // 判断当前行是否已经有过相加

const colorObj = {
    "": "#d3d3d3",
    "2": "#fef4f2",
    "4": "#fed9a2",
    "8": "#fc8c5e",
    "16": "#f8692f",
    "32": "#f8563d",
    "64": "#ff3936",
    "128": "#00c3dd",
    "256": "#00a4be",
    "512": "#00abcb",
    "1024": "#00abcb",
    "2048": "#00abcb",
    "4096": "#005d6e"
}

// 获取随机数
function getRandomNum(min, max) {
    return Math.round(min + Math.random() * (max - min)) 
}

// 获取随机位置
function getRandomPosition() {
    let arrs = [] // 内容为空的集合
    for(let i = 0; i < 4; i++) {
        for(let j = 0; j < 4; j++) {
            if (!data[i][j]) arrs.push([i,j])
        }
    }
    if (arrs.length === 0) return false
    return arrs[getRandomNum(0, arrs.length)]
}

// 插入一个随机数
function insertNum() {
    let position = getRandomPosition()
    if (!position) return false
    data[position[0]][position[1]] = 2 // getRandomNum(1, 2) * 2
    render(position)
}

// 按键事件
function keydownHandle() {
    switch(event.keyCode){
        case 37:
            toLeft()
            break;
        case 38:
            toTop()
            break;
        case 39:
            toRight()
            break
        case 40:
            toBottom()
            break;
    }
    insertNum()
	if ([37, 38, 39, 40].includes(event.keyCode) && isEnd()) alert('游戏结束')
}

function toLeft() {
    for (let row = 0; row < 4; row++) {
        hasAdd = false // 重置
        // 重复三遍,保证最右侧的能够移动到最左侧
		for(let start = 0; start < 3; start++) {
			for(let col = 0; col < 3; col++) {
                change([row, col], [row, col + 1])
            }
        }   
    }
}

function toTop() {
	for (let col = 0; col < 4; col++) {
	    hasAdd = false // 重置
		for(let start = 0; start < 3; start++) {
			for(let row = 0; row < 3; row++) {
	            change([row, col], [row + 1, col])
	        }
	    }   
	}
}

function toRight() {
    for (let row = 0; row < 4; row++) {
        hasAdd = false // 重置
    	for(let start = 0; start < 3; start++) {
    		for(let col = 3; col > 0; col--) {
                change([row, col], [row, col - 1])
            }
        }   
    }
}

function toBottom() {
    for (let col = 0; col < 4; col++) {
        hasAdd = false // 重置
    	for(let start = 0; start < 3; start++) {
    		for(let row = 3; row > 0; row--) {
                change([row, col], [row - 1, col])
            }
        }   
    }
}

// 计算两个位置的变化
// firstPosition: [row, col]
function change(firstPosition, secPosition) {
    let firstVal = data[firstPosition[0]][firstPosition[1]]
    let secVal = data[secPosition[0]][secPosition[1]]
	
    if (!firstVal && secVal) {
        // 交换位置
        data[firstPosition[0]][firstPosition[1]] = data[secPosition[0]][secPosition[1]]
        data[secPosition[0]][secPosition[1]] = ''
    } else if (firstVal !== '' && firstVal === secVal && !hasAdd) {
        // 两个值相加放到第一个位置,第二个位置置空
        data[firstPosition[0]][firstPosition[1]] = firstVal + secVal
        data[secPosition[0]][secPosition[1]] = ''
        hasAdd = true
    }
}

// 渲染函数
function render(position) {
    let str = ''
    data.forEach((item, row) => {
        item.forEach((num, col) => {
			let className = ''
			if (row === position[0] && col === position[1]) className = 'new'
            str += `<div style="background-color: ${colorObj[num]}" class="${className}">${num}</div>`
        })
    })
    el.innerHTML = str
}

// 是否结束
function isEnd() {
	for(let row = 0; row < 4; row++) {
		for(let col = 0; col < 3; col++) {
			if (data[row][col] === '' || data[row][col + 1] === '') return false
			if(data[row][col] === data[row][col + 1]) return false
		}
	}
	for(let col = 0; col < 4; col++) {
		for(let row = 0; row < 3; row++) {
			if(data[row][col] === data[row + 1][col]) return false
		}
	}
	return true
}

function reset() {
	data = [
		['', '', '', ''],
		['', '', '', ''],
		['', '', '', ''], 
		['', '', '', '']
	]
	insertNum()
}

insertNum()
<body onkeydown="keydownHandle()">
    <button onclick="reset()">重新开始</button>
    <div class="wrap">
    </div>
</body>
body {
	text-align: center;
}
.wrap {
    background-color: #999;
    padding: 5px;
    display: inline-flex;
    border-radius: 4px;
    flex-wrap: wrap;
    width: 240px;
	text-align: center;
}
.wrap div {
    width: 50px;
    height: 50px;
    text-align: center;
    line-height: 50px;
    background: red;
    border-radius: 4px;
    margin: 5px;
	opacity: 1;
}

.new {
	animation: newFrame 2s;
}
@keyframes newFrame{
	0% {
		opacity: 0.5;
	}
	30% {
		opacity: 1;
	}
	60% {
		opacity: 0.2;
	}
	100% {
		opacity: 1;
	}
}