# Known equivalent mutants for scripts/mutate.mjs: mutants that survive because they cannot change behaviour on any # state the program can reach, not because a test is missing. One per line: # : => # # Copy the "allow-list line" the script prints for a survivor and replace . Only add a line after you have # convinced yourself the mutant is equivalent; otherwise it is a test gap, and the fix is a test. pong/rules.js: (nx === PLAYER_X && vel.dx < 0 && hits(player, ny)) || => (nx === PLAYER_X && vel.dx <= 0 && hits(player, ny)) || # vel.dx is always -1 or 1 (initialPong sets 1, a bounce negates it, serve sets -1 or 1), so < 0 and <= 0 agree pong/rules.js: (nx === PLAYER_X && vel.dx < 0 && hits(player, ny)) || => (nx === PLAYER_X && vel.dx < 1 && hits(player, ny)) || # vel.dx is always -1 or 1, so < 0 and < 1 agree pong/rules.js: (nx === CPU_X && vel.dx > 0 && hits(cpu, ny)) => (nx === CPU_X && vel.dx >= 0 && hits(cpu, ny)) # vel.dx is always -1 or 1, so > 0 and >= 0 agree pong/view.js: text(320, 200, state.score.player > state.score.cpu ? 'You Win!' : 'CPU Wins', 32), => text(320, 200, state.score.player >= state.score.cpu ? 'You Win!' : 'CPU Wins', 32), # only shown when status is Over; a match ends the moment one side reaches WIN and only one point is scored per frame, so the scores are never equal there check.js: .map((s, i) => (i > 0 && !sameHead(s.snake, states[i - 1].snake) ? i : -1)) => .map((s, i) => (i > 0 && !sameHead(s.snake, states[i - 1].snake) ? i : -2)) # -1 is only a "no move here" marker that the next line filters out with i >= 0; -2 is filtered out the same way check.js: .filter((i) => i >= 0) => .filter((i) => i > 0) # the indices kept come from i > 0 on the line above, so 0 never occurs and >= 0 and > 0 agree check.js: .filter((i) => i >= 0) => .filter((i) => i >= 1) # the indices kept come from i > 0 on the line above (integers >= 1, or the marker -1), so >= 0 and >= 1 agree safeio.js: return isDone(r.state) ? [r.state, toArray(outs)] : go(r.state, budget * 2, outs) => return isDone(r.state) ? [r.state, toArray(outs)] : go(r.state, budget * 3, outs) # the budget only decides how many steps run per chunk; the chunks' outputs are joined in order, so any growth factor >= 2 gives the same result (it only changes the stack depth bound) safeio.js: return go(start, 1, leaf([])) => return go(start, 2, leaf([])) # the first budget only decides the chunk sizes; the outputs are joined in order and runFor stops at isDone, so the result is the same life/rules.js: const neighbours = (p) => offsets.map(([dx, dy]) => ({ x: p.x + dx, y: p.y + dy })).filter(onBoard) => const neighbours = (p) => offsets.map(([dx, dy]) => ({ x: p.x - dx, y: p.y + dy })).filter(onBoard) # the eight offsets are symmetric (with (dx, dy) the list also holds (-dx, dy)), so negating dx gives the same set of neighbours, only in another order, and only their count is used life/rules.js: const neighbours = (p) => offsets.map(([dx, dy]) => ({ x: p.x + dx, y: p.y + dy })).filter(onBoard) => const neighbours = (p) => offsets.map(([dx, dy]) => ({ x: p.x + dx, y: p.y - dy })).filter(onBoard) # the eight offsets are symmetric (with (dx, dy) the list also holds (dx, -dy)), so negating dy gives the same set of neighbours, only in another order, and only their count is used life/rules.js: : [...cells.filter((c) => index(c) < index(p)), { x: p.x, y: p.y }, ...cells.filter((c) => index(c) > index(p))] => : [...cells.filter((c) => index(c) <= index(p)), { x: p.x, y: p.y }, ...cells.filter((c) => index(c) > index(p))] # this branch is only reached when no cell has p's index (the line above removes p if it is present), so < and <= agree life/rules.js: : [...cells.filter((c) => index(c) < index(p)), { x: p.x, y: p.y }, ...cells.filter((c) => index(c) > index(p))] => : [...cells.filter((c) => index(c) < index(p)), { x: p.x, y: p.y }, ...cells.filter((c) => index(c) >= index(p))] # this branch is only reached when no cell has p's index (the line above removes p if it is present), so > and >= agree