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
Copy
Ask AI
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.Practical Examples
🎯 Synchronized Game Logic
🎯 Synchronized Game Logic
Examples where models MUST use synchronized random
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
// ✅ 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
Copy
Ask AI
// ✅ 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
Copy
Ask AI
// ❌ 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
Copy
Ask AI
// ❌ 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
Copy
Ask AI
// ❌ 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
Copy
Ask AI
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
Copy
Ask AI
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.