tsg-ut/mnemo

View on GitHub
lib/validator.js

Summary

Maintainability
D
1 day
Test Coverage
const assert = require('assert');
const blockConfigs = require('./block-configs');
const stages = require('../stages');
const Board = require('./board');
const {arrayEquals, zip} = require('./util');

const blockNames = Object.keys(blockConfigs);

// https://github.com/tsg-ut/mnemo/wiki/%E3%82%B9%E3%82%B3%E3%82%A2%E3%81%AE%E4%BB%95%E6%A7%98#%E8%A8%88%E7%AE%97%E5%BC%8F
module.exports.calculateScore = ({clocks: C, blocks: B, stage}) => {
    const {clockLimit: C_L, width: W, height: H} = stage;

    const P_C = Math.floor(500 * Math.exp(2 * (H - C) / (C_L - 2 * H) * Math.log(5 / 2))) * 10;
    const P_B = Math.floor(500 * Math.exp((H - B) / (H * (W - 1)) * Math.log(5 / 2))) * 10;

    return P_C + P_B;
};

module.exports.validateSubmission = (submission) => {
    if (typeof submission.stage !== 'string') {
        return {pass: false, message: 'stage data is missing'};
    }

    const stage = stages.find((s) => s.name === submission.stage);

    if (typeof stage !== 'object') {
        return {pass: false, message: 'stage data is invalid'};
    }

    if (!Array.isArray(submission.board)) {
        return {pass: false, message: 'board should be an array'};
    }

    if (submission.board.length === 0) {
        return {pass: false, message: 'board must at least have one block'};
    }

    let error = null;

    submission.board.forEach((block, index) => {
        const message = (() => {
            if (!arrayEquals(Object.keys(block), ['x', 'y', 'type', 'rotate'])) {
                return `keys of block ${index} are invalid`;
            }

            if (!Number.isInteger(block.x)) {
                return `x position of block ${index} should be an integer`;
            }

            if (block.x < 0 || stage.width <= block.x) {
                return `x position of block ${index} exceeds the board size`;
            }

            if (!Number.isInteger(block.y)) {
                return `y position of block ${index} should be an integer`;
            }

            if (block.y < 0 || stage.height <= block.y) {
                return `y position of block ${index} exceeds the board size`;
            }

            if (typeof block.type !== 'string') {
                return `type of block ${index} should be a string`;
            }

            if (!blockNames.includes(block.type)) {
                return `type of block ${index} is unknown`;
            }

            if (!Number.isInteger(block.rotate)) {
                return `rotation of block ${index} should be an integer`;
            }

            if (block.rotate < 0 || 4 <= block.rotate) {
                return `rotation of block ${index} is invalid`;
            }

            return null;
        })();

        if (message !== null) {
            error = message;
        }
    });

    if (error) {
        return {pass: false, message: error};
    }

    // Validate uniqueness of the block positions
    const positions = new Set();
    submission.board.forEach((block) => {
        const index = block.y * stage.width + block.x;
        positions.add(index);
    });

    if (positions.size !== submission.board.length) {
        return {pass: false, message: 'positions of blocks are not unique'};
    }

    // Validate if the only usable blocks are used
    const blockCounts = new Map();
    submission.board.forEach((block) => {
        if (blockCounts.has(block.type)) {
            blockCounts.set(block.type, blockCounts.get(block.type) + 1);
        } else {
            blockCounts.set(block.type, 1);
        }
    });

    for (const [type, count] of blockCounts.entries()) {
        if (!{}.hasOwnProperty.call(stage.parts, type)) {
            return {pass: false, message: 'used blocks are not matching to the stage'};
        }

        const allowedCount = stage.parts[type];

        if (allowedCount !== null && count > allowedCount) {
            return {pass: false, message: 'used blocks are not matching to the stage'};
        }
    }

    // Validate if this passes test cases

    const board = new Board({
        width: stage.width,
        height: stage.height,
        inputX: stage.inputX,
        outputX: stage.outputX,
        clockLimit: stage.clockLimit,
    });

    submission.board.forEach((block) => {
        board.placeBlock(block);
    });

    let maxClock = 0;

    const {inputs, outputs} = (() => {
        if (stage.ioGenerator) {
            const random = board.getSeededRandom();
            const {input, output} = stage.ioGenerator(random);
            assert(Array.isArray(input));
            assert(Array.isArray(output));

            return {
                inputs: input,
                outputs: output,
            };
        }

        return {
            inputs: stage.input,
            outputs: stage.output,
        };
    })();

    for (const [input, output] of zip(inputs, outputs)) {
        board.run(input);

        if (board.outputValue !== output) {
            return {pass: false, message: 'the board cannot clear the stage'};
        }

        maxClock = Math.max(maxClock, board.clock);
    }

    return {pass: true, clocks: maxClock, blocks: board.weighedBlockCount};
};