react-minesweeper v1.0
This commit is contained in:
parent
f2902210d4
commit
320362463a
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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.
|
|
@ -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 |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -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 -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -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'));
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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' };
|
||||
};
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export enum FieldType {
|
||||
Unknown,
|
||||
Flag,
|
||||
FlagMistake,
|
||||
Mine,
|
||||
MineExploded,
|
||||
Number,
|
||||
Empty
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export enum GameDifficulty {
|
||||
Beginner,
|
||||
Intermediate,
|
||||
Expert
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export enum GameMode {
|
||||
Mine,
|
||||
Flag
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export enum GameStatus{
|
||||
Idle,
|
||||
Playing,
|
||||
GameOver,
|
||||
Won
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,4 @@
|
|||
export interface Coords{
|
||||
row: number;
|
||||
col: number;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export interface DifficultySettings{
|
||||
rows: number;
|
||||
cols: number;
|
||||
nrMines: number;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { Coords } from "./Coords";
|
||||
import { FieldType } from "../enums/FieldType";
|
||||
|
||||
|
||||
export interface TileInfo{
|
||||
coords: Coords;
|
||||
closeMines?: number;
|
||||
fieldType: FieldType;
|
||||
hasMine: boolean;
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -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
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface IMinesweeperProps {
|
||||
description: string;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"DescriptionFieldLabel": "Description Field"
|
||||
}
|
||||
});
|
|
@ -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 |
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue