NOVA™ Ltd — Internal Engineering Reference
Simulation Template Script
Live-incident Apply-page engine — js/[Link]
What this is
This is the current source code of the simulation engine used on every course’s Apply page and end-of-
course final assessment. It is one self-contained section of js/[Link], extracted here for review.
The engine is domain-agnostic: it runs whatever scenario configuration (a "pack") it is given — starting
numbers, gauge labels, action list, escalation thresholds, and narration wording. Two sources currently
supply that configuration:
1. An AI-drafted pack, built per module and per course from that module’s own title, objective and Key
Learning Points (see functions/api/[Link], not included in this document).
2. A fixed generic cyber-incident pack, used as the fallback whenever a module has no AI-drafted pack, or its
draft fails validation.
Functions in this listing
simScript(id, M)
Builds the embedded <script> block that runs the simulation client-side, in the learner’s browser. Reads a
scenario config (M0), applies per-attempt variation (button order and risk numbers), tracks turn-by-turn state,
and renders the operating picture, action buttons, and result.
simulation(cfg)
Builds the HTML shell (operating picture, log, action list, result panel) around one simScript() call. This is
what [Link] calls for every Apply page and the final assessment.
cyberActions()
The fixed six-action set (investigate, report, isolate, comms, restore, hold) used only by the generic cyber
incident pack — the fallback used when a module has no AI-drafted scenario, or when its draft fails
validation.
shuffleSeed(arr, seed)
Deterministic shuffle used when first building a cyber pack’s action order (kept separate from the true per-
attempt shuffle inside simScript itself).
cleanTopic(L, fallback)
Strips a lesson title down to plain wording for use inside brief text.
cyberStage(i, n, L, ctx)
Builds one module’s generic cyber incident scenario — the fallback pack. Sets starting risk, growth rate, and
escalation thresholds with a safety margin so the first escalation can never fire before the learner has had a
clear turn to act.
stageFromPack(p)
NOVA™ Ltd — [Link] Page 1 of 8
NOVA™ Ltd — Internal Engineering Reference
Converts a validated AI-drafted scenario pack (from /api/nova-scenario-pack) into the same cfg shape
cyberStage() produces, so it runs on the same unmodified engine. Returns null — triggering a fallback to the
generic pack — if the pack is missing anything the engine needs.
cyberCampaign(lessons, ctx)
Builds the full set of per-module generic cyber stages, one per lesson, in order.
cyberSim(L, ctx)
Convenience wrapper: a single module’s generic cyber stage.
cyberSimSummative(lessons, ctx)
Builds the generic cyber fallback for the end-of-course final assessment — the hardest difficulty, drawing on
every module by name.
NOVA™ Ltd — [Link] Page 2 of 8
NOVA™ Ltd — Internal Engineering Reference
Source listing
Lines 322–581 of js/[Link], current as of this build.
function simScript(id, M) {
return '<script>(function(){' +
'var root=[Link](' + [Link](id) + ');if(!root)return;' +
'var M0=' + [Link](M) + ';var M=null;' +
'var S,F,turn,over,fired,attempts=0;' +
'var pic=[Link](".nx-sim-pic"),log=[Link](".nx-sim-
log"),acts=[Link](".nx-sim-acts"),out=[Link](".nx-sim-out");' +
'function cl(v){return [Link](0,[Link](100,v));}' +
'function col(v,good){var g=good==="high"?v:100-v;return g>=60?"#16A34A":
(g>=35?"#E0A52E":"#DC2626");}' +
// Per-attempt variation: button order is genuinely reshuffled every time
// (not a fixed order the learner can memorise by position), and the
// starting risk position and its growth rate are nudged up or down a
// little each attempt, so the exact numbers on screen are not the same
// twice running. The clamp below re-checks, after jittering, that the
// first escalation still cannot fire before the learner has had a
// clear turn to act \\u2014 the same guarantee the numbers were built
// with, just re-verified after the jitter is applied.
'function jitterVal(v,pct){var d=v*pct;return v+([Link]()*2-1)*d;}' +
'function shuffle(a){a=[Link]();for(var i=[Link]-1;i>0;i--){var
j=[Link]([Link]()*(i+1));var t=a[i];a[i]=a[j];a[j]=t;}return a;}' +
'function buildVariant(){' +
'var m=[Link]([Link](M0));' +
'[Link]=[Link](5,[Link](jitterVal([Link],0.15)));' +
'[Link]=[Link](6,[Link](jitterVal([Link],0.15)));' +
'[Link]=[Link](4,[Link]([Link],[Link](jitterVal([Link].
reportedGrowth,0.15))));' +
'if([Link]){' +
'var origMargin=[Link][0].at-([Link]+[Link]);' +
'var maxGrowth=[Link][0].[Link](4,origMargin);' +
'if([Link]>maxGrowth)[Link]=[Link](6,[Link](maxGrowth));' +
'if([Link]>[Link])[Link]=[Link];' +
'}' +
'[Link]=shuffle([Link]);' +
'return m;' +
'}' +
'function drawPic(){var h="<div class=\\"nx-sim-turn\\"><span>Operating
picture</span><span>Turn "+[Link](turn,[Link])+" of "+[Link]+"</span></div>";' +
'[Link](function(G){var v=[Link](S[G.k]);h+="<div class=\\"nx-sim-g\\"><div
class=\\"gl\\"><span>"+[Link]+"</span><b>"+v+"</b></div><div class=\\"gb\\"><div class=\\"gf\\"
style=\\"width:"+v+"%;background:"+col(v,[Link])+"\\"></div></div></div>";});' +
'[Link]=h;}' +
'function line(cls,tag,text){var d=[Link]("div");[Link]="nx-sim-le
"+cls;[Link]="<span class=\\"t\\">"+tag+"</span>"+text;[Link](d);}' +
'function gate(a){if([Link]&&[Link]<[Link])return true;if([Link]!
=null&&[Link]>[Link])return true;return false;}' +
'function drawActs(){[Link]="";if(over)return;[Link](function(a){var
b=[Link]("button");[Link]="nx-sim-act";[Link]="<span>"+[Link]+"</
span>";[Link]("click",function(){pick(a);});[Link](b);});}' +
'function fx(o){if(!o)return;for(var k in o){if(S[k]!=null)S[k]=cl(S[k]+o[k]);}}' +
'function pick(a){if(over)return;var lk=gate(a);' +
'if(lk){fx([Link]||{});line("you","You",[Link]||"That action will not work yet.");}' +
NOVA™ Ltd — [Link] Page 3 of 8
NOVA™ Ltd — Internal Engineering Reference
'else{fx([Link]||{});if([Link])F[[Link]]=true;line("you","You",[Link]||[Link]);}' +
'advance();turn++;drawPic();' +
'if([Link]<=0)return end(false,"The network was overrun before you regained
control.");' +
'if([Link]>=[Link]&&[Link]<=[Link])return end(true,null);' +
'if(turn>[Link])return end(false,null);' +
'drawActs();}' +
'function advance(){var g=[Link]?[Link]:[Link];' +
'if([Link]){g=0;line("good","Effect",[Link]||"The pressure was held off \\
u2014 no ground was lost this turn.");}' +
'if(g>0){[Link]=cl([Link]+g);line("adv",[Link]||"Trend",([Link]||"The
position worsens, now at")+" "+[Link]([Link])+".");}' +
'[Link](function(e,i){if(fired[i])return;if([Link]>=[Link])
{if([Link]&&F[[Link]]){fired[i]=1;line("good","Blocked",[Link]||"Escalation
blocked.");return;}fired[i]=1;[Link]=cl([Link]+[Link]);line("warn","Escalation",[Link]
t+" "+([Link][0].label||"Integrity")+" "+[Link]+".");}});' +
'[Link]=false;}' +
'function end(win,msg){over=true;attempts++;[Link]="";[Link]="nx-sim-out show
"+(win?"strong":"weak");' +
'[Link]("h4").textContent=("Outcome: "+(win?([Link]||"Brought under
control"):([Link]||"Not brought under control")))+" \\u2014 "+([Link][0].label||"Integrity")
+" "+[Link]([Link])+"/100";' +
'var sol=[Link](".nx-sim-solution");if(sol)
{[Link]="";[Link]="none";}' +
'var body="";' +
'if(attempts>=2){' +
'if([Link]){body=win?[Link]:"";' +
'if(sol&&[Link]){[Link]="block";[Link]="<h5>The sequence that
keeps control</h5><ol><li>"+[Link]("</li><li>")+"</li></ol>";}}' +
'else{body=win?[Link]:[Link];' +
'if(sol&&[Link]){[Link]="block";[Link]="<h5>How to work
it</h5><ol><li>"+[Link]("</li><li>")+"</li></ol>";}}' +
'}else{' +
'if([Link]){body=win?[Link]:"Run it again.";}' +
'else{body=win?"Strong start.":"Run it again to sharpen your decisions.";}' +
'}' +
'[Link](".nx-sim-out-txt").textContent=body;' +
'line(win?"good":"warn","Result",(win?([Link]||"Brought under control"):
([Link]||"Not brought under control"))+".");' +
'try{if([Link])[Link](' + [Link](id) +
',[Link]([Link]),win);}catch(e){}}' +
'function reset()
{M=buildVariant();S=[Link]([Link]([Link]));F={};turn=1;over=false;fired={};[Link]
L="";[Link]="nx-sim-out";var sol=[Link](".nx-sim-solution");if(sol)
{[Link]="";[Link]="none";}line("you","Brief",[Link]);drawPic();drawActs();}' +
'[Link](".nx-sim-retry").addEventListener("click",reset);' +
'reset();' +
'})();<\/script>';
}
/* ---- live incident simulation: a reactive state model, not a script ---- */
function simulation(cfg) {
cfg = cfg || {};
if (![Link] || ![Link] || ![Link]) return '';
var id = [Link] || uid('nxsim');
return styleBlock(id) +
'<div id="' + id + '" class="nx-int nx-int-sim">' +
([Link] ? '<div class="nx-h">' + esc([Link]) + '</div>' : '') +
([Link] ? '<div class="nx-intro">' + esc([Link]) + '</div>' : '') +
'<div class="nx-sim-pic"></div>' +
NOVA™ Ltd — [Link] Page 4 of 8
NOVA™ Ltd — Internal Engineering Reference
'<div class="nx-sim-log"></div>' +
'<div class="nx-sim-acts"></div>' +
'<div class="nx-sim-out"><h4></h4><p class="nx-sim-out-txt"></p><div class="nx-sim-
solution"></div><button class="nx-sim-retry">Run it again</button></div>' +
'</div>' + simScript(id, cfg);
}
/* ---- reusable cyber action set (fresh copy each call, safe to shuffle) ---- */
function cyberActions() {
return [
{ id: 'invest', label: 'Investigate the alert', fx: { intel: 30 }, note: 'You dig into the
alert and build a clearer picture of what is happening.' },
{ id: 'report', label: 'Report and bring in the response cell', fx: { intel: 8 }, flag:
'reported', note: 'You report through the chain. The response cell engages \u2014 the adversary
makes ground more slowly from here.' },
{ id: 'isolate', label: 'Isolate the compromised host', needIntel: 50, fx: { foothold: -45,
integrity: -8 }, failFx: { integrity: -5 }, failNote: 'Without a clear picture you take the wrong
system offline. A turn is lost and you cause minor disruption.', note: 'You cut the adversary off
the compromised host. Foothold drops, with a small hit to availability.' },
{ id: 'comms', label: 'Cut the adversary\u2019s route out', flag: 'commscut', fx: { integrity:
-4 }, note: 'You block external communications. The adversary makes no ground this turn and cannot
exfiltrate.' },
{ id: 'restore', label: 'Restore systems to a known-good state', needFootholdMax: 30, fx:
{ integrity: 28 }, failNote: 'The foothold is still too strong \u2014 anything you restore is re-
infected. The turn is wasted.', note: 'With the adversary pushed back, you recover systems and
integrity climbs.' },
{ id: 'hold', label: 'Hold and keep watching', fx: { intel: 8 }, note: 'You hold and observe.
You learn a little \u2014 but the adversary keeps moving.' }
];
}
/* deterministic shuffle so the button order is fixed for a stage but is NOT
the winning order \u2014 the learner cannot just click top to bottom. */
function shuffleSeed(arr, seed) {
var a = [Link](), s = (seed || 1);
function rnd() { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff; }
for (var i = [Link] - 1; i > 0; i--) { var j = [Link](rnd() * (i + 1)); var t = a[i]; a[i]
= a[j]; a[j] = t; }
return a;
}
function cleanTopic(L, fallback) {
return (L && [Link]) ? String([Link]).replace(/^Module\s*\d+\s*[:\u2013\-]?\s*/i, '').trim() :
(fallback || 'the system you are responsible for');
}
/* One escalating stage of the campaign. Stage 0 begins the incident; every
later stage is the SAME incident further along and harder. */
function cyberStage(i, n, L, ctx) {
var topic = cleanTopic(L);
var first = (i === 0);
var turns = 6 + [Link](i / 2);
var footStart = [Link](22 + i * 6, 55);
var intelStart = [Link](22 - i * 3, 10);
var growth = 15 + i;
var reportedGrowth = 8 + [Link](i / 2);
// Escalation thresholds are set relative to footStart + growth, not as
// fixed numbers. On the harder stages footStart alone was landing at or
// past the old fixed threshold (45), so the first escalation \u2014 and
// often the second \u2014 fired on turn 1 no matter what the learner chose,
NOVA™ Ltd — [Link] Page 5 of 8
NOVA™ Ltd — Internal Engineering Reference
// before "isolate" (needs 50 intel) could ever be reached. The +8/+20/+15
// margins guarantee at least one clear turn to act before each escalation,
// on every stage, while keeping the harder stages tighter than the easier
// ones.
var esc1At = [Link](footStart + growth + 8, 90);
var esc2At = [Link](esc1At + 20, 96);
var esc3At = [Link](esc2At + 15, 98);
var esc = [
{ at: esc1At, text: 'Lateral movement \u2014 another host is compromised.', integrity: -(16 +
i * 2) }
];
if (i >= 1) [Link]({ at: esc2At, text: 'Privilege escalation \u2014 the adversary gains higher
access.', integrity: -(14 + i * 2) });
[Link]({ at: esc3At, text: 'Data exfiltration is underway.', integrity: -(28 + i * 2),
blockedBy: 'commscut', blockedText: 'Exfiltration attempt blocked \u2014 the route out was already
cut.' });
var brief = first
? ('A monitoring alert has fired during "' + topic + '". This is where the incident begins \
u2014 act turn by turn, the adversary will not wait.')
: ('The incident has escalated. Fresh activity during "' + topic + '" \u2014 the adversary has
had time to dig in. It is harder now: act decisively, and in the right order.');
return {
title: first ? 'Live incident \u2014 Stage 1' : ('Live incident \u2014 Stage ' + (i + 1) + '
(escalated)'),
intro: first
? 'Work the incident turn by turn. The adversary reacts to what you do \u2014 keep network
integrity up and push the foothold down before the clock runs out.'
: 'The same incident, further along and harder. Read the picture before you act \u2014 the
wrong move at the wrong time now costs you.',
brief: brief, turns: turns,
state: { integrity: 100, foothold: footStart, intel: intelStart },
gauges: [
{ k: 'integrity', label: 'Network integrity', good: 'high' },
{ k: 'foothold', label: 'Adversary foothold', good: 'low' },
{ k: 'intel', label: 'Situational awareness', good: 'high' }
],
actions: shuffleSeed(cyberActions(), i * 7 + 3),
adversary: { growth: growth, reportedGrowth: reportedGrowth, escalations: esc },
win: { integrityMin: 60, footholdMax: 30 },
growthTag: 'Adversary', growthText: 'The intrusion spreads. Foothold rises to',
growthBlockedText: 'Route out cut \u2014 the adversary makes no ground this turn.',
resultWin: 'Contained', resultLose: 'Control lost',
solution: [
'Investigate first \u2014 you cannot isolate a host you do not understand, so build
situational awareness before you act.',
'Report early and bring in the response cell \u2014 it slows the adversary for the rest of
the incident.',
'Cut the adversary\u2019s route out \u2014 it stops the intrusion gaining ground and blocks
exfiltration.',
'Isolate the compromised host once you understand the incident \u2014 this drops the
adversary\u2019s foothold.',
'Restore to a known-good state only once the foothold is low \u2014 restore too early and it
is re-infected.'
],
strong: first ? 'You read the incident, brought in the right people, cut the adversary off and
recovered. Strong start.' : 'Under a harder, escalated incident you kept control. That is the
standard.',
weak: 'The adversary got ahead of you. Understand first, bring in the response cell early, and
cut the foothold before you try to recover \u2014 then run it again.'
};
NOVA™ Ltd — [Link] Page 6 of 8
NOVA™ Ltd — Internal Engineering Reference
/* Turns an AI-drafted per-module scenario pack (from /api/nova-scenario-pack,
already validated and clamped server-side) into the same cfg shape
cyberStage() produces, so it runs on the unmodified engine. A second,
defensive shape check runs here too — if the pack is missing anything
the engine needs, this returns null and the caller falls back to the
existing generic cyber pack for that module rather than risk a broken
Apply page. */
function stageFromPack(p) {
if (!p || ![Link] || ![Link] ||  || ![Link] ||
 || ![Link] || ![Link] || ![Link] || ![Link])
return null;
return {
title: [Link], intro: [Link], brief: [Link], turns: [Link],
state: { integrity: [Link], foothold: [Link], intel: [Link] },
gauges: [
{ k: 'integrity', label: [Link], good: 'high' },
{ k: 'foothold', label: [Link], good: 'low' },
{ k: 'intel', label: [Link], good: 'high' }
],
actions: [Link],
adversary: { growth: [Link], reportedGrowth: [Link], escalations: [Link] },
win: [Link],
growthTag: [Link] || 'Trend',
growthText: [Link] || 'The position worsens, now at',
growthBlockedText: [Link] || 'That was held off \u2014 no ground was lost this
turn.',
resultWin: [Link] || 'Brought under control',
resultLose: [Link] || 'Not brought under control',
solution: [Link],
strong: [Link], weak: [Link]
};
}
/* The full campaign: one escalating stage per module, in order. */
function cyberCampaign(lessons, ctx) {
var n = (lessons && [Link]) ? [Link] : 0;
return (lessons || []).map(function (L, i) { return cyberStage(i, n, L, ctx); });
}
/* Kept for backward compatibility; the course now uses cyberCampaign. */
function cyberSim(L, ctx) { return cyberStage(0, 1, L, ctx); }
/* End-of-course finale: the incident at its worst, across several systems. */
function cyberSimSummative(lessons, ctx) {
var n = (lessons && [Link]) ? [Link] : 0;
var course = (ctx && (ctx._displayTitle || [Link]))
? String(ctx._displayTitle || [Link]).replace(/\s*(Initial\s+)?Course$/i, '').trim()
: 'this course';
return {
summative: true,
brief: 'Multiple alerts have fired across systems you are responsible for. This is the final
assessment' + (n ? ', pulling together everything from all ' + n + ' modules' : '') + '. Act turn by
turn \u2014 the adversary will not wait.',
turns: 8,
state: { integrity: 100, foothold: 45, intel: 12 },
gauges: [
{ k: 'integrity', label: 'Network integrity', good: 'high' },
{ k: 'foothold', label: 'Adversary foothold', good: 'low' },
NOVA™ Ltd — [Link] Page 7 of 8
NOVA™ Ltd — Internal Engineering Reference
{ k: 'intel', label: 'Situational awareness', good: 'high' }
],
actions: shuffleSeed(cyberActions(), 999),
adversary: {
growth: 17, reportedGrowth: 10,
// As with the module stages: thresholds set relative to the starting
// foothold (45) + growth (17), not fixed numbers. The old first
// threshold (45) equalled the starting foothold, so it fired before
// the learner's first action could ever take effect.
escalations: [
{ at: 70, text: 'Lateral movement \u2014 a second host is compromised.', integrity: -18 },
{ at: 84, text: 'Privilege escalation \u2014 the adversary gains domain access.',
integrity: -18 },
{ at: 93, text: 'A third system shows signs of compromise.', integrity: -16 },
{ at: 98, text: 'Data exfiltration is underway.', integrity: -30, blockedBy: 'commscut',
blockedText: 'Exfiltration attempt blocked \u2014 the route out was already cut.' }
]
},
win: { integrityMin: 60, footholdMax: 30 },
growthTag: 'Adversary', growthText: 'The intrusion spreads. Foothold rises to',
growthBlockedText: 'Route out cut \u2014 the adversary makes no ground this turn.',
resultWin: 'Contained', resultLose: 'Control lost',
solution: [
'Investigate first \u2014 build situational awareness before you touch anything; you cannot
isolate what you do not understand.',
'Report and bring in the response cell early \u2014 it slows the adversary for the rest of
the incident.',
'Cut the adversary\u2019s route out \u2014 this stops it gaining ground and blocks
exfiltration when it comes.',
'Isolate the compromised hosts once you understand the incident \u2014 this drops the
foothold.',
'Restore to a known-good state only once the foothold is low \u2014 restoring while it is
high just gets re-infected.',
'Repeat investigate and isolate as the incident escalates across systems \u2014 keep
awareness high and the foothold falling.'
],
strong: 'Under the hardest, longest incident across several systems you kept control. That is
end-of-course standard.',
weak: 'The incident got ahead of you. Understand first, bring in the response cell early, and
cut the foothold before you recover \u2014 then run the finale again.'
};
}
NOVA™ Ltd — [Link] Page 8 of 8