Newer
Older
Labyrinth / src / MazeMap.ts
export enum Direction
{
    none = 0,
    top = 1 << 0,
    left = 1 << 1,
    bottom = 1 << 2,
    right = 1 << 3,
}

export function OppositeDirection(dir:Direction): Direction
{
    switch (dir) {
        case Direction.top:     return Direction.bottom;
        case Direction.bottom:  return Direction.top;
        case Direction.right:   return Direction.left;
        case Direction.left:    return Direction.right;
        case Direction.none:    return Direction.none;
        default:
            throw new Error(`"${dir}" does not have a recognized opposite.`);
    }
}

function DirectionToVector(dir:Direction): number[]
{
    switch (dir) {
        case Direction.top:     return [-1, 0];
        case Direction.left:    return [0, -1];
        case Direction.bottom:  return [1, 0];
        case Direction.right:   return [0, 1];
        default: return [0, 0];
    }
}

export enum TileValidationError
{
    InvalidID = "Id must not be null or empty.",
    NoPathways = "Tile must have at least one pathway.",
}

export class MazeTile
{
    public paths:Direction[];
    public id:string;

    constructor(id:string)
    {
        this.id = id;
        this.paths = [];
    }

    public getRepresentation(): number
    {
        let output = Direction.none;
        for (const dir of this.paths) {
            output |= dir;
        }
        return output;
    }

    public validate(): TileValidationError[]
    {
        const errors: TileValidationError[] = [];

        if(this.id == null || this.id === "")
        {
            errors.push(TileValidationError.InvalidID);
        }

        if(this.paths.length === 0)
        {
            errors.push(TileValidationError.NoPathways);
        }

        return errors;
    }

    public hasDirection(dir:Direction): boolean
    {
        for (const d of this.paths) {
            if(d === dir)
            {
                return true;
            }
        }
        return false;
    }
}

export enum MazeValidationError
{
    InvalidID = "Id must not be null or empty.",
    InvalidTiles = "Maze contains invalid tiles.",
    PathWithoutDestination = "Pathways must have a corresponding destination tile.",
    WrongShape = "Maze must be shaped like a rectangle.",
    NoHolesAllowed = "Maze has null-tiles.",
}

export class MazeMap
{
    public id:string;
    public layout:MazeTile[][];

    constructor(id:string)
    {
        this.id = id;
        this.layout = [];
    }

    public getRepresentation(): number[][]
    {
        const output:number[][] = [];
        for (const row of this.layout) {
            const newRow:number[] = [];
            for (const cell of row) {
                newRow.push(cell.getRepresentation());
            }
            output.push(newRow);
        }
        return output;
    }

    public validate(): string[]
    {
        const errors:string[] = [];

        if(this.id == null || this.id === "") {
            errors.push(MazeValidationError.InvalidID);
        }

        let hasInvalidTiles = false;
        let hasNullTiles = false;
        for (const row of this.layout) {
            if(row == null)
            {
                hasNullTiles = true;
                continue;
            }
            for (const tile of row) {
                if(tile == null)
                {
                    hasNullTiles = true;
                    continue;
                }
                if(tile.validate().length > 0)
                {
                    hasInvalidTiles = true;
                }
            }
        }
        if(hasInvalidTiles)
        {
            errors.push(MazeValidationError.InvalidTiles);
        }
        if(hasNullTiles)
        {
            errors.push(MazeValidationError.NoHolesAllowed);
        }

        let hasWrongShape = false;
        for (const row of this.layout) {
            if(row == null || row.length !== this.layout[0].length)
            {
                errors.push(MazeValidationError.WrongShape);
                hasWrongShape = true;
                break;
            }
        }

        // Only do the complex path validation if there are no violations of shape requirements.
        if(!hasNullTiles && !hasWrongShape)
        {
            let hasPathsWithoutDestination = false;
            for(let i=0;i<this.layout.length && !hasPathsWithoutDestination;i++)
            {
                for(let j=0;j<this.layout[i].length && !hasPathsWithoutDestination;j++)
                {
                    for(let k=0;k<this.layout[i][j].paths.length && !hasPathsWithoutDestination;k++)
                    {
                        const dir = this.layout[i][j].paths[k];
                        if(dir === Direction.none)
                        {
                            continue;
                        }
                        const [x, y] = DirectionToVector(dir);
                        const opposite = OppositeDirection(dir);

                        if(i+x < 0 || i+x >= this.layout.length || j+y < 0 || j+y >= this.layout[i+x].length)
                        {
                            hasPathsWithoutDestination = true;
                            break;
                        }

                        if(!this.layout[i+x][j+y].hasDirection(opposite))
                        {
                            hasPathsWithoutDestination = true;
                            break;
                        }
                    }
                }
            }
            if(hasPathsWithoutDestination)
            {
                errors.push(MazeValidationError.PathWithoutDestination);
            }
        }

        return errors;
    }
}