モンスター図鑑埋め作業

図鑑はやはり埋めておこうと、配合作業をしてます。貯まっていた通信コインを使って一気に作業するのに、らくらく配合の機能を使うと便利。一方で位階配合は攻略本や攻略サイトを見ても出てないし、3DS上でコイン引換所のパンドラチェスト前とらくらく配合のエース君の前を行ったり来たりするのも面倒なので位階配合レシピを計算するプログラム書きました。実はプログラマーなんで。

ガンガン埋めてっても漏れる傾向にあったのはやはり配合できないやつや複雑なやつ。SSを除けば、なげきムーン・ドラゴメタルパンドラボックスを残してました。なげきムーンは草原の塔の屋上に行って、夜&晴天にするとすぐ出てきます。また、ドラゴメタルも黒鉄の屋上に行って、昼&晴天にすると出現しました。パンドラボックスはまだ捕獲していない。

興味ある&プログラムわかる方向けに位階配合とるためのコードをさらしておきます。ノーサポートで使える方はご自由に利用ください。Node 5.xで動くECMAScript6で書いてます。面倒くさいことに、今回の位階システムは一旦全部ケースを洗わないと適正な組み合わせが取れない(と思う)ので、一発での逆引きができず面倒。らくらく配合検索でも時間かかってますよね。あれはさすがに中に配合レシピをキャッシュしてるんだと思うんだけど、それでもあれだけ時間かかってる。とりあえず、仕事じゃないので動けばいいやと。

'use strict';

const getDigits = (id) => {
    const base = Math.floor(id / 10);
    return [base > 34 ? 350 : (base + 1) * 10, id - base * 10];
};

const checkAndDownCourt = (list, id) => {
    for(let i = id; i > 0; i--) {
        if(!list[i].specialOnly) {
            return i;
        }
    }
    return 0;
};

const hitTest = (combination, lower, upper) => {
    if(combination.length === 4) {
        return 0; // special x 4
    }
    if(combination.length === 2) {
        if(isNaN(combination[1])) {
            if((combination[0] === lower.id && combination[1] === upper.descent) ||
                (combination[0] === upper.id && combination[1] === lower.descent)) {
                return 1; // Low priority
            }
        } else if(combination[0] === lower.id && combination[1] === upper.id) {
            return 2; // High priority
        }
    }
    return 0; // Didn't hit
};

const getSpecialCase = (list, lower, upper) => {
    return list.reduce((result, target) => {
        const special = target.special;
        if(Array.isArray(special)) {
            for(let combination of special) {
                const hit = hitTest(combination, lower, upper);
                if(hit === 2) {
                    return [target.id].concat(result);
                } else if(hit === 1) {
                    return [].concat(result).concat(target.id);
                }
            }
        }
        return result;
    }, []);
};

const getCourtBreed = (list, lower, upper) => {
    const lowerDigit = getDigits(lower.id);
    const upperDigit = getDigits(upper.id);
    var r1 = checkAndDownCourt(list, upperDigit[0] + getDigits(upperDigit[1] + lowerDigit[1])[1]);
    var r2 = checkAndDownCourt(list, lowerDigit[0] + getDigits(upperDigit[1] + lowerDigit[1] + 5)[1]);
    if(r1 === r2) {
        r2 = checkAndDownCourt(list, r2 - 1);
    }
    var r3 = checkAndDownCourt(list, Math.floor((upper.id + lower.id) / 2));
    if(r1 === r3) {
        r3 = checkAndDownCourt(list, r3 - 1);
    }
    if(r2 === r3) {
        r3 = checkAndDownCourt(list, r3 - 1);
    }

    var result = [r1, r2, r3];

    // Special Case
    const specialCase = getSpecialCase(list, lower, upper);
    if(specialCase.length >= 2) {
        result[1] = specialCase[0];
        result[2] = specialCase[1];
    } else if(specialCase.length === 1) {
        result[2] = specialCase[0];
    }

    // finishing
    return result.reduce((result, r) => {
        if(r > 0) {
            return [].concat(result).concat(list[r]);
        }
        return result;
    }, []);
};

const likeTarget = (list, str) => {
    const hiraToKata = (src) => {
        return src.replace(/[\u3041-\u3096]/g, function(match) {
            var chr = match.charCodeAt(0) + 0x60;
            return String.fromCharCode(chr);
        });
    };
    const test = hiraToKata(str);
    for(let i = list.length - 1; i > 0; i--) {
        const target = list[i];
        if(target.name == str || target.reading == test) {
            return Object.assign({search: str}, target);
        }
    }
    // retry
    for(let i = list.length - 1; i > 0; i--) {
        const target = list[i];
        if(~target.name.indexOf(str) || ~target.reading.indexOf(test)) {
            return Object.assign({search: str}, target);
        }
    }
    return null;
};

const selectCombinationsFromTarget = (list, target) => {
    return new Promise((resolve) => {
        var results = [];
        if(target.specialOnly) {
            results = target.special.map((combination) => {
                return combination.map((id) => {
                    return Object.assign({}, list[id]);
                });
            });
        } else {
            // FIX: into DB because it spends over 20 seconds.
            for (let i = 1; i < list.length; i++) {
                const lower = list[i];
                for (let j = i; j < list.length; j++) {
                    const upper = list[j];
                    const combinations = getCourtBreed(list, lower, upper);
                    for (let k = 0; k < combinations.length; k++) {
                        if (combinations[k].id === target.id) {
                            results.push([lower, upper]);
                        }
                    }
                }
            }
        }
        resolve(results);
    });
};

export {
    getCourtBreed,
    likeTarget,
    selectCombinationsFromTarget
};

こっちはXLSXで作ったマスターデータを読み込むもの

'use strict';

import XLSX from 'xlsx';

const filePath = __dirname + '/CourtRank.xlsx';
const MAX_A_RANK_ID = 397;

const importFromXlsx = (max = MAX_A_RANK_ID) => {
    const book = XLSX.readFile(filePath);
    const sheet = book.Sheets['Sheet1'];
    let list = [{id: 0, name: '', rank: '', descent: '', scout: [], specialOnly: true, special: []}]; // index 0
    for(let i = 1; i <= max; i++) {
        var scout = [];
        const scoutCell = sheet['M' + i];
        if(scoutCell) {
            scout = scoutCell.v.split('_');
        }
        const special = ['O', 'P', 'Q', 'R'].reduce((special, prefix) => {
            const cell = sheet[prefix + i];
            if(cell) {
                const value = cell.v;
                const combination = value.split('_');
                if(combination.length >= 2) {
                    const result = combination.map((str) => {
                        const num = parseInt(str, 10);
                        if(isNaN(num)) {
                            return str;
                        }
                        return num;
                    });
                    return [result].concat(special);
                }
            }
            return special;
        }, []);
        list.push({
            id: sheet['A' + i].v,
            name: sheet['B' + i].v,
            reading: sheet['C' + i].v,
            rank: sheet['D' + i].v,
            size: sheet['E' + i].v,
            descent: sheet['F' + i].v,
            scout: scout,
            specialOnly: sheet['N' + i].v,
            special: special
        });
    }
    return list;
};

export {importFromXlsx};

あとは、Excelで、こんなの作ってる。これは某攻略サイトのページをスクレイピングしてベース作った。スクレイピングのコードはどこのサイトから剥がしたかバレちゃうので内緒。

f:id:iogrande:20160412023600p:plain

使い方は、WEBページに組み込んで使ってるんだけど、一番シンプルなものとしてテストコードで。

'use strict';

import {importFromXlsx} from '../Importer';
import {getCourtBreed, likeTarget, selectCombinationsFromTarget} from '../Breeder';

describe('Breeder & Importer', () => {

    it('breed', () => {
        const list = importFromXlsx();
        const result = getCourtBreed(list, list[145], list[302]);
        expect([result[0].id, result[1].id, result[2].id]).toEqual([316, 152, 359]);
    });

    it('likeTargetName by Name', () => {
        const list = importFromXlsx(548);
        expect(likeTarget(list, '伐採マシン').id).toBe(343);
    });

    it('likeTargetName by Reading', () => {
        const list = importFromXlsx(548);
        expect(likeTarget(list, 'コクリュウマル').id).toBe(366);
    });

    it('likeTargetName by Part', () => {
        const list = importFromXlsx(548);
        expect(likeTarget(list, 'ゾーマ').id).toBe(521);
    });

    it('selectCombinationsFromTarget', (done) => {
        const list = importFromXlsx(548);
        selectCombinationsFromTarget(list, list[521]).then((results) => {
            expect([results[0][0].id, results[0][1].id]).toEqual([428, 502]);
            done();
        });
    });

});

どこぞのPaaS使ってサービス提供&公開してもいいんだけど、課金もかかるし、サーバ代のためにアフィリエイトとか貼るとすぐ嫌われそうだし。とりあえず自分の為だけのサービスにしてます。