基于 HTML5 Canvas 制作一个精美的 2048 小游戏--day 1

news2025/1/20 7:53:32

基于 HTML5 Canvas 制作一个精美的 2048 小游戏

在这个快节奏的生活中,简单而富有挑战性的游戏总能给我们带来乐趣。2048 是一款受欢迎的益智游戏,不仅考验智力,还能让人回味无穷。今天,我带领大家将一起学习如何使用 HTML5 Canvas 来制作一个精美的 2048 小游戏。

一、了解游戏规则

在深入代码之前,我们需要了解游戏的基本规则:

  1. 目标:通过合并相同的数字块,最终达到2048。
  2. 操作:玩家可以通过上下左右的箭头键控制数字块的移动。
  3. 生成新块:每次成功移动后,会随机生成一个“2”或“4”的数字块。
  4. 游戏结束:当所有方块被填满且无法进行任何合并时,游戏结束。

二、建立 HTML 结构

首先,我们需要搭建游戏的基础 HTML 结构。在 HTML 文件中,引入 Canvas 元素以绘制游戏界面。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>2048 游戏</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <canvas id="gameCanvas"></canvas>
    </div>
    <script src="script.js"></script>
</body>
</html>

三、样式设计

接下来,我们需要为游戏添加一些样式,使其更具吸引力。我们将在 CSS 文件中设置按钮和画布的样式。

body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #faf8ef;
    font-family: 'Arial', sans-serif;
}

.container {
    position: relative;
}

canvas {
    border: 2px solid #bbada0;
    background-color: #eee4da;
}

四、游戏逻辑实现

在脚本文件中,我们将编写游戏的核心逻辑,包括初始化游戏、绘制方块、移动操作和合并方块等。

4.1 初始化

首先,我们需要创建一个类似于二维数组的数字格子,并用随机数填充初始状态。



const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

const gridSize = 4;
const tileSize = 100;
canvas.width = gridSize * tileSize;
canvas.height = gridSize * tileSize;

let board = Array.from({ length: gridSize }, () => Array(gridSize).fill(0));

function initBoard() {
    addRandomTile();
    addRandomTile();
    drawBoard();
}

function drawBoard() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    for (let r = 0; r < gridSize; r++) {
        for (let c = 0; c < gridSize; c++) {
            drawTile(r, c);
        }
    }
}

function drawTile(r, c) {
    const value = board[r][c];
    ctx.fillStyle = value !== 0 ? getTileColor(value) : '#ccc0b3';
    ctx.fillRect(c * tileSize, r * tileSize, tileSize - 10, tileSize - 10);

    if (value !== 0) {
        ctx.fillStyle = '#776e65';
        ctx.font = 'bold 45px Arial';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(value, c * tileSize + tileSize / 2 - 5, r * tileSize + tileSize / 2);
    }
}

function getTileColor(value) {
    switch (value) {
        case 2: return '#eee4da';
        case 4: return '#ede0c8';
        case 8: return '#f2b179';
        case 16: return '#f59563';
        case 32: return '#f67c5f';
        case 64: return '#f67c5f';
        case 128: return '#edcf72';
        case 256: return '#edcc61';
        case 512: return '#edc850';
        case 1024: return '#edc53f';
        case 2048: return '#edc22e';
        default: return '#ccc0b3';
    }
}

function addRandomTile() {
    let emptyCells = [];
    for (let r = 0; r < gridSize; r++) {
        for (let c = 0; c < gridSize; c++) {
            if (board[r][c] === 0) {
                emptyCells.push({ r, c });
            }
        }
    }

    if (emptyCells.length) {
        const { r, c } = emptyCells[Math.floor(Math.random() * emptyCells.length)];
        board[r][c] = Math.random() < 0.9 ? 2 : 4;
    }
}

4.2 移动与合并方块

通过键盘事件监听来实现方块的移动和合并,我们定义方向常量,结合用户输入的方向实现移动和合并的逻辑。

document.addEventListener('keydown', (event) => {
    switch (event.key) {
        case 'ArrowUp':
            moveUp();
            break;
        case 'ArrowDown':
            moveDown();
            break;
        case 'ArrowLeft':
            moveLeft();
            break;
        case 'ArrowRight':
            moveRight();
            break;
    }
    drawBoard();
});

function canMergeTiles(r1, c1, r2, c2) {
    return board[r1][c1] !== 0 && board[r1][c1] === board[r2][c2];
}

function moveUp() {
    for (let c = 0; c < gridSize; c++) {
        for (let r = 1; r < gridSize; r++) {
            if (board[r][c] !== 0) {
                let targetRow = r;
                while (targetRow > 0 && board[targetRow - 1][c] === 0) {
                    // 向上移动
                    board[targetRow - 1][c] = board[targetRow][c];
                    board[targetRow][c] = 0;
                    targetRow--;
                }
                if (targetRow > 0 && canMergeTiles(targetRow - 1, c, targetRow, c)) {
                    // 合并方块
                    board[targetRow - 1][c] *= 2;
                    board[targetRow][c] = 0;
                }
            }
        }
    }
}

function moveDown() {
    for (let c = 0; c < gridSize; c++) {
        for (let r = gridSize - 2; r >= 0; r--) {
            if (board[r][c] !== 0) {
                let targetRow = r;
                while (targetRow < gridSize - 1 && board[targetRow + 1][c] === 0) {
                    // 向下移动
                    board[targetRow + 1][c] = board[targetRow][c];
                    board[targetRow][c] = 0;
                    targetRow++;
                }
                if (targetRow < gridSize - 1 && canMergeTiles(targetRow + 1, c, targetRow, c)) {
                    // 合并方块
                    board[targetRow + 1][c] *= 2;
                    board[targetRow][c] = 0;
                }
            }
        }
    }
}

function moveLeft() {
    for (let r = 0; r < gridSize; r++) {
        for (let c = 1; c < gridSize; c++) {
            if (board[r][c] !== 0) {
                let targetCol = c;
                while (targetCol > 0 && board[r][targetCol - 1] === 0) {
                    // 向左移动
                    board[r][targetCol - 1] = board[r][targetCol];
                    board[r][targetCol] = 0;
                    targetCol--;
                }
                if (targetCol > 0 && canMergeTiles(r, targetCol - 1, r, targetCol)) {
                    // 合并方块
                    board[r][targetCol - 1] *= 2;
                    board[r][targetCol] = 0;
                }
            }
        }
    }
}

function moveRight() {
    for (let r = 0; r < gridSize; r++) {
        for (let c = gridSize - 2; c >= 0; c--) {
            if (board[r][c] !== 0) {
                let targetCol = c;
                while (targetCol < gridSize - 1 && board[r][targetCol + 1] === 0) {
                    // 向右移动
                    board[r][targetCol + 1] = board[r][targetCol];
                    board[r][targetCol] = 0;
                    targetCol++;
                }
                if (targetCol < gridSize - 1 && canMergeTiles(r, targetCol + 1, r, targetCol)) {
                    // 合并方块
                    board[r][targetCol + 1] *= 2;
                    board[r][targetCol] = 0;
                }
            }
        }
    }
}

五、完善游戏体验

在游戏逻辑实现后,我们需要添加分数计算、胜利和失败的提示,以及重新开始游戏的功能。这些都将进一步提升游戏体验。

5.1 分数系统

我们为游戏添加分数系统,每次合并方块时更新分数。

let score = 0;

function mergeTiles(r1, c1, r2, c2) {
    if (board[r1][c1] === board[r2][c2]) {
        board[r1][c1] *= 2;
        score += board[r1][c1];
        board[r2][c2] = 0;
    }
}

5.2 结束提示

当玩家没有可移动的方块时,可以弹出提示框告知游戏结束。

function checkGameOver() {
    for (let r = 0; r < gridSize; r++) {
        for (let c = 0; c < gridSize; c++) {
            if (board[r][c] === 0) {
                return false; // 还有空格
            }
            if (c < gridSize - 1 && canMergeTiles(r, c, r, c + 1)) {
                return false; // 可以合并
            }
            if (r < gridSize - 1 && canMergeTiles(r, c, r + 1, c)) {
                return false; // 可以合并
            }
        }
    }
    return true; // 游戏结束
}

function showGameOver() {
    alert('游戏结束!您的得分是:' + score);
}

结论

现在,您应该对如何使用 HTML5 Canvas 制作一个 2048 小游戏有了更详细的了解。从简单的 UI 设计到移动和合并方块的逻辑实现,每一步都至关重要。虽然本文未能详细说明所有代码,但希望您能根据提供的思路和示例进行深入探索和实现。创建游戏是一项有趣且富有成就感的工作,快去尝试制作您自己的 2048 小游戏吧!

完整代码

HTML (index.html)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>2048 游戏</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <canvas id="gameCanvas"></canvas>
    </div>
    <script src="script.js"></script>
</body>
</html>

CSS (style.css)

body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #faf8ef;
    font-family: 'Arial', sans-serif;
}

.container {
    position: relative;
}

canvas {
    border: 2px solid #bbada0;
    background-color: #eee4da;
}

JavaScript (script.js)

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

const gridSize = 4;
const tileSize = 100;
canvas.width = gridSize * tileSize;
canvas.height = gridSize * tileSize;

let board = Array.from({ length: gridSize }, () => Array(gridSize).fill(0));
let score = 0;

initBoard();

function initBoard() {
    addRandomTile();
    addRandomTile();
    drawBoard();
}

function drawBoard() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    for (let r = 0; r < gridSize; r++) {
        for (let c = 0; c < gridSize; c++) {
            drawTile(r, c);
        }
    }
    // 显示分数
    ctx.fillStyle = '#776e65';
    ctx.font = 'bold 20px Arial';
    ctx.textAlign = 'center';
    ctx.fillText('Score: ' + score, canvas.width / 2, canvas.height - 20);
}

function drawTile(r, c) {
    const value = board[r][c];
    ctx.fillStyle = value !== 0 ? getTileColor(value) : '#ccc0b3';
    ctx.fillRect(c * tileSize, r * tileSize, tileSize - 10, tileSize - 10);

    if (value !== 0) {
        ctx.fillStyle = '#776e65';
        ctx.font = 'bold 45px Arial';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(value, c * tileSize + tileSize / 2 - 5, r * tileSize + tileSize / 2);
    }
}

function getTileColor(value) {
    switch (value) {
        case 2: return '#eee4da';
        case 4: return '#ede0c8';
        case 8: return '#f2b179';
        case 16: return '#f59563';
        case 32: return '#f67c5f';
        case 64: return '#f67c5f';
        case 128: return '#edcf72';
        case 256: return '#edcc61';
        case 512: return '#edc850';
        case 1024: return '#edc53f';
        case 2048: return '#edc22e';
        default: return '#ccc0b3';
    }
}

function addRandomTile() {
    let emptyCells = [];
    for (let r = 0; r < gridSize; r++) {
        for (let c = 0; c < gridSize; c++) {
            if (board[r][c] === 0) {
                emptyCells.push({ r, c });
            }
        }
    }

    if (emptyCells.length) {
        const { r, c } = emptyCells[Math.floor(Math.random() * emptyCells.length)];
        board[r][c] = Math.random() < 0.9 ? 2 : 4;
    }
}

document.addEventListener('keydown', (event) => {
    let moved = false;
    switch (event.key) {
        case 'ArrowUp':
            moved = moveUp();
            break;
        case 'ArrowDown':
            moved = moveDown();
            break;
        case 'ArrowLeft':
            moved = moveLeft();
            break;
        case 'ArrowRight':
            moved = moveRight();
            break;
    }
    if (moved) {
        addRandomTile();
        drawBoard();
        if (checkGameOver()) {
            showGameOver();
        }
    }
});

function canMergeTiles(r1, c1, r2, c2) {
    return board[r1][c1] !== 0 && board[r1][c1] === board[r2][c2];
}

function moveUp() {
    let moved = false;
    for (let c = 0; c < gridSize; c++) {
        for (let r = 1; r < gridSize; r++) {
            if (board[r][c] !== 0) {
                let targetRow = r;
                while (targetRow > 0 && board[targetRow - 1][c] === 0) {
                    board[targetRow - 1][c] = board[targetRow][c];
                    board[targetRow][c] = 0;
                    targetRow--;
                    moved = true;
                }
                if (targetRow > 0 && canMergeTiles(targetRow - 1, c, targetRow, c)) {
                    board[targetRow - 1][c] *= 2;
                    score += board[targetRow - 1][c];
                    board[targetRow][c] = 0;
                    moved = true;
                }
            }
        }
    }
    return moved;
}

function moveDown() {
    let moved = false;
    for (let c = 0; c < gridSize; c++) {
        for (let r = gridSize - 2; r >= 0; r--) {
            if (board[r][c] !== 0) {
                let targetRow = r;
                while (targetRow < gridSize - 1 && board[targetRow + 1][c] === 0) {
                    board[targetRow + 1][c] = board[targetRow][c];
                    board[targetRow][c] = 0;
                    targetRow++;
                    moved = true
                }
                if (targetRow < gridSize - 1 && canMergeTiles(targetRow + 1, c, targetRow, c)) {
                    board[targetRow + 1][c] *= 2;
                    score += board[targetRow + 1][c];
                    board[targetRow][c] = 0;
                    moved = true;
                }
            }
        }
    }
    return moved;
}

function moveLeft() {
    let moved = false;
    for (let r = 0; r < gridSize; r++) {
        for (let c = 1; c < gridSize; c++) {
            if (board[r][c] !== 0) {
                let targetCol = c;
                while (targetCol > 0 && board[r][targetCol - 1] === 0) {
                    board[r][targetCol - 1] = board[r][targetCol];
                    board[r][targetCol] = 0;
                    targetCol--;
                    moved = true;
                }
                if (targetCol > 0 && canMergeTiles(r, targetCol - 1, r, targetCol)) {
                    board[r][targetCol - 1] *= 2;
                    score += board[r][targetCol - 1];
                    board[r][targetCol] = 0;
                    moved = true;
                }
            }
        }
    }
    return moved;
}

function moveRight() {
    let moved = false;
    for (let r = 0; r < gridSize; r++) {
        for (let c = gridSize - 2; c >= 0; c--) {
            if (board[r][c] !== 0) {
                let targetCol = c;
                while (targetCol < gridSize - 1 && board[r][targetCol + 1] === 0) {
                    board[r][targetCol + 1] = board[r][targetCol];
                    board[r][targetCol] = 0;
                    targetCol++;
                    moved = true;
                }
                if (targetCol < gridSize - 1 && canMergeTiles(r, targetCol + 1, r, targetCol)) {
                    board[r][targetCol + 1] *= 2;
                    score += board[r][targetCol + 1];
                    board[r][targetCol] = 0;
                    moved = true;
                }
            }
        }
    }
    return moved;
}

function checkGameOver() {
    for (let r = 0; r < gridSize; r++) {
        for (let c = 0; c < gridSize; c++) {
            if (board[r][c] === 0) {
                return false; // 还有空格
            }
            if (c < gridSize - 1 && canMergeTiles(r, c, r, c + 1)) {
                return false; // 可以合并
            }
            if (r < gridSize - 1 && canMergeTiles(r, c, r + 1, c)) {
                return false; // 可以合并
            }
        }
    }
    return true; // 游戏结束
}

function showGameOver() {
    alert('游戏结束!您的得分是:' + score);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2279269.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux——线程条件变量(同步)

Linux——多线程的控制-CSDN博客 文章目录 目录 文章目录 前言 一、条件变量是什么&#xff1f; 1、死锁的必要条件 1. 互斥条件&#xff08;Mutual Exclusion&#xff09; 2. 请求和保持条件&#xff08;Hold and Wait&#xff09; 3. 不可剥夺条件&#xff08;No Preemption&…

“AI 辅助决策系统:决策路上的智慧领航员

在当今瞬息万变的时代&#xff0c;无论是企业的运营管理&#xff0c;还是个人在生活中的重大抉择&#xff0c;都需要精准、高效的决策。然而&#xff0c;信息的繁杂和未来的不确定性&#xff0c;常常让决策变得困难重重。这时&#xff0c;AI 辅助决策系统宛如一位智慧的领航员&…

某讯一面,感觉问Redis的难度不是很大

前不久&#xff0c;有位朋友去某讯面试&#xff0c;他说被问到了很多关于 Redis 的问题&#xff0c;比如为什么用 Redis 作为 MySQL 的缓存&#xff1f;Redis 中大量 key 集中过期怎么办&#xff1f;如何保证缓存和数据库数据的一致性&#xff1f;我将它们整理出来&#xff0c;…

DDD - 微服务落地的技术实践

文章目录 Pre概述如何发挥微服务的优势怎样提供微服务接口原则微服务的拆分与防腐层的设计 去中心化的数据管理数据关联查询的难题Case 1Case 2Case 3 总结 Pre DDD - 软件退化原因及案例分析 DDD - 如何运用 DDD 进行软件设计 DDD - 如何运用 DDD 进行数据库设计 DDD - 服…

闪豆多平台视频批量下载器

1. 视频链接获取与解析 首先&#xff0c;在哔哩哔哩网页中随意点击一个视频&#xff0c;比如你最近迷上了一个UP主的美食制作视频&#xff0c;想要下载下来慢慢学。点击视频后&#xff0c;复制视频页面的链接。复制完成后&#xff0c;不要急着关闭浏览器&#xff0c;因为接下来…

【STM32-学习笔记-14-】FLASH闪存

文章目录 FALSH闪存一、FLASH简介二、FLASH基本结构三、FLASH解锁四、使用指针访问存储器五、FLASH擦除以及编程流程Ⅰ、程序存储器全擦除1. 读取FLASH_CR的LOCK位2. 检查LOCK位是否为13. 设置FLASH_CR的MER 1和STRT 1&#xff08;如果LOCK位0&#xff09;4. 检查FLASH_SR的B…

微信消息群发(定时群发)-UI自动化产品(基于.Net平台+C#)

整理 | 小耕家的喵大仙 出品 | CSDN&#xff08;ID&#xff1a;lichao19897314&#xff09; 关联源码及工具下载https://download.csdn.net/download/lichao19897314/90096681https://download.csdn.net/download/lichao19897314/90096681https://download.csdn.net/download/…

FPGA产业全景扫描

随着芯片种类日益丰富、功能日益强大&#xff0c;人们不禁好奇&#xff1a;一块FPGA是如何从最初的概念一步步呈现在我们面前的&#xff1f; FPGA设计、FPGA原型验证/仿真、FPGA板级调试和应用&#xff0c;是FPGA从概念到应用的必经之路。本文将围绕这几个核心环节&#xff0c…

SW - 钣金零件保存成DWG时,需要将折弯线去掉

文章目录 SW - 钣金零件保存成DWG时&#xff0c;需要将折弯线去掉概述笔记备注END SW - 钣金零件保存成DWG时&#xff0c;需要将折弯线去掉 概述 如果做需要弯折的切割件&#xff0c;最好做成钣金零件。 最近做了几个小钣金(将钣金展开&#xff0c;建立新草图&#xff0c;在2…

git系列之revert回滚

1. Git 使用cherry-pick“摘樱桃” step 1&#xff1a; 本地切到远程分支&#xff0c;对齐要对齐的base分支&#xff0c;举例子 localmap git pull git reset --hard localmap 对应的commit idstep 2&#xff1a; 执行cherry-pick命令 git cherry-pick abc123这样就会将远程…

Excel重新踩坑6:工作实战总结之根据筛选条件求平均成绩

一、前言&#xff1a; 这个博客的实战场景&#xff1a;给了一组学生数据&#xff0c;这些数据中&#xff0c;有全市20个社区&#xff0c;1-9年级的学生各科成绩。要求按照各社区统计1-9年级的所有学生各科平均值。下面首先介绍会用到的一些函数&#xff0c;然后再简单说明实战…

【数据分析】02- A/B 测试:玩转假设检验、t 检验与卡方检验

一、背景&#xff1a;当“审判”成为科学 1.1 虚拟场景——法庭审判 想象这样一个场景&#xff1a;有一天&#xff0c;你在王国里担任“首席审判官”。你面前站着一位嫌疑人&#xff0c;有人指控他说“偷了国王珍贵的金冠”。但究竟是他干的&#xff0c;还是他是被冤枉的&…

SpringMVC 实战指南:打造高效 Web 应用的秘籍

第一章&#xff1a;三层架构和MVC 三层架构&#xff1a; 开发服务器端&#xff0c;一般基于两种形式&#xff0c;一种 C/S 架构程序&#xff0c;一种 B/S 架构程序使用 Java 语言基本上都是开发 B/S 架构的程序&#xff0c;B/S 架构又分成了三层架构三层架构&#xff1a; 表现…

通过idea创建的springmvc工程需要的配置

在创建的spring mvc工程中&#xff0c;使用idea开发之前需要配置文件包括porm.xml、web.xml、springmvc.xml 1、porm.xml 工程以来的spring库&#xff0c;主要包括spring-aop、spring-web、spring-webmvc&#xff0c;示例配置如下&#xff1a; <project xmlns"http:/…

二、点灯基础实验

嵌入式基础实验第一个就是点灯&#xff0c;地位相当于编程界的hello world。 如下为LED原理图&#xff0c;要让相应LED发光&#xff0c;需要给I/O口设置输出引脚&#xff0c;低电平&#xff0c;二极管才会导通 2.1 打开初始工程&#xff0c;编写代码 以下会实现BLINKY常亮&…

搭建一个基于Spring Boot的数码分享网站

搭建一个基于Spring Boot的数码分享网站可以涵盖多个功能模块&#xff0c;例如用户管理、数码产品分享、评论、点赞、收藏、搜索等。以下是一个简化的步骤指南&#xff0c;帮助你快速搭建一个基础的数码分享平台。 — 1. 项目初始化 使用 Spring Initializr 生成一个Spring …

迅为RK3576开发板Android 多屏显示

迅为iTOP-3576开发板采用瑞芯微RK3576高性能、低功耗的应用处理芯片&#xff0c;集成了4个Cortex-A72和4个Cortex-A53核心&#xff0c;以及独立的NEON协处理器。它适用于ARM PC、边缘计算、个人移动互联网设备及其他多媒体产品。 1.1 Android 多屏同显 iTOP-RK3576 开发板支持…

gather算子的CUDA编程和算子测试

知乎介绍参考添加链接描述 完整测试框架参考本人仓库 添加链接描述 gather算子的onnx定义参考添加链接描述,该算子的主要变换参考下图: 这里我们不妨以input = [A, dimsize, D], indices = [B,C], axis = 1举例子,此时对应的output形状是[A,B,C,D],并且根据gather算子定…

深度学习 Pytorch 张量的线性代数运算

pytorch中并未设置单独的矩阵对象类型&#xff0c;因此pytorch中&#xff0c;二维张量就相当于矩阵对象&#xff0c;并且拥有一系列线性代数相关函数和方法。 在实际机器学习和深度学习建模过程中&#xff0c;矩阵或者高维张量都是基本对象类型&#xff0c;而矩阵所涉及到的线…

Linux下构建OpenEuler22.03+Nginx的Docker镜像

1. 制作OpenEuler22.03的Docker镜像 首先&#xff0c;下载OpenEuler20.03的镜像压缩包&#xff1a; 下载链接为&#xff1a; https://mirrors.aliyun.com/openeuler/openEuler-22.03-LTS/docker_img/x86_64/openEuler-docker.x86_64.tar.xz 这里我们可以顺便下载一下对应的…