<!-- Two labeled transition systems side by side for the bisimulation game -->
<div class="controls">
<button id="btn-example1" class="ex-btn active" type="button">Example 1: bisimilar</button>
<button id="btn-example2" class="ex-btn" type="button">Example 2: not bisimilar</button>
</div>
<p class="hint" id="hint-text">Systems A and B <strong>are bisimilar</strong>: every move has a matching reply. Click any arrow to play as the Attacker — the Defender replies automatically.</p>
<div class="arena">
<div class="lts-panel">
<div class="lts-title">System A</div>
<svg id="svg-a" class="lts-svg" viewBox="0 0 200 160"></svg>
</div>
<div class="divider">↔</div>
<div class="lts-panel">
<div class="lts-title">System B</div>
<svg id="svg-b" class="lts-svg" viewBox="0 0 200 160"></svg>
</div>
</div>
<div class="status-bar" id="status-bar">Click an arrow on either side to fire a move as the Attacker.</div>
<div class="btns">
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Layout for the bisimulation demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; }
.controls { display: flex; gap: .4rem; margin-bottom: .6rem; }
.ex-btn { font: 600 13px system-ui; padding: .3rem .75rem; border: 1px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 6px; cursor: pointer; }
.ex-btn.active { background: #1d3557; color: #fff; }
.hint { font-size: .85rem; color: #444; margin-bottom: .5rem; line-height: 1.45; }
.arena { display: flex; align-items: center; gap: .5rem; }
.lts-panel { flex: 1; min-width: 0; }
.lts-title { font-size: .8rem; font-weight: 700; text-align: center; color: #555;
margin-bottom: .2rem; letter-spacing: .04em; text-transform: uppercase; }
.lts-svg { width: 100%; border: 1px solid #d0d9e3; border-radius: 8px; background: #f7f9fb; }
.divider { font-size: 1.4rem; color: #aaa; user-select: none; }
.status-bar { font-size: .9rem; font-weight: 600; min-height: 1.4em;
margin: .5rem 0; padding: .3rem .5rem; border-radius: 6px; }
.status-bar.ok { background: #d4f5dc; color: #0a5c23; }
.status-bar.bad { background: #fde8ea; color: #9b2335; }
.status-bar.info { background: #e8f0fb; color: #1a3a6b; }
.btns { margin-top: .4rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* SVG node and edge styling for the LTS diagrams */
.node circle { cursor: pointer; transition: fill .15s; }
.node circle.current { fill: #1d3557; stroke: #1d3557; }
.node circle.visited { fill: #e8eef3; stroke: #4a7fa5; }
.node circle.plain { fill: #fff; stroke: #4a7fa5; }
.node text { font: 700 11px ui-monospace, monospace; fill: #1d3557; pointer-events: none; }
.node circle.current + text { fill: #fff; }
.edge { stroke: #4a7fa5; stroke-width: 1.5; fill: none; }
.edge-label { font: 600 11px ui-monospace, monospace; fill: #2a5f87; }
.edge-btn { cursor: pointer; }
.edge-btn:hover .edge { stroke: #c92f3c; }
.edge-btn:hover .edge-label { fill: #c92f3c; }
.edge-btn.fired .edge { stroke: #e63946; }
.edge-btn.fired .edge-label { fill: #e63946; }
.edge-btn.replied .edge { stroke: #0a7d33; }
.edge-btn.replied .edge-label { fill: #0a7d33; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Bisimulation game — Attacker fires a transition, Defender replies with a matching one
// in the other system. If the Defender can always reply, the systems are bisimilar.
// Example 1: bisimilar pair — branching collapse
const EXAMPLES = [
{
// System A: two branches both looping on b; System B: one state looping on b
A: {
states: [{id:'s0',x:100,y:30},{id:'s1',x:40,y:110},{id:'s2',x:160,y:110}],
edges: [{from:'s0',to:'s1',label:'a'},{from:'s0',to:'s2',label:'a'},
{from:'s1',to:'s1',label:'b'},{from:'s2',to:'s2',label:'b'}],
init: 's0'
},
B: {
states: [{id:'t0',x:100,y:30},{id:'t1',x:100,y:110}],
edges: [{from:'t0',to:'t1',label:'a'},{from:'t1',to:'t1',label:'b'}],
init: 't0'
},
// Bisimulation relation: s1~t1, s2~t1 (both loop on b)
bisim: [['s0','t0'],['s1','t1'],['s2','t1']]
},
{
// Example 2: NOT bisimilar — different options after a
A: {
states: [{id:'p0',x:100,y:30},{id:'p1',x:60,y:110},{id:'p2',x:140,y:110}],
edges: [{from:'p0',to:'p1',label:'a'},{from:'p0',to:'p2',label:'a'},
{from:'p1',to:'p1',label:'b'},{from:'p2',to:'p2',label:'c'}],
init: 'p0'
},
B: {
states: [{id:'q0',x:100,y:30},{id:'q1',x:100,y:110}],
edges: [{from:'q0',to:'q1',label:'a'},{from:'q1',to:'q1',label:'b'},
{from:'q1',to:'q1',label:'c'}],
init: 'q0'
},
bisim: [] // no bisimulation exists
}
];
let exIdx = 0;
let curA, curB;
let firedEdge = null, repliedEdge = null;
function isBisimilar(a, b, ex) {
// Check if (a,b) is in the bisimulation relation for this example
return ex.bisim.some(([x, y]) => x === a && y === b);
}
function drawLTS(svgEl, lts, curState, firedId, repliedId) {
// Draw all edges first (below nodes), then nodes on top
svgEl.innerHTML = '';
const ns = 'http://www.w3.org/2000/svg';
function mk(tag, attrs) {
const el = document.createElementNS(ns, tag);
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
return el;
}
// Draw edges
for (const e of lts.edges) {
const sf = lts.states.find(s => s.id === e.from);
const st = lts.states.find(s => s.id === e.to);
const isSelf = e.from === e.to;
const g = mk('g', { class: 'edge-btn' + (e.id === firedId ? ' fired' : e.id === repliedId ? ' replied' : '') });
g.dataset.edgeId = e.id;
if (isSelf) {
// Self-loop arc above the node
const cx = sf.x, cy = sf.y;
const path = mk('path', { class: 'edge', d: `M${cx-8},${cy-14} C${cx-28},${cy-38} ${cx+28},${cy-38} ${cx+8},${cy-14}` });
const arrow = mk('polygon', { points: `${cx+8},${cy-14} ${cx+4},${cy-22} ${cx+14},${cy-20}`, fill: '#4a7fa5', stroke: 'none', class: (e.id === firedId ? 'fired' : e.id === repliedId ? 'replied' : '') });
if (e.id === firedId) { arrow.setAttribute('fill','#e63946'); } else if (e.id === repliedId) { arrow.setAttribute('fill','#0a7d33'); }
const lbl = mk('text', { class: 'edge-label', x: cx, y: cy - 42, 'text-anchor': 'middle' });
lbl.textContent = e.label;
g.append(path, arrow, lbl);
} else {
// Straight directed edge with arrowhead
const dx = st.x - sf.x, dy = st.y - sf.y, len = Math.hypot(dx, dy);
const ux = dx / len, uy = dy / len;
const r = 14;
const x1 = sf.x + ux * r, y1 = sf.y + uy * r;
const x2 = st.x - ux * r, y2 = st.y - uy * r;
const path = mk('path', { class: 'edge', d: `M${x1},${y1} L${x2},${y2}` });
const ax = x2 - ux*7 - uy*5, ay = y2 - uy*7 + ux*5;
const bx = x2 - ux*7 + uy*5, by = y2 - uy*7 - ux*5;
const arrow = mk('polygon', { points: `${x2},${y2} ${ax},${ay} ${bx},${by}`, fill: '#4a7fa5', stroke: 'none' });
if (e.id === firedId) { arrow.setAttribute('fill','#e63946'); } else if (e.id === repliedId) { arrow.setAttribute('fill','#0a7d33'); }
const mx = (x1 + x2) / 2 - uy * 9, my = (y1 + y2) / 2 + ux * 9;
const lbl = mk('text', { class: 'edge-label', x: mx, y: my, 'text-anchor': 'middle' });
lbl.textContent = e.label;
g.append(path, arrow, lbl);
}
svgEl.appendChild(g);
}
// Draw state nodes on top of edges
for (const s of lts.states) {
const g = mk('g', { class: 'node', transform: `translate(${s.x},${s.y})` });
const cls = s.id === curState ? 'current' : (s.visited ? 'visited' : 'plain');
const c = mk('circle', { r: 14, class: cls, stroke: '#4a7fa5', 'stroke-width': 1.5 });
const t = mk('text', { 'text-anchor': 'middle', dy: '0.35em' });
t.textContent = s.id;
g.append(c, t);
svgEl.appendChild(g);
}
}
function assignEdgeIds(lts, prefix) {
// Assign stable string IDs to edges for fired/replied tracking
lts.edges.forEach((e, i) => { e.id = prefix + '_e' + i; });
}
function setStatus(msg, cls) {
const el = document.getElementById('status-bar');
el.textContent = msg;
el.className = 'status-bar ' + cls;
}
function loadExample(idx) {
exIdx = idx;
const ex = EXAMPLES[idx];
assignEdgeIds(ex.A, 'A');
assignEdgeIds(ex.B, 'B');
curA = ex.A.init;
curB = ex.B.init;
firedEdge = null; repliedEdge = null;
ex.A.states.forEach(s => { s.visited = false; });
ex.B.states.forEach(s => { s.visited = false; });
const isB = ex.bisim.length > 0;
document.getElementById('hint-text').innerHTML = isB ? 'Systems A and B <strong>are bisimilar</strong>: every move has a matching reply. Click any arrow to play as the Attacker — the Defender replies automatically.' : 'Systems A and B <strong>are NOT bisimilar</strong>: after action <em>a</em>, System A must choose between <em>b</em> and <em>c</em>, while System B can do both from the same state. The Attacker wins by firing <em>a</em> to the branch the Defender cannot match.';
setStatus('Click an arrow on either side to fire a move as the Attacker.', 'info');
render();
}
function render() {
const ex = EXAMPLES[exIdx];
drawLTS(document.getElementById('svg-a'), ex.A, curA, firedEdge, repliedEdge);
drawLTS(document.getElementById('svg-b'), ex.B, curB, firedEdge, repliedEdge);
// Re-attach click listeners after every redraw
attachListeners(document.getElementById('svg-a'), 'A', ex.A);
attachListeners(document.getElementById('svg-b'), 'B', ex.B);
}
function findReply(fromSystem, toSystem, fromState, label, ex) {
// Defender strategy: find an edge with the same label from the defender's current state
// The Defender looks for an edge with the same label from toSystem's current state
// such that the reached states are bisimilar to the attacker's target.
const attEdge = fromSystem.edges.find(e => e.id === firedEdge);
if (!attEdge) return null;
const targetState = attEdge.to;
for (const e of toSystem.edges) {
if (e.from !== (toSystem === ex.A ? curA : curB)) continue;
if (e.label !== label) continue;
// Check that the reached pair is in the bisimulation relation
const aState = toSystem === ex.A ? e.to : targetState;
const bState = toSystem === ex.A ? targetState : e.to;
if (isBisimilar(aState, bState, ex)) return e;
}
return null;
}
function attackFrom(side, edgeId, ex) {
// Player fires a transition; defender auto-replies with best move
const attSystem = side === 'A' ? ex.A : ex.B;
const defSystem = side === 'A' ? ex.B : ex.A;
const edge = attSystem.edges.find(e => e.id === edgeId);
if (!edge) return;
const curSrc = side === 'A' ? curA : curB;
if (edge.from !== curSrc) {
setStatus('That transition does not start from the current state.', 'bad'); return;
}
firedEdge = edgeId;
// Advance attacker's current state
if (side === 'A') { curA = edge.to; } else { curB = edge.to; }
render();
// Defender chooses a reply edge with matching label landing in a bisimilar pair
const reply = findReply(attSystem, defSystem, edge.from, edge.label, ex);
if (reply) {
repliedEdge = reply.id;
if (side === 'A') { curB = reply.to; } else { curA = reply.to; }
setStatus(i18nFmt('Matched! Action '%label%' answered — systems still bisimilar.', { label: edge.label }), 'ok');
render();
} else {
if (ex.bisim.length === 0) {
setStatus(i18nFmt('No reply for action '%label%' — the Defender is stuck. Not bisimilar!', { label: edge.label }), 'bad');
} else {
setStatus(i18nFmt('No reply for action '%label%' — the Defender is stuck. Not bisimilar!', { label: edge.label }), 'bad');
}
}
}
function attachListeners(svgEl, side, lts) {
// Attach click handlers to every edge group in the SVG
for (const g of svgEl.querySelectorAll('.edge-btn')) {
g.addEventListener('click', () => {
attackFrom(side, g.dataset.edgeId, EXAMPLES[exIdx]);
});
}
}
document.getElementById('btn-example1').addEventListener('click', () => {
document.querySelectorAll('.ex-btn').forEach(b => b.classList.remove('active'));
document.getElementById('btn-example1').classList.add('active');
loadExample(0);
});
document.getElementById('btn-example2').addEventListener('click', () => {
document.querySelectorAll('.ex-btn').forEach(b => b.classList.remove('active'));
document.getElementById('btn-example2').classList.add('active');
loadExample(1);
});
document.getElementById('btn-reset').addEventListener('click', () => loadExample(exIdx));
loadExample(0);
<!-- Two labeled transition systems side by side for the bisimulation game -->
<div class="controls">
<button id="btn-example1" class="ex-btn active" type="button">Example 1: bisimilar</button>
<button id="btn-example2" class="ex-btn" type="button">Example 2: not bisimilar</button>
</div>
<p class="hint" id="hint-text">Systems A and B <strong>are bisimilar</strong>: every move has a matching reply. Click any arrow to play as the Attacker — the Defender replies automatically.</p>
<div class="arena">
<div class="lts-panel">
<div class="lts-title">System A</div>
<svg id="svg-a" class="lts-svg" viewBox="0 0 200 160"></svg>
</div>
<div class="divider">↔</div>
<div class="lts-panel">
<div class="lts-title">System B</div>
<svg id="svg-b" class="lts-svg" viewBox="0 0 200 160"></svg>
</div>
</div>
<div class="status-bar" id="status-bar">Click an arrow on either side to fire a move as the Attacker.</div>
<div class="btns">
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Layout for the bisimulation demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; }
.controls { display: flex; gap: .4rem; margin-bottom: .6rem; }
.ex-btn { font: 600 13px system-ui; padding: .3rem .75rem; border: 1px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 6px; cursor: pointer; }
.ex-btn.active { background: #1d3557; color: #fff; }
.hint { font-size: .85rem; color: #444; margin-bottom: .5rem; line-height: 1.45; }
.arena { display: flex; align-items: center; gap: .5rem; }
.lts-panel { flex: 1; min-width: 0; }
.lts-title { font-size: .8rem; font-weight: 700; text-align: center; color: #555;
margin-bottom: .2rem; letter-spacing: .04em; text-transform: uppercase; }
.lts-svg { width: 100%; border: 1px solid #d0d9e3; border-radius: 8px; background: #f7f9fb; }
.divider { font-size: 1.4rem; color: #aaa; user-select: none; }
.status-bar { font-size: .9rem; font-weight: 600; min-height: 1.4em;
margin: .5rem 0; padding: .3rem .5rem; border-radius: 6px; }
.status-bar.ok { background: #d4f5dc; color: #0a5c23; }
.status-bar.bad { background: #fde8ea; color: #9b2335; }
.status-bar.info { background: #e8f0fb; color: #1a3a6b; }
.btns { margin-top: .4rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* SVG node and edge styling for the LTS diagrams */
.node circle { cursor: pointer; transition: fill .15s; }
.node circle.current { fill: #1d3557; stroke: #1d3557; }
.node circle.visited { fill: #e8eef3; stroke: #4a7fa5; }
.node circle.plain { fill: #fff; stroke: #4a7fa5; }
.node text { font: 700 11px ui-monospace, monospace; fill: #1d3557; pointer-events: none; }
.node circle.current + text { fill: #fff; }
.edge { stroke: #4a7fa5; stroke-width: 1.5; fill: none; }
.edge-label { font: 600 11px ui-monospace, monospace; fill: #2a5f87; }
.edge-btn { cursor: pointer; }
.edge-btn:hover .edge { stroke: #c92f3c; }
.edge-btn:hover .edge-label { fill: #c92f3c; }
.edge-btn.fired .edge { stroke: #e63946; }
.edge-btn.fired .edge-label { fill: #e63946; }
.edge-btn.replied .edge { stroke: #0a7d33; }
.edge-btn.replied .edge-label { fill: #0a7d33; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Bisimulation game — Attacker fires a transition, Defender replies with a matching one
// in the other system. If the Defender can always reply, the systems are bisimilar.
// Example 1: bisimilar pair — branching collapse
const EXAMPLES = [
{
// System A: two branches both looping on b; System B: one state looping on b
A: {
states: [{id:'s0',x:100,y:30},{id:'s1',x:40,y:110},{id:'s2',x:160,y:110}],
edges: [{from:'s0',to:'s1',label:'a'},{from:'s0',to:'s2',label:'a'},
{from:'s1',to:'s1',label:'b'},{from:'s2',to:'s2',label:'b'}],
init: 's0'
},
B: {
states: [{id:'t0',x:100,y:30},{id:'t1',x:100,y:110}],
edges: [{from:'t0',to:'t1',label:'a'},{from:'t1',to:'t1',label:'b'}],
init: 't0'
},
// Bisimulation relation: s1~t1, s2~t1 (both loop on b)
bisim: [['s0','t0'],['s1','t1'],['s2','t1']]
},
{
// Example 2: NOT bisimilar — different options after a
A: {
states: [{id:'p0',x:100,y:30},{id:'p1',x:60,y:110},{id:'p2',x:140,y:110}],
edges: [{from:'p0',to:'p1',label:'a'},{from:'p0',to:'p2',label:'a'},
{from:'p1',to:'p1',label:'b'},{from:'p2',to:'p2',label:'c'}],
init: 'p0'
},
B: {
states: [{id:'q0',x:100,y:30},{id:'q1',x:100,y:110}],
edges: [{from:'q0',to:'q1',label:'a'},{from:'q1',to:'q1',label:'b'},
{from:'q1',to:'q1',label:'c'}],
init: 'q0'
},
bisim: [] // no bisimulation exists
}
];
let exIdx = 0;
let curA, curB;
let firedEdge = null, repliedEdge = null;
function isBisimilar(a, b, ex) {
// Check if (a,b) is in the bisimulation relation for this example
return ex.bisim.some(([x, y]) => x === a && y === b);
}
function drawLTS(svgEl, lts, curState, firedId, repliedId) {
// Draw all edges first (below nodes), then nodes on top
svgEl.innerHTML = '';
const ns = 'http://www.w3.org/2000/svg';
function mk(tag, attrs) {
const el = document.createElementNS(ns, tag);
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
return el;
}
// Draw edges
for (const e of lts.edges) {
const sf = lts.states.find(s => s.id === e.from);
const st = lts.states.find(s => s.id === e.to);
const isSelf = e.from === e.to;
const g = mk('g', { class: 'edge-btn' + (e.id === firedId ? ' fired' : e.id === repliedId ? ' replied' : '') });
g.dataset.edgeId = e.id;
if (isSelf) {
// Self-loop arc above the node
const cx = sf.x, cy = sf.y;
const path = mk('path', { class: 'edge', d: `M${cx-8},${cy-14} C${cx-28},${cy-38} ${cx+28},${cy-38} ${cx+8},${cy-14}` });
const arrow = mk('polygon', { points: `${cx+8},${cy-14} ${cx+4},${cy-22} ${cx+14},${cy-20}`, fill: '#4a7fa5', stroke: 'none', class: (e.id === firedId ? 'fired' : e.id === repliedId ? 'replied' : '') });
if (e.id === firedId) { arrow.setAttribute('fill','#e63946'); } else if (e.id === repliedId) { arrow.setAttribute('fill','#0a7d33'); }
const lbl = mk('text', { class: 'edge-label', x: cx, y: cy - 42, 'text-anchor': 'middle' });
lbl.textContent = e.label;
g.append(path, arrow, lbl);
} else {
// Straight directed edge with arrowhead
const dx = st.x - sf.x, dy = st.y - sf.y, len = Math.hypot(dx, dy);
const ux = dx / len, uy = dy / len;
const r = 14;
const x1 = sf.x + ux * r, y1 = sf.y + uy * r;
const x2 = st.x - ux * r, y2 = st.y - uy * r;
const path = mk('path', { class: 'edge', d: `M${x1},${y1} L${x2},${y2}` });
const ax = x2 - ux*7 - uy*5, ay = y2 - uy*7 + ux*5;
const bx = x2 - ux*7 + uy*5, by = y2 - uy*7 - ux*5;
const arrow = mk('polygon', { points: `${x2},${y2} ${ax},${ay} ${bx},${by}`, fill: '#4a7fa5', stroke: 'none' });
if (e.id === firedId) { arrow.setAttribute('fill','#e63946'); } else if (e.id === repliedId) { arrow.setAttribute('fill','#0a7d33'); }
const mx = (x1 + x2) / 2 - uy * 9, my = (y1 + y2) / 2 + ux * 9;
const lbl = mk('text', { class: 'edge-label', x: mx, y: my, 'text-anchor': 'middle' });
lbl.textContent = e.label;
g.append(path, arrow, lbl);
}
svgEl.appendChild(g);
}
// Draw state nodes on top of edges
for (const s of lts.states) {
const g = mk('g', { class: 'node', transform: `translate(${s.x},${s.y})` });
const cls = s.id === curState ? 'current' : (s.visited ? 'visited' : 'plain');
const c = mk('circle', { r: 14, class: cls, stroke: '#4a7fa5', 'stroke-width': 1.5 });
const t = mk('text', { 'text-anchor': 'middle', dy: '0.35em' });
t.textContent = s.id;
g.append(c, t);
svgEl.appendChild(g);
}
}
function assignEdgeIds(lts, prefix) {
// Assign stable string IDs to edges for fired/replied tracking
lts.edges.forEach((e, i) => { e.id = prefix + '_e' + i; });
}
function setStatus(msg, cls) {
const el = document.getElementById('status-bar');
el.textContent = msg;
el.className = 'status-bar ' + cls;
}
function loadExample(idx) {
exIdx = idx;
const ex = EXAMPLES[idx];
assignEdgeIds(ex.A, 'A');
assignEdgeIds(ex.B, 'B');
curA = ex.A.init;
curB = ex.B.init;
firedEdge = null; repliedEdge = null;
ex.A.states.forEach(s => { s.visited = false; });
ex.B.states.forEach(s => { s.visited = false; });
const isB = ex.bisim.length > 0;
document.getElementById('hint-text').innerHTML = isB ? 'Systems A and B <strong>are bisimilar</strong>: every move has a matching reply. Click any arrow to play as the Attacker — the Defender replies automatically.' : 'Systems A and B <strong>are NOT bisimilar</strong>: after action <em>a</em>, System A must choose between <em>b</em> and <em>c</em>, while System B can do both from the same state. The Attacker wins by firing <em>a</em> to the branch the Defender cannot match.';
setStatus('Click an arrow on either side to fire a move as the Attacker.', 'info');
render();
}
function render() {
const ex = EXAMPLES[exIdx];
drawLTS(document.getElementById('svg-a'), ex.A, curA, firedEdge, repliedEdge);
drawLTS(document.getElementById('svg-b'), ex.B, curB, firedEdge, repliedEdge);
// Re-attach click listeners after every redraw
attachListeners(document.getElementById('svg-a'), 'A', ex.A);
attachListeners(document.getElementById('svg-b'), 'B', ex.B);
}
function findReply(fromSystem, toSystem, fromState, label, ex) {
// Defender strategy: find an edge with the same label from the defender's current state
// The Defender looks for an edge with the same label from toSystem's current state
// such that the reached states are bisimilar to the attacker's target.
const attEdge = fromSystem.edges.find(e => e.id === firedEdge);
if (!attEdge) return null;
const targetState = attEdge.to;
for (const e of toSystem.edges) {
if (e.from !== (toSystem === ex.A ? curA : curB)) continue;
if (e.label !== label) continue;
// Check that the reached pair is in the bisimulation relation
const aState = toSystem === ex.A ? e.to : targetState;
const bState = toSystem === ex.A ? targetState : e.to;
if (isBisimilar(aState, bState, ex)) return e;
}
return null;
}
function attackFrom(side, edgeId, ex) {
// Player fires a transition; defender auto-replies with best move
const attSystem = side === 'A' ? ex.A : ex.B;
const defSystem = side === 'A' ? ex.B : ex.A;
const edge = attSystem.edges.find(e => e.id === edgeId);
if (!edge) return;
const curSrc = side === 'A' ? curA : curB;
if (edge.from !== curSrc) {
setStatus('That transition does not start from the current state.', 'bad'); return;
}
firedEdge = edgeId;
// Advance attacker's current state
if (side === 'A') { curA = edge.to; } else { curB = edge.to; }
render();
// Defender chooses a reply edge with matching label landing in a bisimilar pair
const reply = findReply(attSystem, defSystem, edge.from, edge.label, ex);
if (reply) {
repliedEdge = reply.id;
if (side === 'A') { curB = reply.to; } else { curA = reply.to; }
setStatus(i18nFmt('Matched! Action '%label%' answered — systems still bisimilar.', { label: edge.label }), 'ok');
render();
} else {
if (ex.bisim.length === 0) {
setStatus(i18nFmt('No reply for action '%label%' — the Defender is stuck. Not bisimilar!', { label: edge.label }), 'bad');
} else {
setStatus(i18nFmt('No reply for action '%label%' — the Defender is stuck. Not bisimilar!', { label: edge.label }), 'bad');
}
}
}
function attachListeners(svgEl, side, lts) {
// Attach click handlers to every edge group in the SVG
for (const g of svgEl.querySelectorAll('.edge-btn')) {
g.addEventListener('click', () => {
attackFrom(side, g.dataset.edgeId, EXAMPLES[exIdx]);
});
}
}
document.getElementById('btn-example1').addEventListener('click', () => {
document.querySelectorAll('.ex-btn').forEach(b => b.classList.remove('active'));
document.getElementById('btn-example1').classList.add('active');
loadExample(0);
});
document.getElementById('btn-example2').addEventListener('click', () => {
document.querySelectorAll('.ex-btn').forEach(b => b.classList.remove('active'));
document.getElementById('btn-example2').classList.add('active');
loadExample(1);
});
document.getElementById('btn-reset').addEventListener('click', () => loadExample(exIdx));
loadExample(0);
Click a transition on either side to play as the Attacker. The demo plays the Defender's best reply automatically. Notice that System A and System B are bisimilar: every move has a matching answer. To see a non-bisimilar pair, switch to the second example — System C can perform action a and then be forced into b, while System D can choose after a; the Attacker exploits that branch to win.
Comments
Loading comments...