tom-weatherhead/othello-angular-electron

View on GitHub
src/app/components/othello/othello.component.ts

Summary

Maintainability
A
3 hrs
Test Coverage
F
19%
// othello-angular-electron/src/app/components/othello/othello.component.ts

import {
    // AfterViewChecked,
    // AfterViewInit,
    // ChangeDetectorRef,
    Component,
    ElementRef,
    OnInit,
    ViewChild
} from '@angular/core';

import { /* ActivatedRoute, ParamMap, */ Router } from '@angular/router';
// import { Location }                         from '@angular/common';

// import { MatSelectChange } from '@angular/material/select';

// import { of, timer } from 'rxjs';
// import { catchError } from 'rxjs/operators';

// From https://github.com/angular/angular-cli/wiki/stories-third-party-lib :
// First, add this line to src/typings.d.ts :
// declare module 'image-processing-js';
// Then:
// import * as imageProcessingJs from 'image-processing-js';

import {
    createInitialState,
    IGameState,
    moveAutomatically,
    moveManually,
    PlayerColour
} from 'thaw-reversi-engine.ts';
import { createAndFillArray } from 'thaw-common-utilities.ts';

const boardWidth = 8;
const boardHeight = boardWidth;

const boardSquareWidth = 50;
const boardSquareHeight = boardSquareWidth;

const colourWhite = '#ffffff';
const colourBlack = '#000000';
const colourGrey = '#7f7f7f';
const colourGold = '#d4af37';
// const colourGold = '#ffd700';

@Component({
    // selector: 'app-othello',
    selector: 'app-root',
    templateUrl: './othello.component.html',
    styleUrls: ['./othello.component.scss']
})
// export class AppComponent implements AfterViewChecked, AfterViewInit, OnInit {
export class OthelloComponent implements OnInit {
    @ViewChild('canvas', { static: true })
    canvas: ElementRef<HTMLCanvasElement>;

    board: string[][]; // Model
    gameState: IGameState; // Model
    context: CanvasRenderingContext2D; // View

    mapTokenCharToPlayerColourName = {
        'X': 'White',
        'O': 'Black',
        ' ': 'Empty'
    };
    mapPlayerColourNameToBoolean = {
        White: true,
        Black: false
    };
    // automaticMove: Record<string, boolean> = {
    //     X: false,
    //     O: true
    // };
    automaticMove: Record<string, boolean> = {
        White: false,
        Black: true
    };
    playerPly: Record<string, number> = {
        White: 5,
        Black: 5
    };
    // populations: any = {
    //     X: 2,
    //     O: 2
    // };
    blackPopulation = 2;
    whitePopulation = 2;
    lastMoveWasInvalid: boolean;
    isGameOver: boolean;
    showMessage = false;
    message = '';
    doOneAutomove = false;

    public optionsInPlyDDL = [4, 5, 6];
    public selectedPly = 5;
    public messageInPlyDDL: string;

    // constructor(private cd: ChangeDetectorRef) {
    constructor(
        // private changeDetectorRef: ChangeDetectorRef,
        // private route: ActivatedRoute,
        private router: Router /*,
        private location: Location */
    ) {
        this.messageInPlyDDL = `Ply: ${this.selectedPly}`;
    }

    ngOnInit(): void {
        this.context = this.canvas.nativeElement.getContext('2d');
        this.onNewGame();
    }

    // ngAfterViewInit() {
    // }

    // ngAfterViewChecked() {
    // }

    // public onChangeSymbol(event: MatSelectChange): void {
    // changeMessageInPlyDDL(selectedPly: number): void {
    // public changeMessageInPlyDDL(event: MatSelectChange): void {
    // public changeMessageInPlyDDL(): void {
    //     // console.log('changeMessageInPlyDDL(): event is', typeof event, event);
    //     // console.log(
    //     //     'changeMessageInPlyDDL(): event.value is',
    //     //     typeof event.value,
    //     //     event.value
    //     // );
    //     // console.log('this.selectedPly:', this.selectedPly);

    //     this.messageInPlyDDL = `Ply: ${this.selectedPly}`;
    //     this.playerPly.White = this.selectedPly;
    //     this.playerPly.Black = this.selectedPly;
    // }

    displayMessage(message: string): void {
        this.message = message;
        this.showMessage = !!message; // or message && message.length;
    }

    clearCanvas(): void {
        // TODO: Find some way to invalidate the entire canvas
        // in order to ensure that no artifacts are visible when (most of) the pieces are removed.

        // Test 1:
        // this.canvas.nativeElement.width = this.canvas.nativeElement.width;

        // Test 2:
        this.context.fillStyle = colourGold;
        this.context.fillRect(
            0,
            0,
            this.canvas.nativeElement.width,
            this.canvas.nativeElement.height
        );
        this.context.stroke(); // Actually draw the shapes that are described above.
    }

    onNewGame(): void {
        this.lastMoveWasInvalid = false;
        this.isGameOver = false;
        this.gameState = createInitialState();

        this.board = null; // This will force the board to be reconstructed.
        this.clearCanvas();

        this.updateBoardFromGameState();
        this.displayMessage(null);
        this.onAutomaticMove();
    }

    renderSquareOnBoard(row: number, col: number, isWhite: boolean): void {
        const squareBorderThickness = 2;
        const xOffset = col * boardSquareWidth;
        const yOffset = row * boardSquareHeight;

        // Draw the square's border.
        this.context.fillStyle = colourGold;
        this.context.fillRect(xOffset, yOffset, boardSquareWidth, boardSquareHeight);

        // Fill the interior of the square.
        this.context.fillStyle = colourGrey;
        this.context.fillRect(
            xOffset + 2,
            yOffset + 2,
            boardSquareWidth - 2 * squareBorderThickness,
            boardSquareHeight - 2 * squareBorderThickness
        );

        if (isWhite === true || isWhite === false) {
            const centerX = boardSquareWidth / 2;
            const centerY = boardSquareHeight / 2;
            const radius = Math.floor((boardSquareWidth * 2) / 5);
            const fillColour = isWhite ? colourWhite : colourBlack;

            this.context.beginPath();
            this.context.arc(xOffset + centerX, yOffset + centerY, radius, 0, 2 * Math.PI, false);
            this.context.fillStyle = fillColour;
            this.context.strokeStyle = fillColour;
            this.context.fill();
        }

        this.context.stroke(); // Actually draw the shapes that are described above.
    }

    updateBoardFromGameState(): void {
        if (!this.board) {
            // this.board = createAndFillArray(
            //     '',
            //     boardHeight,
            //     boardWidth
            // );
            this.board = createAndFillArray('', boardHeight, boardWidth) as string[][];
        }

        for (let row = 0; row < this.board.length; row++) {
            for (let col = 0; col < this.board[row].length; col++) {
                const currentColourName = this.board[row][col];
                const newColourName =
                    this.mapTokenCharToPlayerColourName[
                        this.gameState.boardAsString[row * this.board[row].length + col]
                    ];

                if (newColourName !== currentColourName) {
                    this.board[row][col] = newColourName;
                    this.renderSquareOnBoard(
                        row,
                        col,
                        this.mapPlayerColourNameToBoolean[newColourName]
                    );
                }
            }
        }

        this.blackPopulation = this.gameState.blackPopulation;
        this.whitePopulation = this.gameState.whitePopulation;
    }

    update_IsGameOver(): boolean {
        if (this.gameState.isGameOver) {
            this.isGameOver = true;
        } else if (this.gameState.numPiecesFlippedInLastMove === 0) {
            if (this.lastMoveWasInvalid) {
                this.isGameOver = true;
            } else {
                this.lastMoveWasInvalid = true;
            }
        } else {
            this.lastMoveWasInvalid = false;
        }

        if (this.isGameOver) {
            console.log('Game over!');
            console.log('Black population:', this.gameState.blackPopulation);
            console.log('White population:', this.gameState.whitePopulation);

            const diff = this.gameState.blackPopulation - this.gameState.whitePopulation;
            let message;

            if (diff > 0) {
                message = `Black wins ${this.gameState.blackPopulation} to ${this.gameState.whitePopulation}`;
            } else if (diff < 0) {
                message = `White wins ${this.gameState.whitePopulation} to ${this.gameState.blackPopulation}`;
            } else {
                message = 'Tie game.';
            }

            console.log(message);
            this.displayMessage(message);
        }

        return this.isGameOver;
    }

    onAutomaticMove(): void {
        // let player = this.gameState.player.token;
        // const c = `${this.gameState.player.colour}`;

        // player will be a string, either 'White' or 'Black'
        let player = PlayerColour[this.gameState.player.colour];

        // console.log(`c is ${typeof c} '${c}'`);

        if (!this.gameState.isGameOver && (this.automaticMove[player] || this.doOneAutomove)) {
            this.doOneAutomove = false;

            const maxPly = this.playerPly[player];
            const moveResult = moveAutomatically(this.gameState, maxPly);

            if (typeof moveResult.lastBestMoveInfo !== 'undefined') {
                // console.log(
                //     `Auto: ${moveResult.player.token} moved at row ${moveResult.lastBestMoveInfo.bestRow}, column ${moveResult.lastBestMoveInfo.bestColumn}`
                // );
                console.log(
                    `Auto: ${player} moved at row ${moveResult.lastBestMoveInfo.bestRow}, column ${moveResult.lastBestMoveInfo.bestColumn}`
                );
            }

            this.gameState = moveResult;
            this.updateBoardFromGameState();
            this.update_IsGameOver();
            // player = this.gameState.player.token;
            player = PlayerColour[this.gameState.player.colour];

            if (this.automaticMove[player]) {
                setTimeout(() => this.onAutomaticMove(), 100);
            }
        }
    }

    onClickCanvas(event: { offsetX: number; offsetY: number }): void {
        // console.log('typeof event:', typeof event);

        // const castEvent = event as { offsetX: number; offsetY: number; };

        // const row = Math.floor(castEvent.offsetY / boardSquareHeight);
        // const col = Math.floor(castEvent.offsetX / boardSquareWidth);
        const row = Math.floor(event.offsetY / boardSquareHeight);
        const col = Math.floor(event.offsetX / boardSquareWidth);

        if (row < 0 || row >= boardHeight || col < 0 || col >= boardWidth) {
            console.error(`Error in onClickCanvas() : row is ${row}; col is ${col}`);
            console.error('  Error in onClickCanvas() : event is', event);

            return;
        }

        console.log(
            `Manual: ${
                PlayerColour[this.gameState.player.colour]
            } moved at row ${row}, column ${col}`
        );
        this.gameState = moveManually(this.gameState, row, col);
        this.updateBoardFromGameState();

        if (!this.update_IsGameOver()) {
            this.onAutomaticMove();
        }
    }

    onClickOneAutomove(): void {
        this.doOneAutomove = true;
        this.onAutomaticMove();
    }

    onClickNewGame(): void {
        this.onNewGame();
    }
}