Understand how random number generation works in Multisynq models and views to maintain perfect synchronization
Random number generation in Multisynq is designed to maintain perfect synchronization while giving you flexibility where you need it. Understanding how randomness works in models versus views is crucial for building applications that stay synchronized across all users.
Models: Math.random()
produces identical sequences across all devices for perfect synchronization.
Views: Math.random()
produces different sequences on each device for local variety.
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.
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.
🎯 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
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");
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)];
}
}
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);
}
}
Avoid these random number generation errors:
❌ 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
// ❌ 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
// ❌ 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
}
}
🎲 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
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;
}
}
Learn more about model constraints and synchronization
Understand view-specific patterns and local randomness
Master timing and scheduling with synchronized randomness
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.