Core Concept
Models:
Math.random() produces identical sequences across all devices for perfect synchronization.Views: Math.random() produces different sequences on each device for local variety.- ✅ Synchronized Model Random
- 🎨 Independent View Random
Models use deterministic random sequences
class GameModel extends Multisynq.Model {
init() {
this.enemies = [];
this.powerUps = [];
// Start spawning systems
this.future(2000).spawnEnemy();
this.future(5000).spawnPowerUp();
}
spawnEnemy() {
// ✅ This random call is synchronized across all devices
const x = Math.random() * 800; // Same value on all devices
const y = Math.random() * 600; // Same value on all devices
const type = Math.random() < 0.3 ? "fast" : "normal"; // Same decision
const enemy = Enemy.create({
position: { x, y },
type: type,
health: 100
});
this.enemies.push(enemy);
// Notify views of new enemy
this.publish("game", "enemy-spawned", {
id: enemy.id,
position: enemy.position,
type: enemy.type
});
// Schedule next spawn
const nextSpawnDelay = 1000 + (Math.random() * 3000); // Synchronized delay
this.future(nextSpawnDelay).spawnEnemy();
}
spawnPowerUp() {
// ✅ All random decisions are identical across devices
if (Math.random() < 0.4) { // Same probability check result
const powerUpTypes = ["health", "speed", "damage", "shield"];
const typeIndex = Math.floor(Math.random() * powerUpTypes.length);
const selectedType = powerUpTypes[typeIndex]; // Same selection
const powerUp = {
id: this.generateId(),
type: selectedType,
position: {
x: 50 + Math.random() * 700, // Same position
y: 50 + Math.random() * 500
},
duration: 10000 + Math.random() * 5000 // Same duration
};
this.powerUps.push(powerUp);
this.publish("game", "powerup-spawned", powerUp);
}
this.future(5000).spawnPowerUp();
}
handleCombat(attackerId, defenderId) {
const attacker = this.getEntity(attackerId);
const defender = this.getEntity(defenderId);
if (attacker && defender) {
// ✅ Critical hit calculation is synchronized
const criticalChance = 0.15;
const isCritical = Math.random() < criticalChance; // Same result everywhere
const baseDamage = attacker.damage;
const finalDamage = isCritical ? baseDamage * 2 : baseDamage;
defender.health -= finalDamage;
this.publish("game", "combat-result", {
attackerId,
defenderId,
damage: finalDamage,
isCritical,
defenderHealth: defender.health
});
if (defender.health <= 0) {
this.handleEntityDeath(defender);
}
}
}
generateDrops(entity) {
// ✅ Loot generation is synchronized
const drops = [];
// Each item has a chance to drop
const lootTable = [
{ item: "gold", chance: 0.8, amount: () => 10 + Math.floor(Math.random() * 20) },
{ item: "potion", chance: 0.3, amount: () => 1 },
{ item: "gem", chance: 0.1, amount: () => 1 },
{ item: "rare_weapon", chance: 0.05, amount: () => 1 }
];
for (const loot of lootTable) {
if (Math.random() < loot.chance) { // Same drop decision
drops.push({
item: loot.item,
amount: loot.amount() // Same amount
});
}
}
return drops;
}
}
GameModel.register("GameModel");
Every call to
Math.random() in the model produces the exact same value on all devices, ensuring perfect synchronization.Views use local random for visual variety
class GameView extends Multisynq.View {
init() {
this.model = this.wellKnownModel("GameModel");
this.canvas = document.getElementById('canvas');
this.ctx = this.canvas.getContext('2d');
this.particles = [];
this.visualEffects = [];
this.setupEventHandlers();
this.startRenderLoop();
// Subscribe to model events
this.subscribe("game", "enemy-spawned", this.onEnemySpawned);
this.subscribe("game", "combat-result", this.onCombatResult);
this.subscribe("game", "powerup-spawned", this.onPowerUpSpawned);
}
onEnemySpawned(data) {
// ✅ Visual effects use local random for variety
this.createSpawnEffect(data.position);
// Each user sees different particle patterns
for (let i = 0; i < 20; i++) {
this.particles.push({
x: data.position.x + (Math.random() - 0.5) * 100, // Different on each device
y: data.position.y + (Math.random() - 0.5) * 100,
vx: (Math.random() - 0.5) * 5, // Different velocities
vy: (Math.random() - 0.5) * 5,
life: 1.0,
decay: 0.02 + Math.random() * 0.02, // Different decay rates
color: this.getRandomSpawnColor() // Different colors
});
}
}
onCombatResult(data) {
const defender = this.getEntityElement(data.defenderId);
// ✅ Damage number animations vary per device
this.showDamageNumber(defender.position, data.damage, data.isCritical);
if (data.isCritical) {
// Critical hit screen shake - different intensity per user
this.screenShake(2 + Math.random() * 3); // Different shake amounts
}
// Blood/impact particles - each user sees different pattern
this.createImpactEffect(defender.position, data.isCritical);
}
onPowerUpSpawned(data) {
// ✅ Spawn animation varies per user
this.createPowerUpAnimation(data);
}
createSpawnEffect(position) {
// ✅ Each device shows different visual effects
const effect = {
x: position.x,
y: position.y,
scale: 0,
rotation: Math.random() * Math.PI * 2, // Random initial rotation
rotationSpeed: (Math.random() - 0.5) * 0.2, // Random spin speed
color: `hsl(${Math.random() * 60 + 30}, 80%, 60%)`, // Random color
particles: []
};
// Generate random particles for this effect
for (let i = 0; i < 15; i++) {
effect.particles.push({
angle: Math.random() * Math.PI * 2,
speed: 2 + Math.random() * 4,
size: 2 + Math.random() * 6,
life: 1.0
});
}
this.visualEffects.push(effect);
}
showDamageNumber(position, damage, isCritical) {
// ✅ Damage number animation varies per user
const damageText = {
x: position.x + (Math.random() - 0.5) * 40, // Random offset
y: position.y - 20 + Math.random() * 10,
damage: damage,
isCritical: isCritical,
life: 1.5,
vx: (Math.random() - 0.5) * 2, // Random drift
vy: -2 - Math.random() * 2, // Random upward speed
scale: isCritical ? 1.5 + Math.random() * 0.5 : 1.0, // Random critical scale
color: isCritical ?
`hsl(${10 + Math.random() * 20}, 100%, 60%)` : // Random red hue
`hsl(${Math.random() * 60 + 200}, 80%, 70%)` // Random blue hue
};
this.damageNumbers.push(damageText);
}
createImpactEffect(position, isCritical) {
// ✅ Impact particles are unique per device
const particleCount = isCritical ? 30 + Math.random() * 20 : 15 + Math.random() * 10;
for (let i = 0; i < particleCount; i++) {
this.particles.push({
x: position.x,
y: position.y,
vx: (Math.random() - 0.5) * 8, // Random spray direction
vy: (Math.random() - 0.5) * 8,
size: 1 + Math.random() * 4, // Random sizes
life: 0.5 + Math.random() * 1.0, // Random lifespans
color: isCritical ?
`hsl(${Math.random() * 60}, 100%, ${50 + Math.random() * 30}%)` :
`hsl(${200 + Math.random() * 60}, 80%, ${40 + Math.random() * 40}%)`
});
}
}
createPowerUpAnimation(powerUp) {
// ✅ Power-up spawn effects vary per user
const glitterCount = 50 + Math.random() * 30;
for (let i = 0; i < glitterCount; i++) {
this.particles.push({
x: powerUp.position.x + (Math.random() - 0.5) * 60,
y: powerUp.position.y + (Math.random() - 0.5) * 60,
vx: (Math.random() - 0.5) * 3,
vy: -1 - Math.random() * 2, // Upward drift
size: 1 + Math.random() * 3,
life: 2 + Math.random() * 2,
decay: 0.005 + Math.random() * 0.01,
color: this.getPowerUpColor(powerUp.type),
twinkle: Math.random() * Math.PI * 2 // Random twinkle phase
});
}
}
getRandomSpawnColor() {
// ✅ Each device picks different colors
const colors = [
'#ff6b6b', '#4ecdc4', '#45b7d1',
'#96ceb4', '#feca57', '#ff9ff3'
];
return colors[Math.floor(Math.random() * colors.length)];
}
getPowerUpColor(type) {
// Base color by type, but random variation
const baseColors = {
health: [0, 100, 50], // Green HSL
speed: [60, 100, 50], // Yellow
damage: [0, 100, 40], // Red
shield: [240, 100, 50] // Blue
};
const base = baseColors[type] || [180, 50, 50];
// ✅ Add random variation per device
return `hsl(${base[0] + (Math.random() - 0.5) * 30}, ${base[1]}%, ${base[2] + (Math.random() - 0.5) * 20}%)`;
}
screenShake(intensity) {
// ✅ Screen shake varies per user
this.shakeOffset = {
x: (Math.random() - 0.5) * intensity * 2,
y: (Math.random() - 0.5) * intensity * 2
};
// Gradually reduce shake
setTimeout(() => {
this.shakeOffset = { x: 0, y: 0 };
}, 100 + Math.random() * 100); // Random shake duration
}
updateParticles() {
// ✅ Particle physics can have random elements per device
this.particles = this.particles.filter(particle => {
// Update particle
particle.x += particle.vx;
particle.y += particle.vy;
particle.life -= particle.decay || 0.02;
// Random wind effect (different per device)
if (Math.random() < 0.1) {
particle.vx += (Math.random() - 0.5) * 0.1;
}
return particle.life > 0;
});
}
}
GameView.register("GameView");
Never use view random for game logic! View randomness should only affect visual presentation, not gameplay mechanics.
Practical Examples
🎯 Synchronized Game Logic
🎯 Synchronized Game Logic
Examples where models MUST use synchronized random
class RPGModel extends Multisynq.Model {
init() {
this.players = new Map();
this.npcs = new Map();
this.lootSystem = new LootSystem();
}
// ✅ Combat calculations must be synchronized
rollAttack(attackerId, defenderId) {
const attacker = this.players.get(attackerId);
const defender = this.players.get(defenderId);
// ✅ Dice rolls are the same on all devices
const attackRoll = Math.floor(Math.random() * 20) + 1; // d20
const defenseRoll = Math.floor(Math.random() * 20) + 1;
const attackTotal = attackRoll + attacker.attackBonus;
const defenseTotal = defenseRoll + defender.defenseBonus;
const hit = attackTotal > defenseTotal;
let damage = 0;
if (hit) {
// ✅ Damage roll synchronized
damage = Math.floor(Math.random() * attacker.damageDie) + 1 + attacker.damageBonus;
// ✅ Critical hit check synchronized
if (attackRoll === 20) { // Natural 20
damage *= 2;
}
}
// All devices get the same result
this.publish("combat", "attack-result", {
attackerId,
defenderId,
attackRoll,
defenseRoll,
hit,
damage,
critical: attackRoll === 20
});
if (hit) {
defender.health -= damage;
if (defender.health <= 0) {
this.handlePlayerDeath(defenderId);
}
}
}
// ✅ Procedural generation must be synchronized
generateDungeon(seed, width, height) {
// ✅ Same random sequence produces identical dungeons
const dungeon = {
width,
height,
rooms: [],
corridors: [],
treasures: [],
monsters: []
};
// Generate rooms
const roomCount = 5 + Math.floor(Math.random() * 8); // Same count
for (let i = 0; i < roomCount; i++) {
const room = {
x: Math.floor(Math.random() * (width - 10)) + 5,
y: Math.floor(Math.random() * (height - 10)) + 5,
width: 4 + Math.floor(Math.random() * 8),
height: 4 + Math.floor(Math.random() * 8),
type: Math.random() < 0.3 ? "treasure" : "normal"
};
dungeon.rooms.push(room);
// Place monsters in rooms
if (Math.random() < 0.7) { // 70% chance
const monsterCount = 1 + Math.floor(Math.random() * 4);
for (let j = 0; j < monsterCount; j++) {
dungeon.monsters.push({
x: room.x + Math.floor(Math.random() * room.width),
y: room.y + Math.floor(Math.random() * room.height),
type: this.getRandomMonsterType(),
level: 1 + Math.floor(Math.random() * 5)
});
}
}
}
return dungeon;
}
// ✅ Loot drops must be synchronized
generateLoot(monsterLevel, rarity) {
const loot = [];
// Base chance for loot
if (Math.random() < 0.8) { // 80% chance for basic loot
// Gold amount
const goldAmount = (monsterLevel * 10) + Math.floor(Math.random() * (monsterLevel * 20));
loot.push({ type: "gold", amount: goldAmount });
}
// Magic item chance based on rarity
const magicChance = rarity === "rare" ? 0.5 : rarity === "epic" ? 0.8 : 0.2;
if (Math.random() < magicChance) {
const itemTypes = ["weapon", "armor", "accessory", "consumable"];
const itemType = itemTypes[Math.floor(Math.random() * itemTypes.length)];
loot.push({
type: "magic_item",
category: itemType,
level: monsterLevel + Math.floor(Math.random() * 3) - 1,
rarity: rarity
});
}
return loot;
}
// ✅ Random events must be synchronized
triggerRandomEvent() {
const events = [
{ type: "treasure_chest", chance: 0.3 },
{ type: "wandering_monster", chance: 0.4 },
{ type: "mysterious_portal", chance: 0.1 },
{ type: "helpful_npc", chance: 0.2 }
];
const roll = Math.random();
let accumulator = 0;
for (const event of events) {
accumulator += event.chance;
if (roll < accumulator) {
this.executeRandomEvent(event.type);
break;
}
}
}
getRandomMonsterType() {
const monsters = ["goblin", "orc", "skeleton", "spider", "wolf"];
return monsters[Math.floor(Math.random() * monsters.length)];
}
}
RPGModel.register("RPGModel");
🎨 Visual Variety in Views
🎨 Visual Variety in Views
Examples where views SHOULD use local random
class RPGView extends Multisynq.View {
init() {
this.model = this.wellKnownModel("RPGModel");
this.canvas = document.getElementById('canvas');
this.ctx = this.canvas.getContext('2d');
this.weatherParticles = [];
this.ambientEffects = [];
this.uiAnimations = [];
this.setupEventHandlers();
this.startWeatherSystem();
this.startAmbientSystem();
// Subscribe to game events
this.subscribe("combat", "attack-result", this.onAttackResult);
this.subscribe("game", "treasure-found", this.onTreasureFound);
this.subscribe("game", "level-up", this.onLevelUp);
}
// ✅ Weather effects vary per device for atmosphere
startWeatherSystem() {
this.updateWeather();
}
updateWeather() {
const weatherType = this.model.currentWeather || "clear";
switch (weatherType) {
case "rain":
this.spawnRaindrops();
break;
case "snow":
this.spawnSnowflakes();
break;
case "fog":
this.updateFogParticles();
break;
}
// Continue weather updates
setTimeout(() => this.updateWeather(), 100);
}
spawnRaindrops() {
// ✅ Each device shows different raindrop patterns
if (Math.random() < 0.8) { // 80% chance each frame
const count = 2 + Math.floor(Math.random() * 5);
for (let i = 0; i < count; i++) {
this.weatherParticles.push({
type: "rain",
x: Math.random() * this.canvas.width,
y: -10,
vx: -2 + Math.random() * 4, // Random wind
vy: 5 + Math.random() * 10, // Random fall speed
length: 5 + Math.random() * 15, // Random raindrop length
alpha: 0.3 + Math.random() * 0.4 // Random transparency
});
}
}
}
spawnSnowflakes() {
// ✅ Each device shows different snowfall patterns
if (Math.random() < 0.6) { // 60% chance each frame
const count = 1 + Math.floor(Math.random() * 3);
for (let i = 0; i < count; i++) {
this.weatherParticles.push({
type: "snow",
x: Math.random() * this.canvas.width,
y: -10,
vx: (Math.random() - 0.5) * 2, // Random horizontal drift
vy: 1 + Math.random() * 3, // Random fall speed
size: 2 + Math.random() * 6, // Random size
rotation: Math.random() * Math.PI * 2, // Random initial rotation
rotationSpeed: (Math.random() - 0.5) * 0.1, // Random spin
alpha: 0.5 + Math.random() * 0.5
});
}
}
}
// ✅ Combat effects vary per device for visual interest
onAttackResult(data) {
const attackerPos = this.getPlayerPosition(data.attackerId);
const defenderPos = this.getPlayerPosition(data.defenderId);
// Create hit effect at defender
this.createHitEffect(defenderPos, data.damage, data.critical);
// Show damage number with random variation
this.showDamageNumber(defenderPos, data.damage, data.critical);
if (data.hit) {
// Screen flash effect - different timing per device
this.flashScreen(data.critical ? "red" : "white", Math.random() * 200 + 100);
// Particle burst - different pattern per device
this.createParticleBurst(defenderPos, data.critical ? 30 : 15);
}
}
createHitEffect(position, damage, isCritical) {
// ✅ Hit effects have random visual variety
const effect = {
x: position.x,
y: position.y,
scale: 0,
maxScale: isCritical ? 1.5 + Math.random() * 0.5 : 1.0 + Math.random() * 0.3,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 0.3,
color: isCritical ?
`hsl(${Math.random() * 60}, 100%, 60%)` : // Random warm color
`hsl(${200 + Math.random() * 80}, 80%, 70%)`, // Random cool color
life: 1.0,
decay: 0.05 + Math.random() * 0.03,
pulsePhase: Math.random() * Math.PI * 2, // Random pulse timing
pulseSpeed: 0.2 + Math.random() * 0.1
};
this.visualEffects.push(effect);
}
createParticleBurst(position, count) {
// ✅ Each device shows different particle patterns
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 2 + Math.random() * 6;
this.particles.push({
x: position.x,
y: position.y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
size: 1 + Math.random() * 4,
life: 0.5 + Math.random() * 1.0,
decay: 0.02 + Math.random() * 0.02,
color: `hsl(${Math.random() * 360}, 80%, 60%)`,
gravity: Math.random() * 0.2 // Random gravity effect
});
}
}
// ✅ UI animations can have random timing for liveliness
onTreasureFound(data) {
// Treasure sparkle effect
this.createTreasureSparkles(data.position);
// Show treasure notification with random slide-in direction
this.showTreasureNotification(data.treasure, Math.random() < 0.5 ? "left" : "right");
}
createTreasureSparkles(position) {
// ✅ Sparkle effects vary per device
const sparkleCount = 20 + Math.random() * 20;
for (let i = 0; i < sparkleCount; i++) {
this.particles.push({
x: position.x + (Math.random() - 0.5) * 100,
y: position.y + (Math.random() - 0.5) * 100,
vx: (Math.random() - 0.5) * 4,
vy: -2 - Math.random() * 3, // Upward motion
size: 2 + Math.random() * 4,
life: 1 + Math.random() * 2,
decay: 0.01 + Math.random() * 0.02,
color: `hsl(${45 + Math.random() * 30}, 100%, ${60 + Math.random() * 30}%)`, // Random gold hue
twinkle: Math.random() * Math.PI * 2,
twinkleSpeed: 0.1 + Math.random() * 0.2
});
}
}
// ✅ Ambient effects create atmosphere with random variety
startAmbientSystem() {
setInterval(() => {
this.spawnAmbientEffects();
}, 2000 + Math.random() * 3000); // Random interval
}
spawnAmbientEffects() {
// Random ambient effects for atmosphere
const effectType = Math.random();
if (effectType < 0.3) {
this.spawnFireflies();
} else if (effectType < 0.6) {
this.spawnDustMotes();
} else if (effectType < 0.8) {
this.spawnLeaves();
}
// Sometimes no effect for natural rhythm
}
spawnFireflies() {
// ✅ Firefly patterns vary per device
const count = 1 + Math.floor(Math.random() * 4);
for (let i = 0; i < count; i++) {
this.ambientEffects.push({
type: "firefly",
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
vx: (Math.random() - 0.5) * 1,
vy: (Math.random() - 0.5) * 1,
brightness: 0.5 + Math.random() * 0.5,
blinkPhase: Math.random() * Math.PI * 2,
blinkSpeed: 0.05 + Math.random() * 0.1,
life: 10 + Math.random() * 20 // Random lifespan
});
}
}
flashScreen(color, duration) {
// ✅ Screen flash timing varies per device
const flash = document.createElement('div');
flash.style.cssText = `
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: ${color}; pointer-events: none; z-index: 1000;
opacity: 0.${3 + Math.floor(Math.random() * 5)}; // Random opacity
`;
document.body.appendChild(flash);
// Random fade-out timing
setTimeout(() => {
flash.style.transition = `opacity ${duration}ms ease-out`;
flash.style.opacity = '0';
setTimeout(() => flash.remove(), duration);
}, 50 + Math.random() * 50);
}
}
RPGView.register("RPGView");
Best Practices
🎯 Model Randomness
Use for synchronized game logic
// ✅ Good model random usage
class GameModel extends Multisynq.Model {
// Game mechanics
rollDice(sides) {
return Math.floor(Math.random() * sides) + 1;
}
// Procedural generation
generateLevel() {
const layout = [];
for (let i = 0; i < 100; i++) {
layout.push(Math.random() < 0.3 ? "wall" : "floor");
}
return layout;
}
// AI decisions
makeAIChoice(options) {
return options[Math.floor(Math.random() * options.length)];
}
}
🎨 View Randomness
Use for visual variety only
// ✅ Good view random usage
class GameView extends Multisynq.View {
// Visual effects
createExplosion(pos) {
for (let i = 0; i < 50; i++) {
this.addParticle({
x: pos.x + (Math.random() - 0.5) * 20,
y: pos.y + (Math.random() - 0.5) * 20,
color: `hsl(${Math.random() * 60}, 100%, 60%)`
});
}
}
// Animation timing
startIdleAnimation() {
const delay = 1000 + Math.random() * 2000;
setTimeout(() => this.playIdleAnim(), delay);
}
}
Common Mistakes
Avoid these random number generation errors:
❌ Using View Random for Game Logic
❌ Using View Random for Game Logic
// ❌ NEVER do this - breaks synchronization
class BadView extends Multisynq.View {
handleAttack() {
// ❌ This calculation will be different on each device!
const damage = Math.floor(Math.random() * 20) + 1;
// ❌ This will cause desync!
this.publish("combat", "deal-damage", { damage });
}
checkCriticalHit() {
// ❌ Critical hit checks must be in the model!
if (Math.random() < 0.1) {
this.publish("combat", "critical-hit", {});
}
}
}
// ✅ Do this instead
class GoodView extends Multisynq.View {
handleAttack() {
// ✅ Let the model handle calculations
this.publish("combat", "attack-attempt", {
attackerId: this.playerId,
targetId: this.selectedTarget
});
}
}
❌ Inconsistent Random Seeds
❌ Inconsistent Random Seeds
// ❌ Don't try to manually seed random in models
class BadModel extends Multisynq.Model {
init() {
// ❌ This doesn't work - Math.random is already deterministic
Math.seedrandom(this.sessionId);
// ❌ Each device might set different seeds
const seed = Date.now();
this.random = new SomeRandomLibrary(seed);
}
}
// ✅ Just use Math.random directly
class GoodModel extends Multisynq.Model {
init() {
// ✅ Math.random is automatically synchronized
this.generateLevel();
}
generateLevel() {
// ✅ This will be identical on all devices
for (let i = 0; i < 100; i++) {
if (Math.random() < 0.3) {
this.placeObstacle(i);
}
}
}
}
❌ Mixing Random Sources
❌ Mixing Random Sources
// ❌ Don't mix different random sources
class ConfusedModel extends Multisynq.Model {
init() {
this.customRandom = new CustomRandom(12345);
}
spawnEnemy() {
// ❌ Mixing Math.random with custom random breaks sync
const x = Math.random() * 800; // Synchronized
const y = this.customRandom.next() * 600; // NOT synchronized!
const type = Math.random() < 0.5 ? "A" : "B"; // Synchronized again
// This will cause desyncs because y values differ
}
}
// ✅ Use only Math.random in models
class ConsistentModel extends Multisynq.Model {
spawnEnemy() {
// ✅ All random calls use Math.random
const x = Math.random() * 800;
const y = Math.random() * 600;
const type = Math.random() < 0.5 ? "A" : "B";
// Perfect synchronization
}
}
Advanced Patterns
🎲 Dice Rolling Systems
🎲 Dice Rolling Systems
class DiceRollingModel extends Multisynq.Model {
// Standard dice rolling
rollDie(sides) {
return Math.floor(Math.random() * sides) + 1;
}
rollMultiple(count, sides, modifier = 0) {
let total = 0;
const rolls = [];
for (let i = 0; i < count; i++) {
const roll = this.rollDie(sides);
rolls.push(roll);
total += roll;
}
return {
rolls: rolls,
total: total + modifier,
modifier: modifier
};
}
// Advantage/disadvantage system
rollWithAdvantage(sides) {
const roll1 = this.rollDie(sides);
const roll2 = this.rollDie(sides);
return Math.max(roll1, roll2);
}
rollWithDisadvantage(sides) {
const roll1 = this.rollDie(sides);
const roll2 = this.rollDie(sides);
return Math.min(roll1, roll2);
}
// Exploding dice (roll again on max)
rollExploding(sides) {
let total = 0;
let roll;
do {
roll = this.rollDie(sides);
total += roll;
} while (roll === sides);
return total;
}
}
🌍 Procedural Generation
🌍 Procedural Generation
class ProceduralModel extends Multisynq.Model {
generateIsland(size) {
const island = [];
for (let y = 0; y < size; y++) {
const row = [];
for (let x = 0; x < size; x++) {
// Distance from center
const centerX = size / 2;
const centerY = size / 2;
const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
const maxDistance = size / 2;
// Base height based on distance
let height = 1 - (distance / maxDistance);
// Add noise
height += (Math.random() - 0.5) * 0.3;
// Determine terrain type
let terrain;
if (height < 0.2) {
terrain = "water";
} else if (height < 0.4) {
terrain = "beach";
} else if (height < 0.7) {
terrain = "grass";
} else {
terrain = "mountain";
}
row.push({ height, terrain });
}
island.push(row);
}
return island;
}
generateMaze(width, height) {
// Initialize maze with walls
const maze = Array(height).fill().map(() => Array(width).fill("wall"));
// Recursive backtracker algorithm
const stack = [];
const start = { x: 1, y: 1 };
maze[start.y][start.x] = "path";
stack.push(start);
while (stack.length > 0) {
const current = stack[stack.length - 1];
const neighbors = this.getUnvisitedNeighbors(maze, current, width, height);
if (neighbors.length > 0) {
// Choose random neighbor
const next = neighbors[Math.floor(Math.random() * neighbors.length)];
// Remove wall between current and next
const wallX = current.x + (next.x - current.x) / 2;
const wallY = current.y + (next.y - current.y) / 2;
maze[wallY][wallX] = "path";
maze[next.y][next.x] = "path";
stack.push(next);
} else {
stack.pop();
}
}
return maze;
}
getUnvisitedNeighbors(maze, pos, width, height) {
const neighbors = [];
const directions = [
{ x: 0, y: -2 }, // Up
{ x: 2, y: 0 }, // Right
{ x: 0, y: 2 }, // Down
{ x: -2, y: 0 } // Left
];
for (const dir of directions) {
const newX = pos.x + dir.x;
const newY = pos.y + dir.y;
if (newX > 0 && newX < width - 1 &&
newY > 0 && newY < height - 1 &&
maze[newY][newX] === "wall") {
neighbors.push({ x: newX, y: newY });
}
}
return neighbors;
}
}
Next Steps
Writing a Multisynq Model
Learn more about model constraints and synchronization
Writing a Multisynq View
Understand view-specific patterns and local randomness
Sim Time & Future
Master timing and scheduling with synchronized randomness
Simple Animation
See randomness in action with animated examples
Random number generation in Multisynq is a powerful feature that ensures perfect synchronization when used correctly. Remember: models get identical random sequences for game logic synchronization, while views get independent random sequences for visual variety. Use this distinction to build engaging, synchronized multiplayer experiences.