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.",
    TileIdsNotUnique = "Some tiles have the same id.",
    StartPositionNotOnMap = "The starting tile has to be on the map.",
    InvalidStartPosition = "The starting tile needs to have pathways.",
}

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

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

    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 getTile(tileID: string): MazeTile
    {
        for (const row of this.layout) {
            for (const tile of row) {
                if(tile.id === tileID){
                    return tile;
                }
            }
        }
        return null;
    }

    public getStartTile(): MazeTile
    {
        return this.layout[this.startPosition[0]][this.startPosition[1]];
    }

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

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

        let hasInvalidTiles = false;
        let hasNullTiles = false;
        let hasIDDuplicates = 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(!hasIDDuplicates){
                    for (const row2 of this.layout) {
                        if(row2 == null){
                            continue;
                        }
                        for (const tile2 of row2) {
                            if(tile2 == null || tile2 === tile)
                            {
                                continue;
                            }
                            if(tile2.id === tile.id)
                            {
                                hasIDDuplicates = true;
                            }
                        }
                    }
                }
            }
        }
        if(hasInvalidTiles)
        {
            errors.push(MazeValidationError.InvalidTiles);
        }
        if(hasNullTiles)
        {
            errors.push(MazeValidationError.NoHolesAllowed);
        }
        if(hasIDDuplicates)
        {
            errors.push(MazeValidationError.TileIdsNotUnique);
        }

        let hasWrongShape = false;
        if(this.layout.length === 0)
        {
            errors.push(MazeValidationError.WrongShape);
            hasWrongShape = true;
        }
        else
        {
            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);
            }
        }

        if(this.layout != null){
            if(this.startPosition[0] < 0 || this.startPosition[0] >= this.layout.length)
            {
                errors.push(MazeValidationError.StartPositionNotOnMap);
            }
            else if(this.layout.length>0
                && (this.startPosition[1] < 0 || this.startPosition[1] >= this.layout[0].length))
            {
                errors.push(MazeValidationError.StartPositionNotOnMap);
            }
            else if(this.layout.length > 0
                && this.layout[this.startPosition[0]][this.startPosition[1]] != null)
            {
                const start = this.layout[this.startPosition[0]][this.startPosition[1]];
                let hasPaths = false;
                for (const dir of start.paths) {
                    if(dir !== Direction.none){
                        hasPaths = true;
                        break;
                    }
                }
                if(!hasPaths)
                {
                    errors.push(MazeValidationError.InvalidStartPosition);
                }
            }
        }

        return errors;
    }
}