链式反应本地存储工具类
/**
 * 本地存储工具类 - rotaryChainLocalStorageUtil.js
 * 功能:封装游戏配置、历史记录的本地存储操作
 */
const localStorageUtil = {
    // ========================== 常量定义(存储键名,避免硬编码)==========================
    STORAGE_KEYS: {
        GAME_SETTINGS: 'chainReactionSettings', // 游戏配置(速度、辅助功能等)
        HISTORY_RECORDS: 'chainReactionHistory', // 游戏历史记录
    },
 
    // ========================== 游戏配置操作(速度、辅助功能等)==========================
    /**
     * 从本地存储加载游戏配置
     * @returns {Object} 游戏配置对象(含默认值)
     * @property {number} speed - 装置旋转速度(0.5=1倍速,0.25=2倍速)
     * @property {boolean} flashEnabled - 是否开启闪烁动画
     * @property {boolean} assistEnabled - 是否启用辅助预测
     * @property {boolean} selectedScore - 辅助优先级(true=分数优先,false=乘数优先)
     * @property {boolean} assistPredictGlobal - 是否启用全局预测
     */
    loadGameSettings() {
        try {
            const settingsStr = localStorage.getItem(this.STORAGE_KEYS.GAME_SETTINGS);
            if (!settingsStr) {
                // 无配置时返回默认值
                return {
                    speed: 0.5,
                    flashEnabled: true,
                    assistEnabled: false,
                    selectedScore: true,
                    assistPredictGlobal: false
                };
            }
            // 解析配置并兼容旧版本(缺失字段用默认值)
            const parsedSettings = JSON.parse(settingsStr);
            return {
                speed: parsedSettings.speed ? parsedSettings.speed : 0.5,
                flashEnabled: parsedSettings.flashEnabled ? parsedSettings.flashEnabled : true,
                assistEnabled: parsedSettings.assistEnabled ? parsedSettings.assistEnabled : false,
                selectedScore: parsedSettings.selectedScore ? parsedSettings.selectedScore : true,
                assistPredictGlobal: parsedSettings.assistPredictGlobal ? parsedSettings.assistPredictGlobal : false
            };
        } catch (error) {
            // 解析失败时返回默认值
            return {
                speed: 0.5,
                flashEnabled: true,
                assistEnabled: false,
                selectedScore: true,
                assistPredictGlobal: false
            };
        }
    },
 
    /**
     * 将游戏配置保存到本地存储
     * @param {Object} settings - 待保存的配置对象
     * @property {number} speed - 装置旋转速度(0.5/0.25)
     * @property {boolean} flashEnabled - 是否开启闪烁动画
     * @property {boolean} assistEnabled - 是否启用辅助预测
     * @property {boolean} selectedScore - 辅助优先级(分数/乘数优先)
     * @property {boolean} assistPredictGlobal - 是否启用全局预测
     * @returns {boolean} 保存结果(true=成功,false=失败)
     */
    saveGameSettings(settings) {
        try {
            // 过滤无效配置,仅保留允许的字段
            const validSettings = {
                speed: settings.speed === 0.5 || settings.speed === 0.25 ? settings.speed : 0.5,
                flashEnabled: typeof settings.flashEnabled === 'boolean' ? settings.flashEnabled : true,
                assistEnabled: typeof settings.assistEnabled === 'boolean' ? settings.assistEnabled : false,
                selectedScore: typeof settings.selectedScore === 'boolean' ? settings.selectedScore : true,
                assistPredictGlobal: typeof settings.assistPredictGlobal === 'boolean' ? settings
                    .assistPredictGlobal : false
            };
            // 保存到localStorage
            localStorage.setItem(
                this.STORAGE_KEYS.GAME_SETTINGS,
                JSON.stringify(validSettings)
            );
            return true;
        } catch (error) {
            return false;
        }
    },
 
    // ========================== 历史记录操作(游戏操作日志)==========================
    /**
     * 从本地存储获取所有历史记录
     * @returns {Array<Object>} 历史记录数组(含$index索引字段)
     * @property {number} time - 随机种子
     * @property {Array<string>} records - 操作记录(格式:"状态_行_列")
     * @property {number} score - 游戏分数
     * @property {number} date - 时间戳
     * @property {number} $index - 原始数组中的索引(用于删除操作)
     */
    getHistoryRecords() {
        try {
            const historyStr = localStorage.getItem(this.STORAGE_KEYS.HISTORY_RECORDS);
            if (!historyStr) return [];
            // 解析处理后的记录
            let historyRecords = []
            try {
                // 优先尝试解密解压(新数据)
                historyRecords = this._parseDataAfterLoad(historyStr);
                // 确保是数组,且移除可能残留的$index
                if (!Array.isArray(historyRecords)) throw new Error('记录格式非数组');
                historyRecords = historyRecords.map(item => {
                    const {
                        $index,
                        ...rest
                    } = item;
                    return rest;
                });
            } catch (e) {
                // 解密失败,解析旧数据(未加密)
                const oldRecords = JSON.parse(historyStr);
                if (!Array.isArray(oldRecords)) return [];
                // 旧数据无$index,直接处理后存储(移除$index步骤)
                historyRecords = oldRecords.map(item => {
                    const {
                        $index,
                        ...rest
                    } = item; // 兼容可能误加的$index
                    return rest;
                });
                // 加密存储旧数据(转换为新格式)
                const savedStr = this._processDataBeforeSave(historyRecords);
                localStorage.setItem(this.STORAGE_KEYS.HISTORY_RECORDS, savedStr);
            }
            // 最终返回时添加$index(仅用于前端定位,不存储)
            return historyRecords.map((record, index) => ({
                ...record,
                $index: index
            }));
        } catch (error) {
            return null;
        }
    },
 
    /**
     * 保存新记录到历史记录(按规则去重:种子前三名、全局前20名)
     * @param {Object} newRecord - 待保存的新记录
     * @property {number} time - 随机种子
     * @property {Array<string>} records - 操作记录
     * @property {number} score - 游戏分数
     * @property {number} date - 时间戳
     * @returns {boolean} 保存结果(true=成功,false=失败)
     */
    saveHistoryRecord(newRecord) {
        try {
            // 1. 校验新记录格式(至少包含种子和操作记录)
            if (!newRecord.time || !newRecord.records || !Array.isArray(newRecord.records)) {
                return false;
            }
            // 2. 获取现有历史记录
            let historyRecords = this.getHistoryRecords().map(item => {
                // 移除$index字段(避免存储冗余)
                const {
                    $index,
                    ...rest
                } = item;
                return rest;
            });
            // 3. 检查新记录是否与历史记录前缀匹配(避免重复保存)
            const {
                isMatched,
                updatedHistory
            } = this._checkRecordMatch(historyRecords, newRecord);
            historyRecords = isMatched ? updatedHistory : [...historyRecords, newRecord];
 
            // 4. 按种子分组,保留每个种子的前三名(按分数降序)
            const recordsBySeed = {};
            historyRecords.forEach(record => {
                const seed = record.time;
                if (!recordsBySeed[seed]) recordsBySeed[seed] = [];
                recordsBySeed[seed].push(record);
            });
            // 处理每个种子的记录(去重+保留前三)
            Object.keys(recordsBySeed).forEach(seed => {
                // 去重:同一种子下相同分数的记录只保留一条
                const uniqueRecords = Array.from(
                    new Map(recordsBySeed[seed].map(r => [`${seed}_${r.score}`, r])).values()
                );
                // 按分数降序,保留前三名
                recordsBySeed[seed] = uniqueRecords
                    .sort((a, b) => b.score - a.score)
                    .slice(0, 3);
            });
 
            // 5. 提取所有种子的前三名,形成种子级保留池
            const seedLevelRecords = [];
            Object.values(recordsBySeed).forEach(seedRecords => {
                seedLevelRecords.push(...seedRecords);
            });
 
            // 6. 筛选全局前20名(按分数降序,去重)
            const uniqueAllRecords = Array.from(
                new Map(historyRecords.map(r => [`${r.time}_${r.score}`, r])).values()
            );
            const top20GlobalRecords = uniqueAllRecords
                .sort((a, b) => b.score - a.score)
                .slice(0, 20);
 
            // 7. 合并结果(种子前三 + 全局前20),去重后保存
            const finalRecordsMap = new Map();
            // 先添加种子级记录
            seedLevelRecords.forEach(record => {
                const key = `${record.time}_${record.score}`;
                if (!finalRecordsMap.has(key)) finalRecordsMap.set(key, record);
            });
            // 再添加全局前20(确保高分不丢失)
            top20GlobalRecords.forEach(record => {
                const key = `${record.time}_${record.score}`;
                if (!finalRecordsMap.has(key)) finalRecordsMap.set(key, record);
            });
            // 转换为数组并按分数降序排序
            const finalRecords = Array.from(finalRecordsMap.values())
                .sort((a, b) => b.score - a.score);
 
            // 处理数据后保存
            const savedStr = this._processDataBeforeSave(finalRecords);
            localStorage.setItem(this.STORAGE_KEYS.HISTORY_RECORDS, savedStr);
            return true;
        } catch (error) {
            return false;
        }
    },
 
    /**
     * 删除指定索引的历史记录
     * @param {number} $index - 记录在原始数组中的索引(从getHistoryRecords获取)
     * @returns {boolean} 删除结果(true=成功,false=失败)
     */
    deleteHistoryRecord($index) {
        try {
            // 1. 获取现有记录(不含$index)
            let historyRecords = this.getHistoryRecords().map(item => {
                const {
                    $index,
                    ...rest
                } = item;
                return rest;
            });
            // 2. 校验索引有效性
            if ($index < 0 || $index >= historyRecords.length) {
                return false;
            }
            // 3. 删除指定记录
            historyRecords.splice($index, 1);
            // 4. 保存更新后的记录
            localStorage.setItem(
                this.STORAGE_KEYS.HISTORY_RECORDS,
                JSON.stringify(historyRecords)
            );
            return true;
        } catch (error) {
            return false;
        }
    },
 
    /**
     * 清空所有历史记录
     * @returns {boolean} 清空结果(true=成功,false=失败)
     */
    clearAllHistoryRecords() {
        try {
            localStorage.removeItem(this.STORAGE_KEYS.HISTORY_RECORDS);
            return true;
        } catch (error) {
            return false;
        }
    },
 
    /**
     * 导出指定key的本地存储数据(按密钥规则处理)
     * @returns {string} 处理后的密文(当前key不存在时返回空字符串)
     */
    exportStorageData() {
        // 1. 校验当前key是否存在,不存在直接返回空字符串
        if (!window.key) return '';
 
        try {
            // 3. 获取原始存储数据
            const rawData = localStorage.getItem(this.STORAGE_KEYS.HISTORY_RECORDS);
            if (!rawData) return '';
 
            // 4. 判断数据类型(明文/密文)
            const isCiphertext = rawData.startsWith('0_') || rawData.startsWith('1_');
            let processedData = '';
 
            if (!isCiphertext) {
                // 明文:先加密,再用unpkg LZMA高压缩
                const originalData = JSON.parse(rawData);
                const encryptedStr = this._processDataBeforeSave(originalData);
                processedData = this._compressData(encryptedStr);
            } else {
                const prefix = rawData.slice(0, 2);
                if (prefix === '0_') {
                    // 0_密文:默认密钥解密 → 重新加密 → unpkg LZMA压缩
                    const base64Data = rawData.slice(2);
                    const decodedStr = decodeURIComponent(escape(atob(base64Data)));
                    const decryptedStr = this._xorDecrypt(decodedStr, 123456780);
                    const originalData = JSON.parse(decryptedStr);
                    const encryptedStr = this._processDataBeforeSave(originalData);
                    processedData = this._compressData(encryptedStr);
                } else if (prefix === '1_') {
                    // 1_密文:直接用unpkg LZMA压缩(无需重新加密,节省性能)
                    processedData = this._compressData(rawData);
                }
            }
            return processedData;
        } catch (error) {
            // 任何处理失败(如解析错误)均返回空字符串
            return '';
        }
    },
 
    /**
     * 合并密文到本地历史记录(仅处理前缀1_且可解析的密文)
     * @param {string} ciphertext - 待合并的密文(需前缀为1_)
     * @returns {boolean} 合并结果(成功=true,失败=false)
     */
    mergeCiphertextToHistory(ciphertext) {
        // 1. 前置校验:当前key不存在,直接返回失败
        if (!window.key) return false;
 
        try {
            // 尝试解压
            let decompressedCiphertext = this._decompressData(ciphertext);
            if (!decompressedCiphertext) return false;
            // 解压后的字符串前缀不是1_则返回失败
            if (!decompressedCiphertext.startsWith('1_')) return false;
            // 2. 解析密文(按1_前缀的密钥规则)
            const base64Data = decompressedCiphertext.slice(2);
            const decodedStr = decodeURIComponent(escape(atob(base64Data)));
            const decryptedStr = this._xorDecrypt(decodedStr, window.key);
            const newRecords = JSON.parse(decryptedStr);
 
            // 3. 校验解析结果是否为数组(历史记录需为数组格式)
            if (!Array.isArray(newRecords) || newRecords.length === 0) return false;
 
            // 4. 批量合并:逐条调用saveHistoryRecord的逻辑(复用去重和保留规则)
            // 先获取现有记录(移除$index)
            let existingRecords = this.getHistoryRecords().map(item => {
                const {
                    $index,
                    ...rest
                } = item;
                return rest;
            });
 
            // 遍历新记录,逐条处理(复用前缀匹配、种子分组、全局排序逻辑)
            newRecords.forEach(newRecord => {
                // 1. 校验新记录格式(至少包含种子和操作记录)
                if (!newRecord.time || !newRecord.records || !Array.isArray(newRecord.records)) {
                    return false;
                }
                // 3. 检查新记录是否与历史记录前缀匹配(避免重复保存)
                const {
                    isMatched,
                    updatedHistory
                } = this._checkRecordMatch(existingRecords, newRecord);
                existingRecords = isMatched ? updatedHistory : [...existingRecords, newRecord];
 
                // 4. 按种子分组,保留每个种子的前三名(按分数降序)
                const recordsBySeed = {};
                existingRecords.forEach(record => {
                    const seed = record.time;
                    if (!recordsBySeed[seed]) recordsBySeed[seed] = [];
                    recordsBySeed[seed].push(record);
                });
                // 处理每个种子的记录(去重+保留前三)
                Object.keys(recordsBySeed).forEach(seed => {
                    // 去重:同一种子下相同分数的记录只保留一条
                    const uniqueRecords = Array.from(
                        new Map(recordsBySeed[seed].map(r => [`${seed}_${r.score}`, r])).values()
                    );
                    // 按分数降序,保留前三名
                    recordsBySeed[seed] = uniqueRecords
                        .sort((a, b) => b.score - a.score)
                        .slice(0, 3);
                });
 
                // 5. 提取所有种子的前三名,形成种子级保留池
                const seedLevelRecords = [];
                Object.values(recordsBySeed).forEach(seedRecords => {
                    seedLevelRecords.push(...seedRecords);
                });
 
                // 6. 筛选全局前20名(按分数降序,去重)
                const uniqueAllRecords = Array.from(
                    new Map(existingRecords.map(r => [`${r.time}_${r.score}`, r])).values()
                );
                const top20GlobalRecords = uniqueAllRecords
                    .sort((a, b) => b.score - a.score)
                    .slice(0, 20);
 
                // 7. 合并结果(种子前三 + 全局前20),去重后保存
                const finalRecordsMap = new Map();
                // 先添加种子级记录
                seedLevelRecords.forEach(record => {
                    const key = `${record.time}_${record.score}`;
                    if (!finalRecordsMap.has(key)) finalRecordsMap.set(key, record);
                });
                // 再添加全局前20(确保高分不丢失)
                top20GlobalRecords.forEach(record => {
                    const key = `${record.time}_${record.score}`;
                    if (!finalRecordsMap.has(key)) finalRecordsMap.set(key, record);
                });
                // 转换为数组并按分数降序排序
                existingRecords = Array.from(finalRecordsMap.values())
                    .sort((a, b) => b.score - a.score);
            });
 
            // 6. 保存合并后的记录(加密存储)
            const savedStr = this._processDataBeforeSave(existingRecords);
            localStorage.setItem(this.STORAGE_KEYS.HISTORY_RECORDS, savedStr);
            return true;
        } catch (error) {
            // 任何解析/合并错误均返回失败
            return false;
        }
    },
 
    // ========================== 内部工具方法(私有,不对外暴露)==========================
    /**
     * 检查新记录与历史记录是否前缀匹配(避免重复保存)
     * @param {Array<Object>} historyRecords - 历史记录数组
     * @param {Object} newRecord - 新记录
     * @returns {Object} 匹配结果
     * @property {boolean} isMatched - 是否匹配
     * @property {Array<Object>} updatedHistory - 更新后的历史记录数组
     */
    _checkRecordMatch(historyRecords, newRecord) {
        // 只对比同一种子的记录
        const sameSeedRecords = historyRecords.filter(item => item.time === newRecord.time);
        if (sameSeedRecords.length === 0) {
            return {
                isMatched: false,
                updatedHistory: historyRecords
            };
        }
        // 遍历同一种子的记录,检查操作序列是否前缀匹配
        for (let i = 0; i < sameSeedRecords.length; i++) {
            const historyItem = sameSeedRecords[i];
            const historyIndex = historyRecords.indexOf(historyItem);
            // 检查前缀匹配(一方是另一方的前面部分且完全相同)
            if (this._checkPrefixMatch(historyItem.records, newRecord.records)) {
                // 新记录操作数更多时,替换旧记录
                if (newRecord.records.length >= historyItem.records.length) {
                    historyRecords[historyIndex] = newRecord;
                }
                return {
                    isMatched: true,
                    updatedHistory: historyRecords
                };
            }
        }
        return {
            isMatched: false,
            updatedHistory: historyRecords
        };
    },
 
    /**
     * 检查两个操作序列是否为前缀匹配
     * @param {Array<string>} arr1 - 历史操作序列
     * @param {Array<string>} arr2 - 新操作序列
     * @returns {boolean} 是否前缀匹配
     */
    _checkPrefixMatch(arr1, arr2) {
        const minLength = Math.min(arr1.length, arr2.length);
        // 对比最短长度内的所有操作
        for (let i = 0; i < minLength; i++) {
            if (arr1[i] !== arr2[i]) return false;
        }
        return true;
    },
 
    // ========================== 新增:数据压缩/加密/编码工具函数 ===========================
    /**
     * 异或加密(效率高,不增加体积)
     * @param {string} data - 待加密的字符串
     * @returns {string} 加密后的字符串(UTF-8编码)
     */
    _xorEncrypt(data, key = 1234567890) {
        let encrypted = [];
        for (let i = 0; i < data.length; i++) {
            // 字符ASCII码与key异或,再转成字符
            const charCode = data.charCodeAt(i) ^ key;
            encrypted.push(String.fromCharCode(charCode));
        }
        return encrypted.join('');
    },
 
    /**
     * 异或解密(对应加密逻辑,密钥需与加密一致)
     * @param {string} encryptedData - 加密后的字符串
     * @returns {string} 解密后的原始字符串
     */
    _xorDecrypt(encryptedData, key) {
        // 异或加密和解密逻辑相同,直接复用加密方法
        return this._xorEncrypt(encryptedData, key);
    },
 
    /**
     * 数据处理统一入口(加密→Base64编码)
     * @param {Object/Array} data - 待处理的原始数据(如配置对象、记录数组)
     * @returns {string} 最终处理后的字符串(用于存储)
     */
    _processDataBeforeSave(data) {
        // 1. 转JSON字符串
        const jsonStr = JSON.stringify(data);
        // 2. 异或加密(简单保护,不增加体积)
        const encryptedStr = this._xorEncrypt(jsonStr, window.key ? window.key : 123456780);
        // 3. Base64编码(避免特殊字符,便于存储)
        return (window.key ? '1_' : '0_') + btoa(unescape(encodeURIComponent(encryptedStr)));
    },
 
    /**
     * 数据解析统一入口(Base64解码→解密)
     * @param {string} savedStr - 存储的字符串
     * @returns {Object/Array|null} 解析后的原始数据(失败返回null)
     */
    _parseDataAfterLoad(savedStr) {
        try {
            // 1. 提取密钥标识(开头的"1_"或"0_")和真实Base64数据
            const prefix = savedStr.slice(0, 2); // 获取前两位字符("1_"或"0_")
            const base64Data = savedStr.slice(2); // 移除前缀后的Base64数据
 
            // 2. 根据前缀匹配对应的解密密钥
            let decryptKey;
            if (prefix === '1_') {
                // "1_" 表示加密时用了window.key
                decryptKey = window.key ? window.key : 123456780; // 兼容window.key意外不存在的情况
            } else if (prefix === '0_') {
                // "0_" 表示加密时用了默认密钥
                decryptKey = 123456780;
            } else {
                // 前缀不匹配(如旧数据无前缀),直接抛错进入兼容逻辑
                throw new Error('无效的密钥标识前缀');
            }
 
            // 3. 正常执行解码和解密流程
            const decodedStr = decodeURIComponent(escape(atob(base64Data))); // Base64解码
            const decryptedStr = this._xorDecrypt(decodedStr, decryptKey); // 异或解密(使用匹配的密钥)
            return JSON.parse(decryptedStr); // JSON解析并返回
        } catch (error) {
            // 解析失败(如前缀异常、Base64错误、解密失败),返回null触发旧数据兼容逻辑
            return null;
        }
    },
 
    // ========================== 数据压缩/解压工具(基于 unpkg 的 pako 库)==========================
    /**
     * pako高压缩(使用deflate算法,level=9为最高级别)
     * @param {string} rawData - 待压缩的原始字符串(如加密后的JSON字符串)
     * @returns {string} 压缩后的Base64字符串(避免二进制数据存储/传输问题)
     */
    _compressData(rawData) {
        try {
            // 1. 将字符串转换为Uint8Array
            const uint8Array = new TextEncoder().encode(rawData);
 
            // 2. pako压缩:使用deflate算法,level=9(最高压缩级别)
            const compressedUint8 = pako.deflate(uint8Array, {
                level: 9
            });
 
            // 3. 转换为Base64:解决二进制数据的特殊字符问题,便于文本框显示和复制
            return btoa(String.fromCharCode(...compressedUint8));
        } catch (error) {
            return ''; // 压缩失败返回空字符串,上层逻辑会提示用户
        }
    },
 
    /**
     * pako解压(对应压缩逻辑,仅处理pako压缩的数据)
     * @param {string} compressedBase64 - 压缩后的Base64字符串(含"COMPRESSED_"前缀的部分)
     * @returns {string} 解压后的原始字符串(如加密后的密文)
     */
    _decompressData(compressedBase64) {
        try {
            // 1. Base64解码:将文本格式的Base64转换为二进制Uint8Array
            const compressedUint8 = new Uint8Array(
                atob(compressedBase64)
                .split('')
                .map(char => char.charCodeAt(0))
            );
 
            // 2. pako解压:使用inflate算法(同步解压,文本数据体积小,无性能压力)
            const decompressedUint8 = pako.inflate(compressedUint8);
 
            // 3. 将Uint8Array转换为字符串
            return new TextDecoder().decode(decompressedUint8);
        } catch (error) {
            return ''; // 解压失败返回空字符串,上层逻辑会提示用户核对数据
        }
    },
};
 
// 浏览器环境(挂载到window全局变量)
window.localStorageUtil = localStorageUtil;
除非特别注明,本页内容采用以下授权方式: Creative Commons Attribution-ShareAlike 3.0 License