Skip to content

Instantly share code, notes, and snippets.

@satyambnsal
Created January 27, 2025 06:19
Show Gist options
  • Select an option

  • Save satyambnsal/ead5fa5134bf953162c7195d83980a41 to your computer and use it in GitHub Desktop.

Select an option

Save satyambnsal/ead5fa5134bf953162c7195d83980a41 to your computer and use it in GitHub Desktop.
tileville-procedural-generation-map
import { HexGrid } from "./HexGrid";
import { Hex } from "./Hex";
import { Matrix2D } from "../util";
interface MapConfig {
size: number;
theme: MapTheme;
difficulty: number;
features: MapFeatures;
}
interface MapFeatures {
hillDensity: number;
waterBodies: boolean;
specialStructures: boolean;
}
enum MapTheme {
GRASSLAND = "grassland",
VOLCANIC = "volcanic",
COASTAL = "coastal",
MOUNTAIN = "mountain"
}
export class ProceduralMapGenerator {
private grid: Matrix2D<Hex>;
private size: number;
private scene: Phaser.Scene;
private config: MapConfig;
private noiseGenerator: Phaser.Math.RandomDataGenerator;
constructor(scene: Phaser.Scene, config: MapConfig) {
this.scene = scene;
this.config = config;
this.size = config.size;
this.grid = new Matrix2D<Hex>();
this.noiseGenerator = new Phaser.Math.RandomDataGenerator([Date.now()]);
}
generate(): HexGrid {
// Create base hexGrid
const hexGrid = new HexGrid(
this.scene,
this.size,
0, // We'll place hills manually
0,
0
);
// Generate terrain heightmap
const heightMap = this.generateHeightMap();
// Apply theme-specific generation
switch(this.config.theme) {
case MapTheme.VOLCANIC:
this.generateVolcanicTerrain(hexGrid, heightMap);
break;
case MapTheme.COASTAL:
this.generateCoastalTerrain(hexGrid, heightMap);
break;
case MapTheme.MOUNTAIN:
this.generateMountainTerrain(hexGrid, heightMap);
break;
default:
this.generateGrasslandTerrain(hexGrid, heightMap);
}
// Place special features based on config
if (this.config.features.specialStructures) {
this.placeSpecialStructures(hexGrid);
}
// Place hills based on heightmap and density
this.placeHills(hexGrid, heightMap);
// Ensure map is playable
this.validateAndFixMap(hexGrid);
return hexGrid;
}
private generateHeightMap(): number[][] {
const heightMap: number[][] = [];
const scale = 0.1; // Adjust for different terrain smoothness
for (let r = 0; r < this.size * 2 + 1; r++) {
heightMap[r] = [];
for (let c = 0; c < this.size * 2 + 1; c++) {
// Use multiple octaves of noise for more natural terrain
const value = this.noiseGenerator.frac() *
this.noiseGenerator.frac() *
Math.sin(r * scale) *
Math.cos(c * scale);
heightMap[r][c] = value;
}
}
return heightMap;
}
private generateVolcanicTerrain(hexGrid: HexGrid, heightMap: number[][]) {
// Place volcanic features
const volcanoCenters: {r: number, c: number}[] = [];
const volcanoCount = Math.floor(this.size / 3);
for (let i = 0; i < volcanoCount; i++) {
const r = Math.floor(this.noiseGenerator.between(1, this.size * 2 - 1));
const c = Math.floor(this.noiseGenerator.between(1, this.size * 2 - 1));
volcanoCenters.push({r, c});
}
// Create volcanic terrain patterns
for (let r = 0; r < this.size * 2 + 1; r++) {
for (let c = 0; c < this.size * 2 + 1; c++) {
const hex = hexGrid.grid.get(r, c);
if (!hex || hex.hexType !== 0) continue;
// Calculate distance to nearest volcano
const distToVolcano = Math.min(...volcanoCenters.map(vc =>
Math.sqrt(Math.pow(r - vc.r, 2) + Math.pow(c - vc.c, 2))
));
if (distToVolcano < 2) {
hex.setType(7); // Volcano
} else if (distToVolcano < 3 && heightMap[r][c] > 0.6) {
hex.setHill(true);
}
}
}
}
private generateCoastalTerrain(hexGrid: HexGrid, heightMap: number[][]) {
// Generate coastline using heightmap
for (let r = 0; r < this.size * 2 + 1; r++) {
for (let c = 0; c < this.size * 2 + 1; c++) {
const hex = hexGrid.grid.get(r, c);
if (!hex || hex.hexType !== 0) continue;
if (heightMap[r][c] < 0.3) {
// Water tiles or ports
if (this.hasLandNeighbor(hexGrid, r, c)) {
hex.setType(5); // Port
}
} else if (heightMap[r][c] > 0.7) {
hex.setHill(true);
}
}
}
}
private generateMountainTerrain(hexGrid: HexGrid, heightMap: number[][]) {
// Create mountain ranges
for (let r = 0; r < this.size * 2 + 1; r++) {
for (let c = 0; c < this.size * 2 + 1; c++) {
const hex = hexGrid.grid.get(r, c);
if (!hex || hex.hexType !== 0) continue;
if (heightMap[r][c] > 0.6) {
hex.setHill(true);
}
}
}
}
private generateGrasslandTerrain(hexGrid: HexGrid, heightMap: number[][]) {
// Create patches of different vegetation
for (let r = 0; r < this.size * 2 + 1; r++) {
for (let c = 0; c < this.size * 2 + 1; c++) {
const hex = hexGrid.grid.get(r, c);
if (!hex || hex.hexType !== 0) continue;
if (heightMap[r][c] > 0.7) {
hex.setHill(true);
} else if (heightMap[r][c] < 0.3) {
hex.setType(2); // Grass
}
}
}
}
private placeHills(hexGrid: HexGrid, heightMap: number[][]) {
const hillCount = Math.floor(this.size * this.size * this.config.features.hillDensity);
let placed = 0;
for (let r = 0; r < this.size * 2 + 1 && placed < hillCount; r++) {
for (let c = 0; c < this.size * 2 + 1 && placed < hillCount; c++) {
const hex = hexGrid.grid.get(r, c);
if (!hex || hex.hexType !== 0 || hex.hasHill) continue;
if (heightMap[r][c] > 0.7 && !this.hasAdjacentHill(hexGrid, r, c)) {
hex.setHill(true);
placed++;
}
}
}
}
private placeSpecialStructures(hexGrid: HexGrid) {
// Place various special structures based on theme
const structureCount = Math.floor(this.size / 2);
let placed = 0;
while (placed < structureCount) {
const r = Math.floor(this.noiseGenerator.between(1, this.size * 2 - 1));
const c = Math.floor(this.noiseGenerator.between(1, this.size * 2 - 1));
const hex = hexGrid.grid.get(r, c);
if (hex && hex.hexType === 0 && !hex.hasHill) {
if (this.config.theme === MapTheme.VOLCANIC) {
hex.setType(7); // Volcano
} else if (this.config.theme === MapTheme.COASTAL) {
hex.setType(5); // Port
}
placed++;
}
}
}
private hasAdjacentHill(hexGrid: HexGrid, row: number, col: number): boolean {
const neighbors = hexGrid.neighbors(row, col);
return neighbors.some(n => n?.hasHill);
}
private hasLandNeighbor(hexGrid: HexGrid, row: number, col: number): boolean {
const neighbors = hexGrid.neighbors(row, col);
return neighbors.some(n => n && n.hexType === 0);
}
private validateAndFixMap(hexGrid: HexGrid) {
// Ensure there's always a valid path to ports/center
// Add necessary paths if missing
const center = hexGrid.grid.get(this.size, this.size);
if (!center) return;
// Check each port's connectivity
for (const hex of hexGrid.hexes) {
if (hex.hexType === 5) {
if (!this.hasPathToCenter(hexGrid, hex, center)) {
this.createPathToCenter(hexGrid, hex, center);
}
}
}
}
private hasPathToCenter(hexGrid: HexGrid, start: Hex, end: Hex): boolean {
const visited = new Set<Hex>();
const queue: Hex[] = [start];
while (queue.length > 0) {
const current = queue.shift()!;
if (current === end) return true;
visited.add(current);
const neighbors = hexGrid.neighbors(current.row, current.col);
for (const neighbor of neighbors) {
if (neighbor && !visited.has(neighbor) &&
(neighbor.hexType === 0 || neighbor.hexType === 3)) {
queue.push(neighbor);
}
}
}
return false;
}
private createPathToCenter(hexGrid: HexGrid, start: Hex, end: Hex) {
// Create a path of road tiles to connect isolated ports
const path = this.findPath(hexGrid, start, end);
if (path) {
for (const hex of path) {
if (hex.hexType === 0) {
hex.setType(3); // Road
}
}
}
}
private findPath(hexGrid: HexGrid, start: Hex, end: Hex): Hex[] | null {
const queue: {hex: Hex, path: Hex[]}[] = [{hex: start, path: [start]}];
const visited = new Set<Hex>();
while (queue.length > 0) {
const {hex, path} = queue.shift()!;
if (hex === end) return path;
visited.add(hex);
const neighbors = hexGrid.neighbors(hex.row, hex.col);
for (const neighbor of neighbors) {
if (neighbor && !visited.has(neighbor) && neighbor.hexType === 0) {
queue.push({
hex: neighbor,
path: [...path, neighbor]
});
}
}
}
return null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment