Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
5 changes: 4 additions & 1 deletion .github/workflows/commentReleasedPRs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ on:
jobs:
comment:
name: Comment released PRs
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
permissions:
contents: read # read the release and its commits
pull-requests: write # comment on and label the released PRs
steps:
- name: Comment released PRs
uses: rdlf0/comment-released-prs-action@v3
Expand Down
17 changes: 0 additions & 17 deletions .github/workflows/compilabilityCheck.yml

This file was deleted.

26 changes: 26 additions & 0 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: PR Validation

on:
pull_request:
types: [opened, reopened, synchronize]

jobs:
pr-validation:
name: PR Validation
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Check out the code
uses: actions/checkout@v6

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22

- name: Install TypeScript
run: npm install -g typescript@6.0.3 # keep in sync with the release workflow

- name: Compile and test
run: tsc && node --test # tsc emits to dist/ and type-checks (fails on errors); tests import the build
128 changes: 72 additions & 56 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,74 @@ on:
env:
ARTIFACT_NAME: minesweeper-compiled

concurrency:
group: release
cancel-in-progress: false

jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
permissions:
contents: write # needed to upload the release asset via the gh CLI
env:
ARTIFACT_PATH: artifact
ASSET_NAME: minesweeper.zip
steps:
- name: Check out the code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22

- name: Install TypeScript
run: npm install -g typescript@6.0.3 # keep in sync with the PR validation workflow

- name: Compile the source
run: tsc --outDir "${ARTIFACT_PATH}/dist"

- name: Prepare artifact
run: mv index.html require.js main.js styles.css favicon.svg robots.txt sitemap.xml manifest.webmanifest sw.js LICENSE assets $ARTIFACT_PATH
run: |
rsync -a \
--exclude="/.git" \
--exclude="/.github" \
--exclude="/.vscode" \
--exclude="/src" \
--exclude="/test" \
--exclude="/node_modules" \
--exclude="/tsconfig.json" \
--exclude="/.gitignore" \
--exclude="*.md" \
--exclude="/${ARTIFACT_PATH}" \
./ "${ARTIFACT_PATH}/"

- name: Prepare release asset
run: zip -r $ASSET_NAME $ARTIFACT_PATH
run: zip -r "$ASSET_NAME" "$ARTIFACT_PATH"

- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ env.ARTIFACT_NAME }}
path: ./${{ env.ARTIFACT_PATH }}

- name: Upload release asset
uses: AButler/upload-release-assets@v3.0
with:
files: "./${{ env.ASSET_NAME }}"
repo-token: ${{ secrets.GITHUB_TOKEN }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "${{ github.event.release.tag_name }}" "$ASSET_NAME" --repo "$GITHUB_REPOSITORY"

deploy:
name: Deploy
needs: build
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
env:
AWS_S3_BUCKET: theminesweeper.com
AWS_REGION: us-east-2
AWS_ROLE: arn:aws:iam::067429979131:role/github_actions_minesweeper
CLOUDFRONT_DISTRIBUTION_ID: E32QGZLPPEMLDF
environment:
name: production
url: "https://${{ env.AWS_S3_BUCKET }}"
Expand All @@ -58,63 +84,53 @@ jobs:
contents: read
steps:
- name: Download artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: ${{ env.ARTIFACT_NAME }}

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ env.AWS_ROLE }}
aws-region: ${{ env.AWS_REGION }}

- name: Upload to S3
run: |
declare -a files=(
[0]="index.html"
[1]="*.js"
[2]="*.css"
[3]="*.svg"
[4]="*.png"
[5]="*.webmanifest"
[6]="*.txt"
[7]="*.xml"
[8]="LICENSE"
)

declare -a caches=(
[0]="no-caches"
[1]="max-age=31536000 immutable"
[2]="max-age=31536000 immutable"
[3]="max-age=31536000 immutable"
[4]="max-age=31536000 immutable"
[5]="max-age=31536000 immutable"
[6]="max-age=31536000 immutable"
[7]="max-age=31536000 immutable"
[8]=""
)
set -euo pipefail

declare -a contentTypes=(
[0]="text/html; charset=utf-8"
[1]="application/javascript; charset=utf-8"
[2]="text/css; charset=utf-8"
[3]="image/svg+xml; charset=utf-8"
[4]="image/png; charset=utf-8"
[5]="application/manifest+json; charset=utf-8"
[6]="text/plain; charset=utf-8"
[7]="application/xml; charset=utf-8"
[8]=""
# Each rule: <include-glob>|<content-type>|<cache-control>
rules=(
"index.html|text/html; charset=utf-8|no-cache"
"*.js|application/javascript; charset=utf-8|public, max-age=3600"
"*.css|text/css; charset=utf-8|public, max-age=3600"
"*.svg|image/svg+xml; charset=utf-8|public, max-age=3600"
"*.png|image/png; charset=utf-8|public, max-age=3600"
"*.webmanifest|application/manifest+json; charset=utf-8|public, max-age=3600"
"*.txt|text/plain; charset=utf-8|public, max-age=3600"
"*.xml|application/xml; charset=utf-8|public, max-age=3600"
"LICENSE|text/plain; charset=utf-8|public, max-age=3600"
)

for i in "${!files[@]}"
do
echo "Uploading ${files[$i]}"

aws s3 sync . s3://$AWS_S3_BUCKET \
--delete \
--acl public-read \
--exclude "*" \
--include "${files[$i]}" \
--cache-control "${caches[$i]}" \
--content-type "${contentTypes[$i]}"
for rule in "${rules[@]}"; do
IFS="|" read -r glob ctype cache <<< "$rule"
echo "Uploading $glob"
aws s3 sync . "s3://$AWS_S3_BUCKET" \
--delete \
--exclude "*" \
--include "$glob" \
--content-type "$ctype" \
--cache-control "$cache"
done

# sw.js is uploaded by the *.js rule above; the service worker must always
# revalidate, so force its Cache-Control afterwards.
aws s3 cp "s3://$AWS_S3_BUCKET/sw.js" "s3://$AWS_S3_BUCKET/sw.js" \
--metadata-directive REPLACE \
--content-type "application/javascript; charset=utf-8" \
--cache-control "no-cache"

- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id "$CLOUDFRONT_DISTRIBUTION_ID" \
--paths "/*"
9 changes: 8 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,12 @@ $ git commit -am "Something descriptive but concise about the changes I made"
$ git push
```
6. Repeat 3-5 as needed
7. Test your changes locally
7. Test your changes locally - serve the project root over HTTP and open the printed URL in your browser (the game loads as ES modules, so opening `index.html` from the filesystem won't work):
```sh
$ npx serve
```
And run the unit tests (Node's built-in runner - no dependencies; build first so the compiled `dist/` exists):
```sh
$ tsc && node --test
```
8. Create a new pull request - assign it to yourself, add some appropriate tags and link it to the corresponding issue
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
![](https://github.com/rdlf0/minesweeper/workflows/CI/CD/badge.svg)
[![Release](https://github.com/rdlf0/minesweeper/actions/workflows/release.yml/badge.svg)](https://github.com/rdlf0/minesweeper/actions/workflows/release.yml)

# Minesweeper

## Requirements
- typescript v3 or later (if you choose the compile option below)
- a non-ancient browser
- _to play locally:_ a static file server — the game loads as ES modules, which browsers refuse to load over `file://`, so opening `index.html` straight from the filesystem won't work. Any server will do, e.g. `npx serve` (requires [Node.js](https://nodejs.org)).
- _to compile from source:_ typescript v4 or later

## Options to play
### Online
Enjoy the published version of the game at [theminesweeper.com](https://theminesweeper.com). You can also install it as a progressive web app!
### Locally - download precompiled
Simply download the asset from the [latest release](https://github.com/rdlf0/minesweeper/releases/latest), unzip, and then open `index.html`.
Download the asset from the [latest release](https://github.com/rdlf0/minesweeper/releases/latest), unzip, then serve the folder over HTTP and open the printed URL in your browser:
```
$ npx serve
```
(Opening `index.html` directly from the filesystem won't work — the game loads as ES modules, which browsers block over `file://`.)
### Locally - compile from source
Clone, go to the root project directory and run:
Clone, go to the root project directory, compile, then serve:
```
$ tsc
$ npx serve
```
After that open the `index.html`.
After that open the printed URL (e.g. http://localhost:3000) in your browser.

## Settings
| Setting | Options | Default |
Expand Down
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#1e1e1e" />
<title>Minesweeper</title>
<script data-main="dist/main" src="require.js"></script>
<script type="module" src="dist/main.js"></script>
<link rel="icon" href="favicon.svg" />
<link rel="apple-touch-icon" href="/assets/icons/favicon_192_apple.png" />
<link rel="stylesheet" href="styles.css" />
Expand Down Expand Up @@ -43,7 +43,7 @@
<div id="board"></div>
<div id="settings"></div>
</main>
<script type="text/javascript" src="main.js"></script>
<script type="text/javascript" src="pwa-resize.js"></script>
</body>

</html>
File renamed without changes.
5 changes: 0 additions & 5 deletions require.js

This file was deleted.

19 changes: 12 additions & 7 deletions src/board.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Cell } from "./cell";
import { Mode, FIRST_CLICK } from "./config";
import { State } from "./state";
import { Cell } from "./cell.js";
import { Mode, FIRST_CLICK } from "./config.js";
import { State } from "./state.js";
import {
EVENT_CELL_CLICKED,
EVENT_CELL_REVEALED,
EVENT_GAME_OVER,
EVENT_SAFE_AREA_CREATED,
PubSub,
} from "./util/pub-sub";
import { Session } from "./util/session";
} from "./util/pub-sub.js";
import { Session } from "./util/session.js";

interface EventSubscriber {
event: string;
Expand All @@ -31,7 +31,7 @@ export class Board {

constructor(
private mode: Mode,
private state: State,
private state: State | null,
private el: HTMLElement,
) {
this.initGrid();
Expand Down Expand Up @@ -92,8 +92,13 @@ export class Board {
}

private plantMinesFromState(): void {
const state = this.state;
if (state == null) {
return;
}

for (let i = 0; i < this.mode.rows * this.mode.cols; i++) {
if (this.state.isHighBit(i)) {
if (state.isHighBit(i)) {
const row = Math.floor(i / this.mode.cols);
const col = i % this.mode.cols;
this.grid[row][col].setMine();
Expand Down
12 changes: 6 additions & 6 deletions src/cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import {
EVENT_CELL_UNFLAGGED,
EVENT_GAME_OVER,
PubSub,
} from "./util/pub-sub";
import { Session } from "./util/session";
} from "./util/pub-sub.js";
import { Session } from "./util/session.js";

enum CellState {
Default = "default",
Flagged = "flagged",
Questioned = "questioned",
Revealed = "revealed",
RevealedMine = "revealedMine",
Exploаded = "exploaded",
Exploded = "exploded",
WronglyFlagged = "wronglyFlagged",
}

Expand Down Expand Up @@ -106,16 +106,16 @@ export class Cell {
}

private explode(): void {
this.setState(CellState.Exploаded);
this.setState(CellState.Exploded);
PubSub.publish(EVENT_GAME_OVER);
}

public revealMine(): void {
// Leave flags
if (this.state === CellState.Flagged) return;

// Reveal not exploaded mines
if (this.state !== CellState.Exploаded) {
// Reveal not exploded mines
if (this.state !== CellState.Exploded) {
this.setState(CellState.RevealedMine)
}
}
Expand Down
Loading