react-minesweeper v1.0

This commit is contained in:
Pieter Heemeryck 2020-07-01 23:37:37 +02:00
parent f2902210d4
commit 320362463a
39 changed files with 18226 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

32
samples/react-minesweeper/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.10.0",
"libraryName": "spfx-minesweeper",
"libraryId": "550b232e-4d6f-476b-9594-e6408fc3e8bb",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Pieter Heemeryck
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,58 @@
# Minesweeper
## Summary
This web part is the classic game Minesweeper, put in a Fluent UI powered SPFx web part!
![Minesweeper](./assets/Minesweeper.gif)
## Used SharePoint Framework Version
![1.10.0](https://img.shields.io/badge/version-1.10.0-green.svg)
## Applies to
* [SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Office 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
## Prerequisites
None.
## Solution
Solution|Author(s)
--------|---------
react-minesweeper | [Pieter Heemeryck](https://twitter.com/heemeryckpieter)
## Version history
Version|Date|Comments
-------|----|--------
1.0|July, 2020|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
* Clone this repository
* in the command line run:
* `npm install`
* `gulp serve`
## Features
* Difficulties: Beginner, Intermediate, Expert
* High scores are stored in localStorage
* Mine mode / flag mode (swap left & right click), useful on mobile
* [Chording](http://www.minesweeper.info/wiki/Chord)
This Web Part illustrates the following concepts on top of the SharePoint Framework:
* [Fluent UI](https://developer.microsoft.com/en-us/fluentui#/)
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-minesweeper" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 KiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"minesweeper-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/minesweeper/MinesweeperWebPart.js",
"manifest": "./src/webparts/minesweeper/MinesweeperWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"MinesweeperWebPartStrings": "lib/webparts/minesweeper/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "spfx-minesweeper",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "spfx-minesweeper-client-side-solution",
"id": "550b232e-4d6f-476b-9594-e6408fc3e8bb",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/spfx-minesweeper.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

7
samples/react-minesweeper/gulpfile.js vendored Normal file
View File

@ -0,0 +1,7 @@
'use strict';
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(require('gulp'));

17044
samples/react-minesweeper/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
{
"name": "spfx-minesweeper",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.10.0",
"@microsoft/sp-lodash-subset": "1.10.0",
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.10.0",
"@microsoft/sp-tslint-rules": "1.10.0",
"@microsoft/sp-module-interfaces": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.10.0",
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1,37 @@
import { DifficultySettings } from "../models/DifficultySettings";
import { IIconProps } from "office-ui-fabric-react";
import { Coords } from "../models/Coords";
export default abstract class Globals {
public static CacheKey = class {
public static readonly HighScore = 'MinesweeperHighScore';
};
public static DifficultySettings = class {
public static readonly Beginner: DifficultySettings = {rows: 8, cols: 8, nrMines: 10};
public static readonly InterMediate: DifficultySettings = {rows: 16, cols: 16, nrMines: 40};
public static readonly Expert: DifficultySettings = {rows: 16, cols: 30, nrMines: 99};
};
public static GeneralSettings = class{
public static readonly TimerIntervalMs: number = 100;
public static readonly DeltaCoords: Coords[] = [
{row:-1, col:-1},
{row:0, col:-1},
{row:1, col:-1},
{row:-1, col:0},
{row:1, col:0},
{row:1, col:1},
{row:0, col:1},
{row:-1, col:1}
];
};
public static Icons = class {
public static readonly Flag: IIconProps = { iconName: 'IconSetsFlag' };
public static readonly HighScore: IIconProps = { iconName: 'FavoriteStarFill' };
public static readonly Mine: IIconProps = { iconName: 'StarBurstSolid' };
public static readonly PlayerWon: IIconProps = { iconName: 'CheckMark' };
public static readonly Reset: IIconProps = { iconName: 'Refresh' };
};
}

View File

@ -0,0 +1,9 @@
export enum FieldType {
Unknown,
Flag,
FlagMistake,
Mine,
MineExploded,
Number,
Empty
}

View File

@ -0,0 +1,5 @@
export enum GameDifficulty {
Beginner,
Intermediate,
Expert
}

View File

@ -0,0 +1,4 @@
export enum GameMode {
Mine,
Flag
}

View File

@ -0,0 +1,6 @@
export enum GameStatus{
Idle,
Playing,
GameOver,
Won
}

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,4 @@
export interface Coords{
row: number;
col: number;
}

View File

@ -0,0 +1,5 @@
export interface DifficultySettings{
rows: number;
cols: number;
nrMines: number;
}

View File

@ -0,0 +1,10 @@
import { Coords } from "./Coords";
import { FieldType } from "../enums/FieldType";
export interface TileInfo{
coords: Coords;
closeMines?: number;
fieldType: FieldType;
hasMine: boolean;
}

View File

@ -0,0 +1,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "e227a876-dff3-4d78-9e15-5800ac737b3d",
"alias": "MinesweeperWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "Minesweeper" },
"description": { "default": "The classic game of minesweeper written for SPFx" },
"officeFabricIconFontName": "StarBurstSolid",
"properties": {
"description": "Minesweeper"
}
}]
}

View File

@ -0,0 +1,60 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'MinesweeperWebPartStrings';
import Minesweeper from './components/Grid/Minesweeper';
import { IMinesweeperProps } from './components/Grid/IMinesweeperProps';
export interface IMinesweeperWebPartProps {
description: string;
}
export default class MinesweeperWebPart extends BaseClientSideWebPart <IMinesweeperWebPartProps> {
public render(): void {
const element: React.ReactElement<IMinesweeperProps> = React.createElement(
Minesweeper,
{
description: this.properties.description
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,3 @@
export interface IMinesweeperProps {
description: string;
}

View File

@ -0,0 +1,17 @@
import { TileInfo } from "../../../../models/TileInfo";
import { GameStatus } from "../../../../enums/GameStatus";
import { GameMode } from "../../../../enums/GameMode";
import { GameDifficulty } from "../../../../enums/GameDifficulty";
import { DifficultySettings } from "../../../../models/DifficultySettings";
export interface IMinesweeperState {
gameDifficulty: GameDifficulty;
gameMode: GameMode;
gameStatus: GameStatus;
grid: TileInfo [] [];
highScoreMs: number;
nrMinesLeft: number;
showHighScore: boolean;
settings: DifficultySettings;
timeMs: number;
}

View File

@ -0,0 +1,44 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.minesweeper {
padding: 10px;
.table{
* {padding:0;} // removes table cell spacing
border-spacing: 0;
cursor: pointer;
}
.gameInfo{
&.difficulty_Beginner{
max-width: 223px;
}
&.difficulty_Intermediate{
max-width: 440px;
}
&.difficulty_Expert{
max-width: 817px;
}
.gameInfoSpans{
vertical-align: -webkit-baseline-middle;
padding-right: 10px;
}
.grid{
@include ms-Grid;
}
.row{
@include ms-Grid-row;
}
.col{
@include ms-Grid-col;
@include ms-sm6;
}
}
}

View File

@ -0,0 +1,479 @@
import * as React from 'react';
import styles from './Minesweeper.module.scss';
import { IMinesweeperProps } from './IMinesweeperProps';
import { IMinesweeperState } from './IMinesweeperState';
import Tile from '../Tile/Tile';
import { FieldType as FieldType } from '../../../../enums/FieldType';
import { Coords } from '../../../../models/Coords';
import { TileInfo } from '../../../../models/TileInfo';
import { GameStatus } from '../../../../enums/GameStatus';
import Globals from '../../../../data/Globals';
import {IconButton, Icon, Callout, Dropdown, IDropdownOption} from 'office-ui-fabric-react';
import { GameMode } from '../../../../enums/GameMode';
import { GameDifficulty } from '../../../../enums/GameDifficulty';
import { DifficultySettings } from '../../../../models/DifficultySettings';
export default class Minesweeper extends React.Component<IMinesweeperProps, IMinesweeperState> {
//#region Init
private _timerRef: any = null;
constructor(props) {
super(props);
const settings = Globals.DifficultySettings.Beginner;
let highScoreMs = Number(localStorage.getItem(this.getHighScoreCacheKey(settings)));
if(highScoreMs === 0){
highScoreMs = undefined;
}
let grid = this.initGrid(settings);
this.state = {
gameDifficulty: GameDifficulty.Beginner,
gameMode: GameMode.Mine,
gameStatus: GameStatus.Idle,
grid,
highScoreMs,
nrMinesLeft: settings.nrMines,
showHighScore: false,
settings,
timeMs: 0
};
}
//#endregion
//#region Render
public render(): React.ReactElement<IMinesweeperProps> {
return (
<div className={ styles.minesweeper }>
{this.renderGameInfo()}
{this.renderGrid()}
{this.state.showHighScore && <Callout target={"#minesweeper_highScore"} onDismiss={this.toggleHighScore} ><div style={{padding: '5px'}}>{isNaN(this.state.highScoreMs) ? `No high score yet` : `Best time: ${this.state.highScoreMs / 1000}s`}</div></Callout>}
</div>
);
}
private renderGameInfo(){
return(
<div className={`${styles.grid} ${styles.gameInfo} ${styles[`difficulty_${GameDifficulty[this.state.gameDifficulty]}`]}`} dir={'ltr'}>
<div className={styles.row} >
<div className={styles.col}>
<Dropdown options={[{key: GameDifficulty.Beginner, text: 'Beginner'}, {key: GameDifficulty.Intermediate, text: 'Intermediate'}, {key: GameDifficulty.Expert, text: 'Expert'}]} onChange={(e, d) => this.selectDifficulty(e, d)} selectedKey={this.state.gameDifficulty} styles={{root: {minWidth: '150px'}}}></Dropdown>
</div>
</div>
<div className={styles.row} >
<div className={styles.col}>
<span className={styles.gameInfoSpans} title={"Time"}>{(this.state.timeMs / 1000).toFixed(1)} <Icon iconName={'clock'}/></span>
<span className={styles.gameInfoSpans} title={"Mines left"}>{this.state.nrMinesLeft} <Icon iconName={'StarburstSolid'}/></span>
</div>
<div className={styles.col} dir={'rtl'}>
<IconButton iconProps={Globals.Icons.Reset} onClick={this.reset} title={"Reset"}/>
<IconButton iconProps={this.state.gameStatus === GameStatus.Won ? Globals.Icons.PlayerWon : Globals.Icons.HighScore} onClick={this.toggleHighScore} id={"minesweeper_highScore"} title={"High score"}/>
<IconButton iconProps={this.state.gameMode === GameMode.Mine ? Globals.Icons.Mine: Globals.Icons.Flag} onClick={this.toggleMode} title={this.state.gameMode === GameMode.Mine ? "Mine mode":"Flag mode"}/>
</div>
</div>
</div>
);
}
private renderGrid(){
return(
<table className={styles.table}>
<tbody>
{this.state.grid.map((row: TileInfo[], rowIndex) =>{
return (
<tr key={rowIndex}>
{row.map((tileInfo: TileInfo, colIndex) => {
return (
<td key={`${rowIndex}_${colIndex}`}>
<Tile
tileInfo={tileInfo}
onClick={this.tileClick}
onContextMenu={this.tileRightClick}
/>
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
);
}
//#endregion
//#region Events
private tileClick = (coord: Coords): void => {
if(this.shouldDiscoverSurrounding(coord)){
this.discoverSurrounding(coord);
}
else if(this.state.gameMode === GameMode.Flag){
this.plantFlag(coord);
}
else{
this.discover(coord);
}
}
private tileRightClick = (coord: Coords, e: React.MouseEvent): void =>{
e.preventDefault();
if(this.shouldDiscoverSurrounding(coord)){
this.discoverSurrounding(coord);
}
else if(this.state.gameMode === GameMode.Flag){
this.discover(coord);
}
else{
this.plantFlag(coord);
}
}
private toggleMode = (): void => {
this.setState({
gameMode: this.state.gameMode === GameMode.Mine ? GameMode.Flag : GameMode.Mine
});
}
private selectDifficulty = (e: React.FormEvent, option: IDropdownOption): void => {
let settings = Globals.DifficultySettings.Beginner;
switch(option.key){
case GameDifficulty.Intermediate:
settings = Globals.DifficultySettings.InterMediate;
break;
case GameDifficulty.Expert:
settings = Globals.DifficultySettings.Expert;
break;
}
let highScoreMs = Number(localStorage.getItem(this.getHighScoreCacheKey(settings)));
if(highScoreMs === 0){
highScoreMs = undefined;
}
this.setState({
gameDifficulty: +option.key,
highScoreMs,
settings,
}, () => this.reset());
}
private reset = (): void => {
let executeReset = true;
if(this.state.gameStatus === GameStatus.Playing){
executeReset = window.confirm('Are you sure you want to reset the game?');
}
if(executeReset){
clearInterval(this._timerRef);
this.setState({
gameStatus: GameStatus.Idle,
timeMs: 0,
nrMinesLeft: this.state.settings.nrMines,
grid: this.initGrid(this.state.settings)
});
}
}
private toggleHighScore = (): void => {
this.setState({
showHighScore: !this.state.showHighScore
});
}
private updateTimer(): void{
this.setState({
timeMs: this.state.timeMs + Globals.GeneralSettings.TimerIntervalMs
});
}
//#endregion
//#region Game logic
private initGrid(settings: DifficultySettings): TileInfo[][]{
let grid: TileInfo [][] = [];
let minePositions: number [] = [];
while(minePositions.length < settings.nrMines){
let pos = this.getRandomInt(settings.rows*settings.cols);
if(minePositions.indexOf(pos) < 0){
minePositions.push(pos);
}
}
for(let i = 0; i < settings.rows; i++){
grid[i] = [];
for(let j = 0; j < settings.cols; j++){
let pos = i*settings.rows + j;
let hasMine = minePositions.indexOf(pos) > -1;
grid[i][j] = {coords: {row: i, col: j}, fieldType: FieldType.Unknown, hasMine};
}
}
return grid;
}
private discover(coord: Coords){
let grid = this.state.grid;
let tile = grid[coord.row][coord.col];
let highScoreMs = this.state.highScoreMs;
let nrMinesLeft = this.state.nrMinesLeft;
switch(this.state.gameStatus){
case GameStatus.Idle: // first click starting the game
while(tile.hasMine){
grid = this.initGrid(this.state.settings);
}
this.setState({
gameStatus: GameStatus.Playing
}, () => this._timerRef = setInterval(() => this.updateTimer(), Globals.GeneralSettings.TimerIntervalMs));
break;
case GameStatus.GameOver:
case GameStatus.Won:
return;
}
if(tile.fieldType === FieldType.Number || tile.fieldType === FieldType.Empty){
return;
}
if(tile.hasMine){
this.gameOver(grid, tile);
return;
}
if(tile.fieldType === FieldType.Flag){
nrMinesLeft++;
}
let closeMines = this.getSurroundingMines(grid, coord);
if(closeMines > 0){
tile.closeMines = closeMines;
tile.fieldType = FieldType.Number;
}
else{
tile.fieldType = FieldType.Empty;
this.traverseEmptyTiles(grid, coord);
}
let playerWon = this.checkPlayerWon(grid);
if(playerWon){
highScoreMs = this.playerWon();
}
this.setState({
grid,
gameStatus: playerWon ? GameStatus.Won : GameStatus.Playing,
highScoreMs,
nrMinesLeft,
showHighScore: playerWon
});
}
private plantFlag(coord: Coords){
let grid = this.state.grid;
let tile = grid[coord.row][coord.col];
let nrMinesLeft = this.state.nrMinesLeft;
if(
this.state.gameStatus !== GameStatus.Playing ||
(tile.fieldType !== FieldType.Unknown && tile.fieldType !== FieldType.Flag)
)
{
return;
}
if(tile.fieldType === FieldType.Flag){
tile.fieldType = FieldType.Unknown;
nrMinesLeft++;
}
else{
if(nrMinesLeft === 0){
return;
}
tile.fieldType = FieldType.Flag;
nrMinesLeft--;
}
this.setState({
grid,
nrMinesLeft
});
}
private discoverSurrounding(coord){
let coordsToDiscover: Coords[] = [];
let gameOver = false;
Globals.GeneralSettings.DeltaCoords.forEach(deltaCoord => {
if(this.isValidCoord(coord, deltaCoord)){
if(this.state.grid[coord.row + deltaCoord.row][coord.col + deltaCoord.col].fieldType === FieldType.Unknown){
if(this.state.grid[coord.row + deltaCoord.row][coord.col + deltaCoord.col].hasMine){
this.gameOver(this.state.grid, this.state.grid[coord.row + deltaCoord.row][coord.col + deltaCoord.col]);
gameOver = true;
}
coordsToDiscover.push({row: coord.row + deltaCoord.row, col: coord.col + deltaCoord.col});
}
}
});
if(!gameOver){
coordsToDiscover.forEach(c => {
this.discover(c);
});
}
}
private traverseEmptyTiles(grid: TileInfo[][], coord: Coords){
Globals.GeneralSettings.DeltaCoords.forEach(deltaCoord => {
if(this.isValidCoord(coord, deltaCoord)){
let tile = grid[coord.row + deltaCoord.row][coord.col + deltaCoord.col];
if(tile.fieldType === FieldType.Unknown || tile.fieldType === FieldType.Flag){
let closeMines = this.getSurroundingMines(grid, {row: coord.row + deltaCoord.row, col: coord.col + deltaCoord.col});
if(closeMines === 0){
tile.fieldType = FieldType.Empty;
this.traverseEmptyTiles(grid, {row: coord.row + deltaCoord.row, col: coord.col + deltaCoord.col});
}
else{
tile.fieldType = FieldType.Number;
tile.closeMines = closeMines;
}
}
}
});
}
private getSurroundingMines(grid: TileInfo[][], coord: Coords): number{
let surroundingMines = 0;
Globals.GeneralSettings.DeltaCoords.forEach(deltaCoord => {
if(this.isValidCoord(coord, deltaCoord)){
if(grid[coord.row + deltaCoord.row][coord.col + deltaCoord.col].hasMine){
surroundingMines++;
}
}
});
return surroundingMines;
}
private getSurroundingFlags(grid: TileInfo[][], coord: Coords): number{
let surroundingFlags = 0;
Globals.GeneralSettings.DeltaCoords.forEach(deltaCoord => {
if(this.isValidCoord(coord, deltaCoord)){
if(grid[coord.row + deltaCoord.row][coord.col + deltaCoord.col].fieldType === FieldType.Flag){
surroundingFlags++;
}
}
});
return surroundingFlags;
}
private isValidCoord(coord: Coords, deltaCoord: Coords): boolean {
let validCoords = true;
if(coord.row + deltaCoord.row < 0 || coord.row + deltaCoord.row >= this.state.settings.rows){
validCoords = false;
}
if(coord.col + deltaCoord.col < 0 || coord.col + deltaCoord.col >= this.state.settings.cols){
validCoords = false;
}
return validCoords;
}
private shouldDiscoverSurrounding(coord: Coords): boolean{
let grid = this.state.grid;
let tile = grid[coord.row][coord.col];
return tile.fieldType === FieldType.Number && this.getSurroundingMines(grid, coord) === this.getSurroundingFlags(grid, coord);
}
private checkPlayerWon(grid: TileInfo[][]): boolean{
let playerWon = true;
grid.forEach(row => {
row.forEach(t => {
if(t.fieldType === FieldType.Unknown && t.hasMine === false){
playerWon = false;
}
});
});
return playerWon;
}
private playerWon(): number{
clearInterval(this._timerRef);
let highScoreMs = this.state.highScoreMs;
let timeMs = this.state.timeMs;
if(this.state.highScoreMs && this.state.highScoreMs < timeMs){
// nothing
}
else{
localStorage.setItem(this.getHighScoreCacheKey(this.state.settings), timeMs.toString());
highScoreMs = timeMs;
}
return highScoreMs;
}
private gameOver(grid: TileInfo[][], tile: TileInfo){
grid.forEach(row => {
row.forEach(t => {
if(t.hasMine && t.fieldType !== FieldType.Flag){
t.fieldType = FieldType.Mine;
}
else if (!t.hasMine && t.fieldType === FieldType.Flag){
t.fieldType = FieldType.FlagMistake;
}
});
});
tile.fieldType = FieldType.MineExploded;
clearInterval(this._timerRef);
this.setState({
gameStatus: GameStatus.GameOver,
grid
});
}
//#endregion
//#region Helpers
private getRandomInt(max: number): number{
return Math.floor(Math.random() * Math.floor(max));
}
private getHighScoreCacheKey(settings: DifficultySettings): string{
return `${Globals.CacheKey.HighScore}_${settings.cols}x${settings.rows}`;
}
//#endregion
}

View File

@ -0,0 +1,10 @@
import { FieldType } from "../../../../enums/FieldType";
import { Coords } from "../../../../models/Coords";
import { TileInfo } from "../../../../models/TileInfo";
export interface ITileProps {
tileInfo: TileInfo;
onClick: (coords: Coords) => void;
onContextMenu: (coords: Coords, e: React.MouseEvent) => void;
}

View File

@ -0,0 +1,67 @@
.basicTile{
width: 25px;
height: 25px;
background-color: lighten($color: gray, $amount: 30%);
border: solid lighten($color: black, $amount: 50%) 1px;
font-size: 18px;
text-align: center;
}
.numberTile{
background-color: lighten($color: gray, $amount: 40%);
font-weight: bolder;
&.nr1{
color: blue;
}
&.nr2{
color: green;
}
&.nr3{
color: red;
}
&.nr4{
color: purple;
}
&.nr5{
color: maroon;
}
&.nr6{
color: turquoise;
}
&.nr7{
color: black;
}
&.nr8{
color: gray;
}
}
.mineTile{
&.exploded{
background-color: indianred;
}
i{
padding-top: 3px !important;
}
}
.flagTile{
background-color: lighten($color: gray, $amount: 40%);
&.mistake{
color: indianred;
}
i{
padding-top: 3px !important;
}
}

View File

@ -0,0 +1,56 @@
import * as React from 'react';
import styles from './Tile.module.scss';
import { ITileProps } from './ITileProps';
import { FieldType } from '../../../../enums/FieldType';
import { Icon } from 'office-ui-fabric-react';
export default class Tile extends React.Component<ITileProps, {}> {
public render(): React.ReactElement<{}> {
let fieldTypeStyling = '';
switch(this.props.tileInfo.fieldType){
case FieldType.Empty:
fieldTypeStyling = styles.numberTile;
break;
case FieldType.Number:
fieldTypeStyling = `${styles.numberTile} ${styles[`nr${this.props.tileInfo.closeMines}`]}`;
break;
case FieldType.Mine:
fieldTypeStyling = styles.mineTile;
break;
case FieldType.MineExploded:
fieldTypeStyling = `${styles.mineTile} ${styles.exploded}`;
break;
case FieldType.Flag:
fieldTypeStyling = styles.flagTile;
break;
case FieldType.FlagMistake:
fieldTypeStyling = `${styles.flagTile} ${styles.mistake}`;
}
return (
<div
className={ styles.basicTile + ' ' + fieldTypeStyling }
onClick={() => this.props.onClick(this.props.tileInfo.coords)}
onContextMenu={(e: React.MouseEvent) => this.props.onContextMenu(this.props.tileInfo.coords, e)}
>
{this.props.tileInfo.closeMines}{this.renderIcon()}
</div>
);
}
private renderIcon(){
let el = null;
switch(this.props.tileInfo.fieldType){
case FieldType.Flag:
case FieldType.FlagMistake:
el = <Icon iconName={'IconSetsFlag'}/>;
break;
case FieldType.Mine:
case FieldType.MineExploded:
el = <Icon iconName={'StarBurstSolid'}/>;
}
return el;
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IMinesweeperWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'MinesweeperWebPartStrings' {
const strings: IMinesweeperWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,38 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}