Sunday, November 5, 2017

React, Angular 4, Vue Implementation of Mastermind Game

Mastermind is a simple number guessing game. Computer picks a 3-digit random number where all digits are distinct. This number is a secret and a player tries to find the secret by guessing. Computer guides the player with a hint message summarizing how much the guess is close the secret. Assume that the secret number is 549 and player's first move is 123. Computer evaluates the input 123 and produces "No Match!" message, hence there is no digit matched! Player's next move is 456Computer again evaluates the input 456 and produces the message "-2": The digits 4 and 5 are all matched but at the very wrong places! Player's next move is 567Computer again evaluates the input 567 and produces the message "+1": Only one digit is matched at the correct place! Player's next move is 584Computer again evaluates the input 584 and produces the message "+1-1"The digit 5 is matched at the correct place and the digit 4 is matched at the wrong place.  Player's next move is 540Computer again evaluates the input 540 and produces the message "+2": The digits 5 and 4 are all matched at the correct places! Finally the player inputs 549 and wins the game!

React JS Implementation

Game.js:
import React, {Component} from 'react';
import Table from './Table';
import Badge from './Badge';
import Alert from './Alert';
import Button from './Button';
import InputText from './InputText';
import Move from './Move';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';

class Game extends Component {
    constructor(props) {
        super(props);
        this.state = {
            secret: 123, guess: 0, tries: 0, moves: [], wins: 0, loses: 0, total: 0, counter: 60, totalWinTime: 0,
            avgWinTime: 0, validationMessage: ""        };
        this.handleChange = this.handleChange.bind(this);
        this.play = this.play.bind(this);
    }

    tick() {
        this.setState({counter: this.state.counter - 1});
        if (this.state.counter <= 0) {
            this.setState({loses: this.state.loses + 1, total: this.state.total + 1});
            this.initGame("Time is out!");
        }
    }

    componentDidMount() {
        this.initGame();
        this.timerID = setInterval(() => this.tick(), 1000);
    }

    componentWillUnmount() {
        clearInterval(this.timerID);
    }

    handleChange(event) {
        this.setState({
            guess: event.target.value        })
    }

    initGame(message) {
        this.state.moves.splice(0);
        if (message !== undefined)
            this.state.moves.push(new Move(this.state.secret, message));
        this.setState({
            moves: this.state.moves,
            secret: this.createSecret(),
            counter: 60,
            tries: 0        });
    }

    play() {
        if (!Number.isInteger(Number(this.state.guess))) {
            this.setState({validationMessage: this.state.guess + " is not an integer!"});
            return;
        }
        if (Number(this.state.guess) < 0) {
            this.setState({validationMessage: this.state.guess + " is a negative integer!"});
            return;
        }
        if (this.state.guess.toString().length !== 3) {
            this.setState({validationMessage: this.state.guess + " is not a 3-digit integer!"});
            return;
        }
        for (let i in this.state.moves) {
            let move = this.state.moves[i];
            if (move.guess === this.state.guess) {
                this.setState({validationMessage: "Already played with " + this.state.guess + "!"});
                return;
            }
        }

        this.setState({tries: this.state.tries + 1});

        if (this.state.guess.localeCompare(this.state.secret) === 0) {
            let totalWinTime = this.state.totalWinTime + 60 - this.state.counter;
            let wins = this.state.wins + 1;
            this.setState({
                wins: wins,
                total: this.state.total + 1,
                totalWinTime: totalWinTime,
                avgWinTime: totalWinTime / wins,
                validationMessage: ""            });
            this.initGame("You win!");
        } else {
            let message = this.createMessage(this.state.guess, this.state.secret);
            this.state.moves.push(new Move(this.state.guess, message));
            this.setState({
                validationMessage: "",
                moves: this.state.moves            });
        }
    }

    createRandomDigit(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }

    createSecret() {
        let numbers = [this.createRandomDigit(1, 9)];
        while (numbers.length < 3) {
            let candidate = this.createRandomDigit(0, 9);
            if (numbers.indexOf(candidate) === -1) numbers.push(candidate);
        }
        return numbers.join('')
    }

    createMessage(guess, secret) {
        guess = Array.from(guess.toString());
        secret = Array.from(secret.toString());
        let perfectMatch = 0, partialMatch = 0;
        for (let i in guess) {
            let index = secret.indexOf(guess[i]);
            if (index === -1) continue;
            if (index === Number(i)) {
                perfectMatch++;
            } else {
                partialMatch++;
            }
        }
        if (!(perfectMatch || partialMatch)) return "No match.";
        let message = "";
        if (perfectMatch > 0) message = "+" + perfectMatch;
        if (partialMatch > 0) message += "-" + partialMatch;
        return message;
    }

    render() {
        return (
            <div className="container" role="main">
                <div className="panel panel-primary">
                    <div className="panel-heading">
                        <h3 className="panel-title">Mastermind Game</h3>
                    </div>
                    <div className="panel-body">
                        <InputText htmlFor="guess" label="Guess" value={this.state.guess} onChange={this.handleChange}/>
                        <Button label="Play" doClick={this.play}/>
                        <Alert valid={this.state.validationMessage.length > 0} message={this.state.validationMessage}/>
                        <Badge label="Tries" value={this.state.tries}/>
                        <Badge label="Counter" value={this.state.counter}/>
                        <Badge label="Wins" value={this.state.wins}/>
                        <Badge label="Loses" value={this.state.loses}/>
                        <Badge label="Total" value={this.state.total}/>
                        <Badge label="Average Win Time" value={this.state.avgWinTime}/>
                    </div>
                </div>
                <Table title="Moves" columns="Guess,Message" values={this.state.moves} properties="guess,message"/>
            </div>
        );
    }
}

export default Game;

Move.js:

class Move {
    constructor(guess, message) {
        this.guess = guess;
        this.message = message;
    };
}

export default Move;

Button.js:

import React, {Component} from 'react';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';

class Button extends Component {
    render() {
        return (
            <div className="form-group">
                <button ref={ (btn) => {this.btn = btn;} }
                        onClick={this.props.doClick}
                        className="btn btn-success">{this.props.label}
                </button>
            </div>
        )
    }
}

export default Button;

Alert.js:
import React, {Component} from 'react';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';

class Alert extends Component {

    render() {
        return (
            <div> {this.props.valid &&
            <div className="alert alert-danger" role="alert">
                {this.props.message}
            </div>
            } </div>
        )
    }
}

export default Alert;

Badge.js:
import React, {Component} from 'react';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';

class Badge extends Component {

    render() {
        return (
            <div className="form-group">
                <label htmlFor={this.props.value}>{this.props.label}</label>
                <span id={this.props.value} className="badge">
                    {this.props.value}
                </span>
            </div>
        )
    }
}

export default Badge;

Table.js:
import React, {Component} from 'react';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';

class Table extends Component {

    render() {
        return (
            <div className="panel panel-success" data-bind="visible: moves().length > 0">
                <div className="panel-heading">
                    <h3 className="panel-title">{this.props.title}</h3>
                </div>
                <div className="panel-body">
                    <table className="table-responsive table table-striped">
                        <thead>
                        <tr>
                            {this.props.columns.split(',').map(
                                (col, index) =>
                                    <th key={index}>{col}</th>
                            )
                            }
                        </tr>
                        </thead>
                        <tbody>
                        {this.props.values.map(
                            (val, i1) =>
                                <tr key={i1}>
                                    {this.props.properties.split(",").map(
                                        (p, i2) =>
                                            <th key={i2}>{val[p]}</th>
                                    )
                                    }
                                </tr>
                        )
                        }
                        </tbody>
                    </table>
                </div>
            </div>
        );
    }
}

export default Table;

You can download the React implementation of the game using this link.

Angular 2 Implementation

app.module.ts:
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
import {NgModule} from '@angular/core';

import {AppComponent} from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule, FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {
}

app.component.ts:
import {Component, OnInit} from '@angular/core';
import {Move} from './move';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  private title: string;
  private guess: number;
  private tries: number;
  private secret: number;
  private gameLevel: number;
  private moves: Move[];

  constructor() {
    this.title = 'Mastermind!';
    this.guess = 123;
    this.tries = 0;
    this.secret = 0;
    this.gameLevel = 3;
    this.moves = [];
  }

  ngOnInit(): void {
    this.secret = this.createSecret();
    console.log(this.secret);
  }

  private createSecret(): number {
    const digits: number[] = [];
    digits.push(this.createDigit(1, 9));
    for (let i = 1; i < this.gameLevel; ++i) {
      let candidate = 0;
      do {
        candidate = this.createDigit(0, 9);
      } while (digits.indexOf(candidate) >= 0);
      digits.push(candidate);
    }
    let value = 0;
    for (let i = 0; i < digits.length; ++i) {
      value = 10 * value + digits[i];
    }
    return value;
  }

  private createDigit(min: number, max: number): number {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }

  public play() {
    if (this.secret === this.guess) {
      this.gameLevel++;
      this.tries = 0;
      this.moves.splice(0);
      this.moves.push(new Move(this.guess, 'You win!'));
      this.secret = this.createSecret();
    } else {
      this.tries++;
      const message: string = this.createMessage();
      this.moves.push(new Move(this.guess, message));
    }
  }

  private createMessage(): string {
    const strSecret: string = this.secret.toString();
    const strGuess: string = this.guess.toString();
    let perfectMatch = 0;
    let partialMatch = 0;
    for (let i = 0; i < strSecret.length; ++i) {
      for (let j = 0; j < strGuess.length; ++j) {
        if (strSecret.charAt(i) === strGuess.charAt(j)) {
          if (i === j) {
            ++perfectMatch;
          } else {
            ++partialMatch;
          }
        }
      }
    }
    if (perfectMatch === 0 && partialMatch === 0) {
      return 'No match';
    }
    let message = '';
    if (perfectMatch > 0) {
      message = '+' + perfectMatch;
    }
    if (partialMatch > 0) {
      message += '-' + partialMatch;
    }
    return message;
  }
}
app.component.html:
<script type="application/javascript" src="node_modules/jquery/dist/jquery.js"></script>
<script type="application/javascript" src="node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
<p>
<div class="container" role="main">
  <div class="panel panel-primary">
    <div class="panel-heading">
      <h3 class="panel-title">{{title}}</h3>
    </div>
    <div class="panel-body">
      <div class="form-group">
        <label for="gameLevel">Game Level</label>
        <span id="gameLevel">{{gameLevel}}</span>
      </div>
      <div class="form-group">
        <label for="tries">Tries</label>
        <span id="tries">{{tries}}</span>
      </div>
      <div class="form-group">
        <label for="guess">Guess</label>
        <input class="form-control" type="number" id="guess" [(ngModel)]="guess"/>
      </div>
      <div class="form-group">
        <button class="btn btn-success" (click)="play()">Play</button>
      </div>
    </div>
  </div>
  <ng-container *ngIf="moves.length>0">
    <div class="panel panel-primary">
      <div class="panel-heading">
        <h3 class="panel-title">Moves</h3>
      </div>
      <div class="panel-body">
        <table class="table table-striped">
          <thead>
          <tr>
            <th>No</th>
            <th>Guess</th>
            <th>Message</th>
          </tr>
          </thead>
          <tbody>
          <tr *ngFor="let move of moves ; let i= index">
            <td>{{i+1}}</td>
            <td>{{move.guess}}</td>
            <td>{{move.message}}</td>
          </tr>
          </tbody>
        </table>
      </div>
    </div>
  </ng-container>
</div>

You can download the Angular 4 implementation of the game using this link.

Vue JS Implementation

game.js:
class Move {
    constructor(guess, message) {
        this.guess = guess;
        this.message = message;
    }
};

Vue.component('utable', {
    props: ['items', 'properties', 'columns'],
    template:
    '<table class="table table-striped">'        + '<thead>'            + '<tr>'            + "<th v-for=\"column in columns\">{{column}}</th>"            + '</tr>'        + '</thead>'        + '<tbody>'            + '<tr v-for="item in items">'            + '<td v-for="prop in properties">{{item[prop]}}</td>'            + '</tr>'        + '</tbody>'    + '</table>'})

var app = new Vue({
    el: '#app',
    data: {
        moves: [],
        movesColumns: ["Guess", "Message"],
        movesProperties: ["guess", "message"],
        secret: 122,
        wins: 0,
        loses: 0,
        guess: 123,
        tries: 0,
        counter: 60    },
    computed: {
        total: function () {
            return this.loses + this.wins;
        }
    },
    created: function () {
        this.init();
        setInterval(() => {
            this.counter--;
            if (this.counter <= 0) {
                this.loses++;
                this.init(new Move(this.secret, "You lose!"));
            }
        }, 1000);
    },
    methods: {
        createSecret: function () {
            var numbers = [this.createRandomDigit(1, 9)];
            while (numbers.length < 3) {
                var candidate = this.createRandomDigit(0, 9);
                if (numbers.indexOf(candidate) === -1)
                    numbers.push(candidate);
            }
            return Number(numbers.join(''));
        },
        createRandomDigit: function (min, max) {
            return Math.floor(Math.random() * (max - min + 1)) + min;
        },
        init: function (move) {
            this.moves = [];
            if (move !== undefined && move !== null) {
                this.moves.push(move);
            }
            this.secret = this.createSecret();
            this.tries = 0;
            this.counter = 60;
        },
        evaluate: function (guess) {
            guess = Array.from(guess.toString());
            let secret = Array.from(this.secret.toString());
            let partialMatch = 0, perfectMatch = 0;
            for (let i in guess) {
                let index = secret.indexOf(guess[i]);
                if (index == -1) continue;
                if (index == i) {
                    perfectMatch++;
                } else {
                    partialMatch++;
                }
            }
            if (partialMatch == 0 && perfectMatch == 0) return "No match!";
            let message = "";
            if (perfectMatch > 0) message = "+" + perfectMatch;
            if (partialMatch > 0) message = message + "-" + partialMatch;
            return message;
        },
        play: function () {
            if (Number(this.guess) === Number(this.secret)) {
                this.wins++;
                this.init(new Move(this.guess, "You win!"));
                return;
            }
            this.tries++;
            if (this.tries > 10) {
                this.loses++;
                this.init(new Move(this.secret, "You lose!"));
                return;
            }
            let message = this.evaluate(this.guess);
            this.moves.push(new Move(this.guess, message));
        }
    }
});

index.html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Mastermind (vue)</title>
    <style type="text/css">
        @import url('node_modules/bootstrap/dist/css/bootstrap.css');
        @import url('node_modules/bootstrap/dist/css/bootstrap-theme.css');
    </style>
    <script type="application/javascript" src="node_modules/jquery/dist/jquery.js"></script>
    <script type="application/javascript" src="node_modules/bootstrap/dist/js/bootstrap.js"></script>
    <script type="application/javascript" src="node_modules/vue/dist/vue.js"></script>
</head>
<body>
<p/>
<div id="app" class="container" role="main">
    <div class="row col-md-4">
        <div class="panel panel-primary">
            <div class="panel-heading">
                <h3 class="panel-title">Game Console</h3>
            </div>
            <div class="panel-body">
                <div>
                    <label for="guess">Guess:</label>
                    <input id="guess" v-model="guess" class="form-control" type="text"/>
                    <button id="playButton" v-on:click="play" class="btn btn-success">Play</button>
                </div>
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-md-2">
            <div class="panel panel-primary">
                <div class="panel-heading">
                    <h3 class="panel-title">Game Statistics</h3>
                </div>
                <div class="panel-body">
                    <div class="form-group">
                        <label for="counter">Counter:</label>
                        <span id="counter">{{counter}}</span>
                    </div>
                    <div class="form-group">
                        <label for="tries">Tries:</label>
                        <span id="tries">{{tries}}</span>
                    </div>
                    <div class="form-group">
                        <label for="wins">Wins:</label>
                        <span id="wins">{{wins}}</span>
                    </div>
                    <div class="form-group">
                        <label for="loses">Loses:</label>
                        <span id="loses">{{loses}}</span>
                    </div>
                    <div class="form-group">
                        <label for="total">Total:</label>
                        <span id="total">{{total}}</span>
                    </div>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="panel panel-primary">
                <div class="panel-heading">
                    <h3 class="panel-title">Moves</h3>
                </div>
                <div class="panel-body">
                    <utable v-bind:items="moves" v-bind:columns="movesColumns" v-bind:properties="movesProperties"></utable>
                </div>
            </div>
        </div>
    </div>
</div>
<script type="application/javascript" src="js/game.js"></script>
</body>
</html>

You can download the Vue js implementation of the game using this link.

2 comments:

  1. This article is actually a pleasant one it helps new web visitors, who are wishing
    for blogging.

    ReplyDelete