<template>
    <div style="position: relative;">
        <canvas id="canvas" width="400" height="400" style="border: 1px #ccc solid;background: #bbada0" tabindex="0"/>
        <div style="position: absolute;top: 0;left: 450px;">
            <p>分数：{{ view ? view.score : 0 }}</p>
            <el-button @click="reset" type="primary">重新开始</el-button>
            <p>W：上移</p>
            <p>S：下移</p>
            <p>A：左移</p>
            <p>D：右移</p>
        </div>
    </div>
</template>

<script lang="ts">
import {Component, Vue} from "vue-property-decorator"
import {SCanvasView, SColor, SPainter, STextAlign, STextBaseLine} from "@persagy-web/draw/lib";

/**
 * 游戏 2048
 *
 * @author 郝洁 <haojie@persagy.com>
 */
class GameView extends SCanvasView {
    /** 网格数组初始化 */
    gridMap = Array(4).fill(null).map(() => Array(4).fill(0))
    /** 游戏结束标识 */
    gameOver: boolean = false
    /** 游戏累计分数 */
    score = 0;
    /** 数字画笔 */
    // numPaint = new SPainter();
    /** 文字画笔 */
    // textPaint = new SPainter();
    /** 格子顏色 */
    color = {
        0: {
            font: "#ffffffff",
            background: "#eee4da59"
        },
        2: {
            font: "#776e65",
            background: "#eee4da"
        },
        4: {
            font: "#776e65",
            background: "#ede0c8"
        },
        8: {
            font: "#f9f6f2",
            background: "#f2b179"
        },
        16: {
            font: "#f9f6f2",
            background: "#f59563"
        },
        32: {
            font: "#f9f6f2",
            background: "#f67c5f"
        },
        64: {
            font: "#f9f6f2",
            background: "#f65e3b"
        },
        128: {
            font: "#f9f6f2",
            background: "#edcf72"
        },
        256: {
            font: "#f9f6f2",
            background: "#edcc61"
        },
        512: {
            font: "#f9f6f2",
            background: "#edc850"
        },
        1024: {
            font: "#f9f6f2",
            background: "#edc53f"
        },
        2048: {
            font: "#f9f6f2",
            background: "#edc22e"
        },
    };

    /**
     * 构造函数
     *
     * @param id canvas DOM id
     */
    constructor(id: string) {
        super(id);
        // 初始化游戏
        this.init()
    }

    /**
     * 初始化游戏
     */
    init(): void {
        // 遍历网格的行
        for (let r = 0; r < 4; r++) {
            // 遍历网格的列
            for (let c = 0; c < 4; c++) {
                this.gridMap[r][c] = 0
            }
        }
        // 随机填充两个 2
        this.gridMap[Math.floor(Math.random() * 4)][Math.floor(Math.random() * 4)] = 2
        this.gridMap[Math.floor(Math.random() * 4)][Math.floor(Math.random() * 4)] = 2

        // 初始化游戏分数
        this.score = 0

        // 游戏结束标识标记
        this.gameOver = false

    }

    /**
     * 键盘按下事件
     *
     * @param event   事件对象
     */
    onKeyDown(event: KeyboardEvent): void {
        if (this.gameOver) {
            return;
        }
        // 按键判断
        switch (event.code) {
            case "KeyW": // 上移动作
            case "ArrowUp":
                // 判断游戏是否继续，底部产生随机位置随机数
                this.moveUp();
                break;

            case "KeyA": // 左移动作
            case "ArrowLeft":
                // 判断游戏是否继续，右部产生随机位置随机数
                this.moveLeft();
                break;

            case "KeyD":    // 右移动作
            case "ArrowRight":
                this.moveRight();
                // 判断游戏是否继续，左部产生随机位置随机数
                break;
            // 按键 下
            case "KeyS": // 下移动作
            case "ArrowDown":
                // 判断游戏是否继续，上部产生随机位置随机数
                this.moveDown();
                break;
            default :
            // do nothing
        }
        // 更新视图
        this.update();
    }

    /**
     * 上移
     */
    moveUp(): void {
        // 数字是否移动
        let moved = false
        // 遍历所有的列
        for (let i = 0; i < 4; i++) {
            // 进行合并操作的列表
            const numArr = Array(4)
            // 依次从网格中取要合并的元素
            for (let j = 0; j < 4; j++) {
                // 取数
                numArr[j] = this.gridMap[j][i]
            }
            // 移动与合并
            if (this.moveNum(numArr)) {
                moved = true;
                // 合并后的下列表反填入网格
                for (let j = 0; j < 4; j++) {
                    // 反填
                    this.gridMap[j][i] = numArr[j]
                }
            }
        }

        // 如果移动了数字
        if (moved) {
            // 索引列表
            const indexArr = []
            // 依次从网格取空格元素的下标索引
            for (let i = 0; i < 4; i++) {
                // 如果为空格
                if (this.gridMap[3][i] == 0) {
                    // 保存下标
                    indexArr.push(i)
                }
            }
            // 生成新的数的下标
            const index = this.newIndex(indexArr);
            if (index >= 0) {
                this.gridMap[3][index] = this.newNum()
            }
        }
        // 测试是否游戏结束
        this.gameOver = this.checkGameover()
    }

    /**
     * 下移
     */
    moveDown(): void {
        // 是否移动了数字
        let moved = false;

        // 遍历所有列
        for (let i = 0; i < 4; i++) {
            // 进行合并操作的列表
            const numArr = Array(4);
            // 依次从网格中取要合并的元素
            for (let j = 0; j < 4; j++) {
                // 取数
                numArr[j] = this.gridMap[3 - j][i];
            }

            // 移动与合并
            if (this.moveNum(numArr)) {
                moved = true;
                // 合并后的下列表反填入网格
                for (let j = 0; j < 4; j++) {
                    // 反填
                    this.gridMap[3 - j][i] = numArr[j];
                }
            }
        }

        // 如果移动了数字
        if (moved) {
            // 索引列表
            const indexArr = [];
            // 依次从网格取空格元素的下标索引
            for (let i = 0; i < 4; i++) {
                // 如果为空格
                if (this.gridMap[0][i] == 0) {
                    // 保存下标
                    indexArr.push(i)
                }
            }

            // 新生成的数的下标
            const index = this.newIndex(indexArr);
            if (index >= 0) {
                this.gridMap[0][index] = this.newNum();
            }
        }

        // 测试是否游戏结束
        this.gameOver = this.checkGameover();
    }

    /**
     * 左移
     */
    moveLeft(): void {
        // 是否移动了数字
        let moved = false;

        // 遍历所有行
        for (let i = 0; i < 4; i++) {
            // 进行合并操作的列表
            const numArr = Array(4);
            // 依次从网格中取要合并的元素
            for (let j = 0; j < 4; j++) {
                // 取数
                numArr[j] = this.gridMap[i][j];
            }

            // 移动与合并
            if (this.moveNum(numArr)) {
                moved = true;
                // 合并后的下列表反填入网格
                for (let j = 0; j < 4; j++) {
                    // 反填
                    this.gridMap[i][j] = numArr[j];
                }
            }
        }

        // 如果移动了数字
        if (moved) {
            // 索引列表
            const indexArr = [];
            // 依次从网格取空格元素的下标索引
            for (let i = 0; i < 4; i++) {
                // 如果为空格
                if (this.gridMap[i][3] == 0) {
                    // 保存下标
                    indexArr.push(i)
                }
            }

            // 新生成的数的下标
            const index = this.newIndex(indexArr);
            if (index >= 0) {
                this.gridMap[index][3] = this.newNum();
            }
        }

        // 测试是否游戏结束
        this.gameOver = this.checkGameover();
    }

    /**
     * 右移移
     */
    moveRight(): void {
        // 是否移动了数字
        let moved = false;

        // 遍历所有行
        for (let i = 0; i < 4; i++) {
            // 进行合并操作的列表
            const numArr = Array(4);
            // 依次从网格中取要合并的元素
            for (let j = 0; j < 4; j++) {
                // 取数
                numArr[j] = this.gridMap[i][3 - j];
            }

            // 移动与合并
            if (this.moveNum(numArr)) {
                moved = true;
                // 合并后的下列表反填入网格
                for (let j = 0; j < 4; j++) {
                    // 反填
                    this.gridMap[i][3 - j] = numArr[j];
                }
            }
        }

        // 如果移动了数字
        if (moved) {
            // 索引列表
            const indexArr = [];
            // 依次从网格取空格元素的下标索引
            for (let i = 0; i < 4; i++) {
                // 如果为空格
                if (this.gridMap[i][0] == 0) {
                    // 保存下标
                    indexArr.push(i)
                }
            }

            // 新生成的数的下标
            const index = this.newIndex(indexArr);
            if (index >= 0) {
                this.gridMap[index][0] = this.newNum();
            }
        }

        // 测试是否游戏结束
        this.gameOver = this.checkGameover();
    }

    /**
     * 移动或合并数字，数据相同则合并
     *
     * @param numArr  长度为 4 的数组
     * @return 移动数字返回 true，否则返回 false
     */
    private moveNum(numArr: number[]): boolean {
        // 记录所有合并数字的和
        let moved = false;

        // 从第二格开始取数
        for (let i = 1; i < numArr.length; i++) {
            const num = numArr[i];
            // 如果选定的格为0，则取下一格
            if (num == 0) {
                continue;
            }

            // 将除去的数往前移
            for (let j = i = 1; j >= 0; j--) {
                if (numArr[j] == 0) {
                    // 前移
                    numArr[j] = num;
                    numArr[j + 1] = 0;
                    moved = true
                } else if (numArr[j] == num) {
                    // 合并
                    numArr[j] += num;
                    numArr[j + 1] = 0;
                    this.score += num * 2;
                    moved = true;
                    break;
                } else {
                    break;
                }
            }
        }
        // 返回所有合并的数组的和
        return moved
    }

    /**
     * 新生成数字的下标
     *
     * @param indexArr 空格的下标列表的长度大于 0，返回随机的下标；否则返回 0。
     */
    private newIndex(indexArr: number[]): number {
        return indexArr.length > 0 ? indexArr[Math.floor(Math.random() * indexArr.length)] : -1
    }

    /**
     * 返回新生成的数（2 或 4）
     *
     * @return 新生成的数（2 或 4）
     */
    private newNum(): number {
        // 30% 的概率为 4，70% 概率为 2。调整比例可增加或减小游戏的难度。
        return Math.random() > 0.3 ? 4 : 2
    }

    /**
     * 检查是否游戏结束。（没有为空的格，没有相邻两格的数字相同）
     *
     * @return 如果结束返回 true，否则返回 false；
     */
    private checkGameover(): boolean {
        // 依次遍历行与列
        for (let i = 0; i < 4; i++) {
            // 依次取相邻（横向/纵向）的两个格的数字
            for (let j = 0; j < 3; j++) {
                // 判断相邻的格是否相同，或是否为空。
                if (this.gridMap[i][j] == this.gridMap[i][j + 1] || this.gridMap[i][j] == 0 || this.gridMap[i][j + 1] == 0
                    || this.gridMap[j][i] == this.gridMap[j + 1][i] || this.gridMap[j][i] == 0 || this.gridMap[j + 1][i] == 0) {
                    // 返回游戏未结束
                    return false
                }
            }
        }
        // 返回游戏结束
        return true
    }


    /**
     * 绘制游戏画面
     *
     * @param canvas  绘制对象
     */
    onDraw(canvas: SPainter): void {
        canvas.clearRect(0, 0, this.width, this.height)
        canvas.pen.color = SColor.Transparent;
        this.drawMap(canvas);
        this.drawNum(canvas);

        //如果游戏结束,绘制格子
        if (this.gameOver) {
            canvas.brush.color = SColor.Red;
            canvas.font.size = 40;
            canvas.font.textAlign = STextAlign.Center;
            if (Date.now() % 100 > 500) {
                canvas.drawText("Game Over", 200, 200);
            }
            this.update();
            return
        }

        // 刷新游戏画面
        this.update();
    }

    /**
     * 绘制背景图
     */
    drawMap(painter: SPainter) {
        // 遍历行
        for (let row = 0; row < 4; row++) {
            // 遍历列
            for (let col = 0; col < 4; col++) {
                this.drawCell(painter, row, col, this.gridMap[row][col]);
            }
        }
    }

    /**
     * 绘制数字
     */
    drawNum(painter: SPainter): void {
        // 遍历行
        for (let row = 0; row < 4; row++) {
            // 遍历列
            for (let col = 0; col < 4; col++) {
                this.drawCellNum(painter, row, col, this.gridMap[row][col]);
            }
        }
    }

    /**
     * 绘制格子中的数字
     */
    drawCellNum(painter: SPainter, row: number, col: number, type: number): void {
        const x = col * 100;
        const y = row * 100;
        if (type != 0) {
            painter.font.textAlign = STextAlign.Center;
            painter.font.size = 30;
            // @ts-ignore
            painter.brush.color = new SColor(this.color[type].font);
            painter.drawText(type + '', x + 45, y + 45);
        }
    }

    /**
     * 绘制具体格子
     *
     * @param painter   绘制对象
     * @param row       行
     * @param col       列
     * @param type      图类型
     */
    private drawCell(painter: SPainter, row: number, col: number, type: number): void {
        const x = col * 100 + 10;
        const y = row * 100 + 10;
        painter.brush.color = new SColor(this.color[type].background);
        painter.drawRoundRect(x, y, 90, 90, 10);
    }

}

@Component
export default class Gcanvas extends Vue {
    /** 实例化 view */
    view: GameView | null = null

    /**
     * 页面挂载完成
     */
    mounted(): void {
        this.view = new GameView('canvas');
        document.getElementById("canvas")!!.focus();
    }

    /**
     * 重置操作
     */
    reset(): void {
        // 初始化游戏
        this.view!!.init()
        document.getElementById("canvas")!!.focus()
    }
}
</script>

<style scoped lang="less">

</style>
