Skip to content

Commit

Permalink
Blueberry biome
Browse files Browse the repository at this point in the history
  • Loading branch information
Vulae committed Apr 28, 2024
1 parent 132b5eb commit 7a93eac
Show file tree
Hide file tree
Showing 18 changed files with 239 additions and 81 deletions.
3 changes: 2 additions & 1 deletion TODO
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* [ ] Theme picker
* [ ] Modern theme (Flat style graphics)
* [ ] SeaBear1015 theme (https://github.com/Vulae/infinite-minesweeper/issues/4)
* [ ] Retro theme: Replace number placeholders

* [ ] Extreme zoom out
* (Zoom out very far, stop rendering individual tiles and just render biome colors)
Expand All @@ -35,7 +36,7 @@
* [X] Biomes
* [X] Vanilla (Normal minesweeper)
* [X] Chocolate (Vanilla with extra mines)
* [ ] Blueberry (Minesweeper, up to 3 mines on a tile)
* [X] Blueberry (Minesweeper, up to 3 mines on a tile)
* [ ] Blueberry Chocolate (Blueberry with extra mines)
* [X] Waffle (2x2 checkers of tiles, dark sections have 3 mines, light sections have 1 mine)
* [X] Stroopwafel (3x3 checkers of tiles, dark sections have 8 mines, light sections have 1 mine)
Expand Down
15 changes: 12 additions & 3 deletions src/components/InfoModalBiomes.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import LucideChevronLeft from "lucide-svelte/icons/chevron-left";
import LucideChevronRight from "lucide-svelte/icons/chevron-right";
const biomeNames = [ 'Vanilla', 'Chocolate', 'Waffle', 'Stroopwafel' ] as const;
const biomeNames = [ 'Vanilla', 'Chocolate', 'Waffle', 'Stroopwafel', 'Blueberry' ] as const;
let currentBiome: ArrayElement<typeof biomeNames> = 'Vanilla';
function newBiome(dir: 'next' | 'prev'): void {
Expand Down Expand Up @@ -91,8 +91,7 @@
<div class="biome-content">
<h2 class="biome-title">Chocolate</h2>
<div class="biome-description">
The standard Minesweeper rules.
<br />
The standard Minesweeper rules.<br />
Much more mines than Vanilla biome.
</div>
</div>
Expand Down Expand Up @@ -121,6 +120,16 @@
</div>
</div>
</div>
{:else if currentBiome == 'Blueberry'}
<div class="biome">
<img class="biome-backdrop" src="/infinite-minesweeper/biome_blueberry_screenshot.png" alt="Blueberry Biome Screenshot">
<div class="biome-content">
<h2 class="biome-title">Blueberry</h2>
<div class="biome-description">
Tiles may have up to 3 mines.
</div>
</div>
</div>
{/if}
</div>
<div class="pointer-events-none flex justify-between z-10">
Expand Down
12 changes: 12 additions & 0 deletions src/lib/BitIO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,15 @@ export class BitIO {
}



/**
* @param value UNSIGNED VALUE
*/
export function bitsToRepresentValue(value: number): number {
let count = 0;
while(value) {
value &= value - 1;
count++;
}
return count;
}
7 changes: 6 additions & 1 deletion src/lib/game/Generator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@

import { perlin_noise2d, splitmix32, voronoi_noise2d } from "../RNG";
import type { World } from "./World";
import { BlueberryTile } from "./tile/Blueberry";
import { ChocolateTile } from "./tile/Chocolate";
import { StroopwafelTile } from "./tile/Stroopwafel";
import type { Tile, ValidTile, ValidTileConstructor } from "./tile/Tile";
import type { ValidTile, ValidTileConstructor } from "./tile/Tile";
import { VanillaTile } from "./tile/Vanilla";
import { WaffleTile } from "./tile/Waffle";

Expand Down Expand Up @@ -54,6 +55,10 @@ const Biomes: Biome = {
weight: 2,
tile: StroopwafelTile
}]
}, {
type: 'biome',
weight: 1,
tile: BlueberryTile
}]
};

Expand Down
8 changes: 7 additions & 1 deletion src/lib/game/ParticleRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ParticleFlag } from "./particle/Flag";
import type { ValidParticle } from "./particle/Particle";
import { ParticleTileReveal } from "./particle/TileReveal";
import type { Theme } from "./theme/Theme";
import { MultiMineTile } from "./tile/MultiMine";



Expand Down Expand Up @@ -36,7 +37,12 @@ export class ParticleRenderer {

public async init(): Promise<void> {
this.listeners.push(this.world.addEventListener('particle_unflag', ({ data: { x, y } }) => {
this.particles.push(new ParticleFlag(x, y));
const tile = this.world.getTile(x, y);
if(tile instanceof MultiMineTile) {
this.particles.push(new ParticleFlag(x, y, true, tile.numMaxMines));
} else {
this.particles.push(new ParticleFlag(x, y, false, 1));
}
}));
this.listeners.push(this.world.addEventListener('particle_explosion', ({ data: { x, y } }) => {
const tile = this.world.getTile(x, y);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/game/World.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export class World extends EventDispatcher<{
}


const SAVE_VERSION = 1;
const SAVE_VERSION = 2;

export type WorldSave = {
version: number;
Expand Down
7 changes: 6 additions & 1 deletion src/lib/game/particle/Flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,19 @@ export class ParticleFlag extends Particle {
return clampNormal(1 - (this.lifetime / 250) + 0.5);
}

public constructor(x: number, y: number) {
public readonly isMultiFlag: boolean;
public readonly numFlags: number;

public constructor(x: number, y: number, isMultiFlag: boolean, numFlags: number) {
super();
this.x = x;
this.y = y;
this.dx = (Math.random() - 0.5) * 0.005;
this.dy = -(Math.random() * 0.002 + 0.005);
this.r = 0;
this.dr = (Math.random() - 0.5) * 0.01;
this.isMultiFlag = isMultiFlag;
this.numFlags = numFlags;
}

public update(renderer: ParticleRenderer, dt: number): void {
Expand Down
92 changes: 78 additions & 14 deletions src/lib/game/theme/retro.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@

import { TextureAtlas } from "../../Atlas";
import type { ValidParticle } from "../particle/Particle";
import type { MultiMineTile } from "../tile/MultiMine";
import { SingleMineTileState, type SingleMineTile } from "../tile/SingleMine";
import type { Tile, ValidTile } from "../tile/Tile";
import { waffleIsDark } from "../tile/Waffle";
import type { ValidTile } from "../tile/Tile";
import { Theme, type SoundEffect } from "./Theme";


Expand All @@ -22,6 +22,9 @@ export class ThemeRetro extends Theme {
explosion3: [ 16, 48, 16, 16 ],
explosion4: [ 16, 64, 16, 16 ],
flag: [ 32, 16, 16, 16 ],
flag_1: [ 32, 32, 16, 16 ],
flag_2: [ 32, 48, 16, 16 ],
flag_3: [ 32, 64, 16, 16 ],
number_0: [ 48, 0, 16, 16 ],
number_1: [ 48, 16, 16, 16 ],
number_2: [ 48, 32, 16, 16 ],
Expand All @@ -31,6 +34,22 @@ export class ThemeRetro extends Theme {
number_6: [ 48, 96, 16, 16 ],
number_7: [ 48, 112, 16, 16 ],
number_8: [ 48, 128, 16, 16 ],
number_9: [ 48, 144, 16, 16 ],
number_10: [ 48, 160, 16, 16 ],
number_11: [ 48, 176, 16, 16 ],
number_12: [ 48, 192, 16, 16 ],
number_13: [ 48, 208, 16, 16 ],
number_14: [ 48, 224, 16, 16 ],
number_15: [ 48, 240, 16, 16 ],
number_16: [ 48, 256, 16, 16 ],
number_17: [ 48, 272, 16, 16 ],
number_18: [ 48, 288, 16, 16 ],
number_19: [ 48, 304, 16, 16 ],
number_20: [ 48, 320, 16, 16 ],
number_21: [ 48, 336, 16, 16 ],
number_22: [ 48, 352, 16, 16 ],
number_23: [ 48, 368, 16, 16 ],
number_24: [ 48, 384, 16, 16 ],
tile_vanilla_covered: [ 64, 0, 16, 16 ],
tile_vanilla_revealed: [ 80, 0, 16, 16 ],
tile_chocolate_covered: [ 64, 16, 16, 16 ],
Expand All @@ -42,7 +61,9 @@ export class ThemeRetro extends Theme {
tile_stroopwafel_light_covered: [ 64, 48, 16, 16 ],
tile_stroopwafel_light_revealed: [ 80, 48, 16, 16 ],
tile_stroopwafel_dark_covered: [ 96, 48, 16, 16 ],
tile_stroopwafel_dark_revealed: [ 112, 48, 16, 16 ]
tile_stroopwafel_dark_revealed: [ 112, 48, 16, 16 ],
tile_blueberry_covered: [ 64, 64, 16, 16 ],
tile_blueberry_revealed: [ 80, 64, 16, 16 ]
});

public async init(): Promise<void> {
Expand All @@ -51,9 +72,8 @@ export class ThemeRetro extends Theme {



private drawNearby(ctx: CanvasRenderingContext2D, tile: Tile): void {
const nearby = tile.minesNearby(true);
switch(nearby) {
private drawNearby(ctx: CanvasRenderingContext2D, minesNearby: number): void {
switch(minesNearby) {
case 0: break;
case 1: this.tileset.draw(ctx, 'number_1', 0, 0, 1, 1); break;
case 2: this.tileset.draw(ctx, 'number_2', 0, 0, 1, 1); break;
Expand All @@ -63,10 +83,36 @@ export class ThemeRetro extends Theme {
case 6: this.tileset.draw(ctx, 'number_6', 0, 0, 1, 1); break;
case 7: this.tileset.draw(ctx, 'number_7', 0, 0, 1, 1); break;
case 8: this.tileset.draw(ctx, 'number_8', 0, 0, 1, 1); break;
case 9: this.tileset.draw(ctx, 'number_9', 0, 0, 1, 1); break;
case 10: this.tileset.draw(ctx, 'number_10', 0, 0, 1, 1); break;
case 11: this.tileset.draw(ctx, 'number_11', 0, 0, 1, 1); break;
case 12: this.tileset.draw(ctx, 'number_12', 0, 0, 1, 1); break;
case 13: this.tileset.draw(ctx, 'number_13', 0, 0, 1, 1); break;
case 14: this.tileset.draw(ctx, 'number_14', 0, 0, 1, 1); break;
case 15: this.tileset.draw(ctx, 'number_15', 0, 0, 1, 1); break;
case 16: this.tileset.draw(ctx, 'number_16', 0, 0, 1, 1); break;
case 17: this.tileset.draw(ctx, 'number_17', 0, 0, 1, 1); break;
case 18: this.tileset.draw(ctx, 'number_18', 0, 0, 1, 1); break;
case 19: this.tileset.draw(ctx, 'number_19', 0, 0, 1, 1); break;
case 20: this.tileset.draw(ctx, 'number_20', 0, 0, 1, 1); break;
case 21: this.tileset.draw(ctx, 'number_21', 0, 0, 1, 1); break;
case 22: this.tileset.draw(ctx, 'number_22', 0, 0, 1, 1); break;
case 23: this.tileset.draw(ctx, 'number_23', 0, 0, 1, 1); break;
case 24: this.tileset.draw(ctx, 'number_24', 0, 0, 1, 1); break;
default: throw new Error('ThemeRetro invalid draw nearby count.');
}
}

private drawFlags(ctx: CanvasRenderingContext2D, numFlags: number): void {
switch(numFlags) {
case 0: break;
case 1: this.tileset.draw(ctx, 'flag_1', 0, 0, 1, 1); break;
case 2: this.tileset.draw(ctx, 'flag_2', 0, 0, 1, 1); break;
case 3: this.tileset.draw(ctx, 'flag_3', 0, 0, 1, 1); break;
default: throw new Error('ThemeRetro invalid draw flag count.');
}
}

private drawSingleMineTile(ctx: CanvasRenderingContext2D, tile: SingleMineTile, covered: keyof typeof this.tileset.textures, revealed: keyof typeof this.tileset.textures, forceCovered: boolean): void {
if(forceCovered) {
this.tileset.draw(ctx, covered, 0, 0, 1, 1);
Expand All @@ -77,33 +123,46 @@ export class ThemeRetro extends Theme {
case SingleMineTileState.Flagged: this.tileset.draw(ctx, covered, 0, 0, 1, 1); this.tileset.draw(ctx, 'flag', 0, 0, 1, 1); break;
case SingleMineTileState.Revealed: {
this.tileset.draw(ctx, revealed, 0, 0, 1, 1);
if(tile.isMine) {
this.tileset.draw(ctx, 'bomb', 0, 0, 1, 1);
} else {
this.drawNearby(ctx, tile);
}
this.drawNearby(ctx, tile.minesNearby());
break; }
}
}

private drawMultiMineTile(ctx: CanvasRenderingContext2D, tile: MultiMineTile, covered: keyof typeof this.tileset.textures, revealed: keyof typeof this.tileset.textures, forceCovered: boolean) {
if(forceCovered) {
this.tileset.draw(ctx, covered, 0, 0, 1, 1);
return;
}
if(!tile.isRevealed) {
this.tileset.draw(ctx, covered, 0, 0, 1, 1);
this.drawFlags(ctx, tile.numFlags());
} else {
this.tileset.draw(ctx, revealed, 0, 0, 1, 1);
this.drawNearby(ctx, tile.minesNearby());
}
}

private drawForcedTile(ctx: CanvasRenderingContext2D, tile: ValidTile, forceCovered: boolean): void {
switch(tile.type) {
case 'vanilla': this.drawSingleMineTile(ctx, tile, 'tile_vanilla_covered', 'tile_vanilla_revealed', forceCovered); break;
case 'chocolate': this.drawSingleMineTile(ctx, tile, 'tile_chocolate_covered', 'tile_chocolate_revealed', forceCovered); break;
case 'waffle': {
if(!waffleIsDark(2, tile.x, tile.y)) {
if(!tile.isDark) {
this.drawSingleMineTile(ctx, tile, 'tile_waffle_light_covered', 'tile_waffle_light_revealed', forceCovered);
} else {
this.drawSingleMineTile(ctx, tile, 'tile_waffle_dark_covered', 'tile_waffle_dark_revealed', forceCovered);
}
break; }
case 'stroopwafel': {
if(!waffleIsDark(3, tile.x, tile.y)) {
if(!tile.isDark) {
this.drawSingleMineTile(ctx, tile, 'tile_stroopwafel_light_covered', 'tile_stroopwafel_light_revealed', forceCovered);
} else {
this.drawSingleMineTile(ctx, tile, 'tile_stroopwafel_dark_covered', 'tile_stroopwafel_dark_revealed', forceCovered);
}
break; }
case 'blueberry': {
this.drawMultiMineTile(ctx, tile, 'tile_blueberry_covered', 'tile_blueberry_revealed', forceCovered);
break; }
}
}

Expand All @@ -122,7 +181,12 @@ export class ThemeRetro extends Theme {
ctx.translate(particle.x + 0.5, particle.y + 0.5);
ctx.rotate(particle.r);
ctx.globalAlpha = particle.opacity;
this.tileset.draw(ctx, 'flag', -0.5, -0.5, 1, 1);
if(!particle.isMultiFlag) {
this.tileset.draw(ctx, 'flag', -0.5, -0.5, 1, 1);
} else {
ctx.translate(-0.5, -0.5);
this.drawFlags(ctx, particle.numFlags);
}
break; }
case 'explosion': {
const explosionTextures: (keyof typeof this.tileset.textures)[] = [ 'explosion1', 'explosion2', 'explosion3', 'explosion4' ];
Expand Down
34 changes: 34 additions & 0 deletions src/lib/game/tile/Blueberry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

import type { BitIO } from "$lib/BitIO";
import { sfc_hash } from "$lib/RNG";
import type { World } from "../World";
import { MultiMineTile } from "./MultiMine";
import type { ValidTile } from "./Tile";



const mineIndices: number[] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1,
2, 2,
3
];



export class BlueberryTile extends MultiMineTile {
public readonly type: 'blueberry' = 'blueberry';

public readonly numMaxMines: number = 3;

public constructor(world: World, x: number, y: number) {
const numMines = mineIndices[Math.floor(sfc_hash(world.tileSeed, x, y, 1) * mineIndices.length)];
super(world, x, y, numMines);
}

public static load(world: World, x: number, y: number, io: BitIO): ValidTile {
return this.loadInternal(new BlueberryTile(world, x, y), io);
}
}


4 changes: 2 additions & 2 deletions src/lib/game/tile/Chocolate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { BitIO } from "$lib/BitIO";
import { sfc_hash } from "$lib/RNG";
import type { World } from "../World";
import { SingleMineTile, SingleMineTileState } from "./SingleMine";
import { SingleMineTile } from "./SingleMine";
import type { ValidTile } from "./Tile";


Expand All @@ -16,7 +16,7 @@ export class ChocolateTile extends SingleMineTile {
}

public static load(world: World, x: number, y: number, io: BitIO): ValidTile {
return this.loadInternal(this, world, x, y, io);
return this.loadInternal(new ChocolateTile(world, x, y), io);
}
}

Expand Down
Loading

0 comments on commit 7a93eac

Please sign in to comment.