Cascading Cards v1
Front to back version, which is is incorrect but easier to do in Turtletoy API... will work on the proper version next
Trying to recreate the cascade effect from Windows Solitaire Win Screen.
Lifted most of the helper code from 'A night of poker' (A night of poker ♠️)
Log in to post a comment.
// Forked from "A night of poker ♠️" by Jurgen
// Modified for Windows Solitaire Cascade Effect
// --- ADJUSTABLE PARAMETERS ---
// -- Simulation Control --
const maxThrows = 6; // min=1 max=100 step=1 (Total cards to throw)
const maxGlobalSteps = 10000; // min=1000 max=20000 step=100 (Emergency stop)
const maxCardSteps = 300; // min=50 max=1000 step=10 (Kill specific card if it bounces too long)
const safetyEarlyStop = 1; // min=0 max=1 step=1 (1=Stop if stuck on floor for 100 steps)
const seed = 0; // min=0 max=10000 step=1 (0=Random, >0=Fixed Pattern)
// -- Spawning --
const numStacks = 4; // min=2 max=8 step=1 (Number of spawn points)
const stackOrder = 1; // min=0 max=1 step=1 (0=Random, 1=Round Robin)
// -- Physics: Movement --
const gravityVal = 90; // min=10 max=200 step=10
const initVelXMin = 10; // min=0 max=50 step=1 (Horizontal Speed Min)
const initVelXMax = 30; // min=0 max=100 step=1 (Horizontal Speed Max)
// -- Physics: Material --
const bouncinessMin = 0.60; // min=0.1 max=1.2 step=0.05
const bouncinessMax = 0.85; // min=0.1 max=1.2 step=0.05
const floorFriction = 0.85; // min=0.50 max=0.99 step=0.01 (Slide resistance on floor)
const enableSpin = 0; // min=0 max=1 step=1 (No, Yes)
// -- Visuals --
const drawSkip = 1; // min=1 max=10 step=1 (Draw every Nth physics frame. 1=All, 2=Half, etc)
const hatchScale = 2.0; // Scale for card detail (Higher = Faster/Cleaner)
Canvas.setpenopacity(1);
loadEnum();
loadCardGames();
turtlelib_init();
// --- SEEDING ---
if (seed === 0) {
R.seedRandom();
} else {
R.seed(seed);
}
const turtle = new Turtle();
const polygons = new Polygons();
// --- PHYSICS CONSTANTS ---
const GRAVITY_VEC = [0, gravityVal];
const DT = 0.05;
const FLOOR_Y = 90;
const CARD_WIDTH = 22;
const CARD_HEIGHT = 30;
const X_LIMIT = 115;
// Velocity threshold below which we force the card to stop
const STOP_VELOCITY_THRESHOLD = 2.0;
// How many steps a card must be near the floor to trigger safety stop
const FLOOR_STUCK_LIMIT = 100;
// --- PHYSICS ENGINE ---
const V2 = {
add: (a, b) => [a[0] + b[0], a[1] + b[1]],
scale: (a, s) => [a[0] * s, a[1] * s],
sub: (a, b) => [a[0] - b[0], a[1] - b[1]],
dot: (a, b) => a[0] * b[0] + a[1] * b[1],
cross: (a, b) => a[0] * b[1] - a[1] * b[0],
rotate: (v, angle) => [
v[0] * Math.cos(angle) - v[1] * Math.sin(angle),
v[0] * Math.sin(angle) + v[1] * Math.cos(angle)
],
lenSq: (a) => a[0]**2 + a[1]**2
};
class CardBody {
constructor(center, width, height, cardObj, vel, restitution) {
this.center = [...center];
this.velocity = vel;
this.acceleration = [...GRAVITY_VEC];
this.angle = 0;
this.angleVelocity = 0;
// Physics Properties
this.mass = 1;
this.inverseMass = 1;
this.restitution = restitution;
// Dimensions & Visuals
this.width = width;
this.height = height;
this.cardObj = cardObj;
// Inertia
this.inertia = this.mass * (width ** 2 + height ** 2) / 12;
this.inverseInertia = enableSpin ? (1 / this.inertia) : 0;
this.vertices = [];
this.updateVertices();
}
updateVertices() {
const hw = this.width / 2;
const hh = this.height / 2;
const localVerts = [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]];
if (this.angle === 0) {
this.vertices = localVerts.map(v => V2.add(this.center, v));
} else {
this.vertices = localVerts.map(v => V2.add(this.center, V2.rotate(v, this.angle)));
}
}
update(dt) {
this.velocity = V2.add(this.velocity, V2.scale(this.acceleration, dt));
this.center = V2.add(this.center, V2.scale(this.velocity, dt));
if(enableSpin) this.angle += this.angleVelocity * dt;
this.updateVertices();
}
applyImpulse(impulse, contactVector) {
this.velocity = V2.add(this.velocity, V2.scale(impulse, this.inverseMass));
if (enableSpin) {
this.angleVelocity += this.inverseInertia * V2.cross(contactVector, impulse);
}
}
draw(turtle) {
this.cardObj.draw(turtle, this.center, this.width, this.height, this.angle);
}
}
// --- STATE MANAGEMENT ---
let activeCard = null;
let cardsThrownCount = 0;
let cardSteps = 0;
let stepsOnFloor = 0;
const deckReference = Deck.standard();
deckReference.shuffle();
// Pre-calculate spawn X positions evenly across width
const stackXPositions = [];
const safeWidth = 170;
const startX = -safeWidth / 2;
const stepX = numStacks > 1 ? safeWidth / (numStacks - 1) : 0;
for(let i=0; i<numStacks; i++) {
stackXPositions.push(numStacks === 1 ? 0 : startX + (i * stepX));
}
function resolveFloorCollisions(body) {
if (body.center[1] < 50) return; // Optimization: don't check if high up
let collided = false;
for (const vertex of body.vertices) {
const penetration = vertex[1] - FLOOR_Y;
if (penetration > 0) {
collided = true;
body.center[1] -= penetration;
body.updateVertices();
const r = V2.sub(vertex, body.center);
const rotVel = [-body.angleVelocity * r[1], body.angleVelocity * r[0]];
const contactVel = V2.add(body.velocity, rotVel);
const normal = [0, -1];
// Only bounce if moving down
if (V2.dot(contactVel, normal) > 0) continue;
const rCrossN = V2.cross(r, normal);
let j = -(1 + body.restitution) * V2.dot(contactVel, normal);
j /= (body.inverseMass + (rCrossN * rCrossN) * body.inverseInertia);
body.applyImpulse(V2.scale(normal, j), r);
// Apply Friction
body.velocity[0] *= floorFriction;
}
}
}
function walk(i) {
// 0. Global Safety Stop
if (i >= maxGlobalSteps) return false;
// 1. Check if we need to spawn a new card
if (activeCard === null) {
if (cardsThrownCount >= maxThrows) return false; // Simulation Done
// --- SPAWN LOGIC ---
// 1. Pick stack position
let spawnX;
if(stackOrder === 1) {
// Round Robin
spawnX = stackXPositions[cardsThrownCount % stackXPositions.length];
} else {
// Random
spawnX = stackXPositions[Math.floor(Math.random() * stackXPositions.length)];
}
// 2. Pick random direction (Left or Right)
const dir = Math.random() > 0.5 ? 1 : -1;
// 3. Random Force based on Min/Max parameters
const speedX = initVelXMin + Math.random() * (initVelXMax - initVelXMin);
const vx = speedX * dir;
const vy = -(Math.random() * 10); // Small pop up vertical variation
// 4. Random Bounciness based on Min/Max parameters
const restitution = bouncinessMin + Math.random() * (bouncinessMax - bouncinessMin);
const cardArt = deckReference.getTop();
if(!cardArt) {
deckReference.reset();
deckReference.shuffle();
return true; // Wait for next loop to spawn
}
cardArt.faceUp = true;
activeCard = new CardBody([spawnX, -50], CARD_WIDTH, CARD_HEIGHT, cardArt, [vx, vy], restitution);
if(enableSpin) activeCard.angle = Math.random() * 0.4 - 0.2;
cardsThrownCount++;
cardSteps = 0;
stepsOnFloor = 0;
}
// 2. Simulate Active Card
activeCard.update(DT);
resolveFloorCollisions(activeCard);
// Draw (controlled by drawSkip parameter)
if (cardSteps % drawSkip === 0) {
activeCard.draw(turtle);
}
cardSteps++;
// 3. Check Termination Conditions for the current card
const isOffScreenX = activeCard.center[0] < -X_LIMIT || activeCard.center[0] > X_LIMIT;
// Check if near floor
const isNearFloor = activeCard.center[1] > FLOOR_Y - 5;
if (isNearFloor) {
stepsOnFloor++;
} else {
stepsOnFloor = 0; // Reset if it bounces up significantly
}
// Generous stop condition: if slow and near floor
const isSlow = Math.abs(activeCard.velocity[0]) < STOP_VELOCITY_THRESHOLD && Math.abs(activeCard.velocity[1]) < STOP_VELOCITY_THRESHOLD;
const isStopped = isSlow && isNearFloor;
// Stop if this specific card has been bouncing for too long
const isTimeout = cardSteps > maxCardSteps;
// Safety Early Stop: Kill if it's been grounded for too long (dragging on floor)
const isFloorStuck = (safetyEarlyStop === 1) && (stepsOnFloor > FLOOR_STUCK_LIMIT);
if (isOffScreenX || isStopped || isTimeout || isFloorStuck) {
activeCard = null; // Card finished, ready for next one
}
return true;
}
// Library code below:
const getPos = (r, theta) => [r * 145 * Math.sin(theta), r * 145 * Math.cos(theta)];
function centerPlotAt(turtle, paths, pos = [0, 0]) {
const bb = pathsBB(paths);
const centerCorrection = [
-(bb[1][0] - bb[0][0]) / 2 - bb[0][0],
-bb[1][1] + (bb[1][1] - bb[0][1]) / 2
];
paths.forEach(path => PT.draw(turtle, path.map(pt => V.add(V.add(pt, centerCorrection), pos))));
}
function pathsBB(paths) {
return paths.map(path => pathBB(path)).reduce((a, c) => [
[Math.min(a[0][0], c[0][0]), Math.min(a[0][1], c[0][1])],
[Math.max(a[1][0], c[1][0]), Math.max(a[1][1], c[1][1])]
], [
[Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],
[Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER]
]);
}
function pathBB(path) {
return path.reduce((a, c) => [
[Math.min(a[0][0], c[0]), Math.min(a[0][1], c[1])],
[Math.max(a[1][0], c[0]), Math.max(a[1][1], c[1])]
], [
[Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],
[Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER]
]);
}
function chip(turtle, polygons, r = 5, pos = [-80, 0], hatchIdx = 0, nSegments = 10, rotation = 0) {
const diamondHatch = 6;
const hatchSettings = [
[[], [[1, .15]], [[1, .15]], []],
[[[1, .15]], [], [], [[1, .15]]],
[
[[rotation+1, 1 / (diamondHatch/r)], [rotation-1, 1 / (diamondHatch/r)]], //center
[], //segments
[], //ring
[[1, .15]] // outer ring (with segments)
],
[[[1, .15]], [[1, .15]], [], []],
[[[rotation+1, 1 / (diamondHatch/r)], [rotation-1, 1 / (diamondHatch/r)]], [[rotation+1, 1 / (diamondHatch/r)], [rotation-1, 1 / (diamondHatch/r)]], [[1, .15]], []],
];
const segmentPts = [
...PT.arc(.8 * r, Math.PI / nSegments, 0, (.8 * r * 7 / nSegments) | 0, true),
...PT.arc( r, Math.PI / nSegments, 0, (r * 7 / nSegments) | 0, true).reverse()
];
for(let i = 0; i < nSegments; i++) {
const seg = polygons.create();
seg.addPoints(...segmentPts.map(pt => V.trans(V.rot2d(rotation+i * 2 * Math.PI / nSegments), pt)).map(pt => V.add(pt, pos)));
hatchSettings[hatchIdx][1].forEach(h => seg.addHatching(h[0], hatchScale * h[1]));
seg.addOutline();
polygons.draw(turtle, seg);
}
[
[.55, hatchSettings[hatchIdx][0]],
[.7, hatchSettings[hatchIdx][2]],
[1, hatchSettings[hatchIdx][3]]
].forEach(rF => {
const outer = polygons.create();
outer.addPoints(...PT.circle(rF[0] * r).map(pt => V.add(pt, pos)));
rF[1].forEach(h => outer.addHatching(h[0], hatchScale * h[1]));
outer.addOutline();
polygons.draw(turtle, outer);
});
}
function loadCardGames() {
const cardRanks = [...Array.from({length: 9}, (e, i) => ''+(i+2)), 'J', 'Q', 'K', 'A'];
const cardFontPaths = [[[[-0.155,-0.137],[-0.155,-0.163],[-0.13,-0.212],[-0.105,-0.237],[-0.055,-0.263],[0.045,-0.263],[0.095,-0.237],[0.12,-0.212],[0.145,-0.163],[0.145,-0.112],[0.12,-0.063],[0.07,0.013],[-0.18,0.263],[0.17,0.263]]],[[[-0.13,-0.263],[0.145,-0.263],[-0.005,-0.063],[0.07,-0.063],[0.12,-0.037],[0.145,-0.012],[0.17,0.063],[0.17,0.113],[0.145,0.188],[0.095,0.238],[0.02,0.263],[-0.055,0.263],[-0.13,0.238],[-0.155,0.213],[-0.18,0.163]]],[[[0.057,-0.263],[-0.193,0.088],[0.183,0.088]],[[0.057,-0.263],[0.057,0.263]]],[[[0.12,-0.263],[-0.13,-0.263],[-0.155,-0.037],[-0.13,-0.063],[-0.055,-0.087],[0.02,-0.087],[0.095,-0.063],[0.145,-0.012],[0.17,0.063],[0.17,0.113],[0.145,0.188],[0.095,0.238],[0.02,0.263],[-0.055,0.263],[-0.13,0.238],[-0.155,0.213],[-0.18,0.163]]],[[[0.145,-0.187],[0.12,-0.237],[0.045,-0.263],[-0.005,-0.263],[-0.08,-0.237],[-0.13,-0.163],[-0.155,-0.037],[-0.155,0.088],[-0.13,0.188],[-0.08,0.238],[-0.005,0.263],[0.02,0.263],[0.095,0.238],[0.145,0.188],[0.17,0.113],[0.17,0.088],[0.145,0.013],[0.095,-0.037],[0.02,-0.063],[-0.005,-0.063],[-0.08,-0.037],[-0.13,0.013],[-0.155,0.088]]],[[[0.17,-0.263],[-0.08,0.263]],[[-0.18,-0.263],[0.17,-0.263]]],[[[-0.055,-0.263],[-0.13,-0.237],[-0.155,-0.187],[-0.155,-0.137],[-0.13,-0.087],[-0.08,-0.063],[0.02,-0.037],[0.095,-0.012],[0.145,0.037],[0.17,0.088],[0.17,0.163],[0.145,0.213],[0.12,0.238],[0.045,0.263],[-0.055,0.263],[-0.13,0.238],[-0.155,0.213],[-0.18,0.163],[-0.18,0.088],[-0.155,0.037],[-0.105,-0.012],[-0.03,-0.037],[0.07,-0.063],[0.12,-0.087],[0.145,-0.137],[0.145,-0.187],[0.12,-0.237],[0.045,-0.263],[-0.055,-0.263]]],[[[0.157,-0.087],[0.133,-0.012],[0.083,0.037],[0.008,0.063],[-0.017,0.063],[-0.092,0.037],[-0.143,-0.012],[-0.167,-0.087],[-0.167,-0.112],[-0.143,-0.187],[-0.092,-0.237],[-0.017,-0.263],[0.008,-0.263],[0.083,-0.237],[0.133,-0.187],[0.157,-0.087],[0.157,0.037],[0.133,0.163],[0.083,0.238],[0.008,0.263],[-0.043,0.263],[-0.117,0.238],[-0.143,0.188]]],[[[-0.355,-0.163],[-0.305,-0.187],[-0.23,-0.263],[-0.23,0.263]],[[0.22,-0.263],[0.145,-0.237],[0.095,-0.163],[0.07,-0.037],[0.07,0.037],[0.095,0.163],[0.145,0.238],[0.22,0.263],[0.27,0.263],[0.345,0.238],[0.395,0.163],[0.42,0.037],[0.42,-0.037],[0.395,-0.163],[0.345,-0.237],[0.27,-0.263],[0.22,-0.263]]],[[[0.107,-0.263],[0.107,0.137],[0.083,0.213],[0.058,0.238],[0.007,0.263],[-0.042,0.263],[-0.093,0.238],[-0.117,0.213],[-0.142,0.137],[-0.142,0.088]]],[[[-0.055,-0.263],[-0.105,-0.237],[-0.155,-0.187],[-0.18,-0.137],[-0.205,-0.063],[-0.205,0.063],[-0.18,0.137],[-0.155,0.188],[-0.105,0.238],[-0.055,0.263],[0.045,0.263],[0.095,0.238],[0.145,0.188],[0.17,0.137],[0.195,0.063],[0.195,-0.063],[0.17,-0.137],[0.145,-0.187],[0.095,-0.237],[0.045,-0.263],[-0.055,-0.263]],[[0.02,0.163],[0.17,0.313]]],[[[-0.168,-0.263],[-0.168,0.263]],[[0.183,-0.263],[-0.168,0.088]],[[-0.043,-0.037],[0.183,0.263]]],[[[-0.03,-0.263],[-0.23,0.263]],[[-0.03,-0.263],[0.17,0.263]],[[-0.155,0.088],[0.095,0.088]]]];;
const cardSuits = ['Hearts','Diamonds','Clubs','Spades'];
const cardNames = ['Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Jack', 'Queen', 'King', 'Ace'];
const SUITS = Enum.from(cardSuits);
const RANKS = Enum.from(cardNames);
this.SUITS = SUITS;
this.RANKS = RANKS;
// Fisher-Yates (aka Knuth) Shuffle
// https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array#2450976
function shuffle(array) {
let currentIndex = array.length, randomIndex;
// While there remain elements to shuffle.
while (currentIndex > 0) {
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
return array;
}
class Player {
#chips;
#hands;
#bet;
constructor(chips, bet) {
this.#chips = chips;
this.#bet = bet;
}
reset() {
this.#chips -= this.#bet;
this.#hands = [new Hand(this.#bet)];
}
deal(card, handIdx) {
this.#hands[handIdx].add(card);
}
set bet(b) {
this.#bet = b;
}
get bet() {
return this.#bet;
}
set chips(c) {
this.#chips = c;
}
get chips() {
return this.#chips;
}
get handCount() {
return this.#hands.length;
}
getHand(handIdx = 0) {
return this.#hands[handIdx];
}
addHand() {
this.#chips -= this.#bet;
this.#hands.push(new Hand(this.#bet));
}
}
this.Player = Player;
class Dealer {
#hand;
constructor() {
this.reset();
}
reset() {
this.#hand = new Hand();
}
hit(deck, player, handIdx = 0) {
if(player === undefined) {
const dealerCard = deck.getTop();
dealerCard.faceUp = (this.#hand.size !== 1)
this.#hand.add(dealerCard);
return;
}
const c = deck.getTop();
c.faceUp = true;
player.deal(c, handIdx);
}
draw(turtle, location) {
this.#hand.draw(turtle, location);
}
reveal() {
this.#hand.setFaceUp(1, true);
}
getHand() {
return this.#hand;
}
get handCount() {
return 1;
}
}
this.Dealer = Dealer;
class Card {
#suit = 0;
#rank = 0;
#faceUp = false;
constructor(suit, rank, faceUp = false) {
this.#suit = suit;
this.#rank = rank;
this.#faceUp = faceUp;
}
get faceUp() {
return this.#faceUp;
}
set faceUp(val) {
this.#faceUp = (val == true);
}
get rank() {
return this.#rank;
}
get suit() {
return this.#suit;
}
get name() {
return `${RANKS.key(this.#rank)} of ${SUITS.key(this.#suit)}`;
}
draw(turtle, pos, width = 13, height = 20, rotation = 0) {
const rot = V.rot2d(rotation);
if(this.#faceUp) {
const suit = getSuitSymbolPts(this.#suit, Math.min(width, height) * .6)
.map(pt => V.trans(rot, V.add(pt, [0, height / 4])));
const rank = cardFontPaths[RANKS.index(this.#rank)].map(path =>
path.map(pt => V.scale(pt, Math.min(width, height) * .8))
.map(pt => V.trans(rot, V.add(pt, [0, -height / 4])))
);
const rankPolygon = polygons.create();
rank.forEach(path => path.map(pt => V.add(pt, pos)).forEach((e, i, a) => {
if(i == a.length - 1) return;
rankPolygon.addSegments(e, a[(i+1)%a.length]);
}));
polygons.draw(turtle, rankPolygon);
const suitPolygon = polygons.create();
suitPolygon.addPoints(...suit.map(pt => V.add(pt, pos)));
suitPolygon.addOutline();
if(this.#suit & (SUITS.SPADES | SUITS.CLUBS)) {
suitPolygon.addHatching(1 + rotation, hatchScale * .15);
} else {
suitPolygon.addHatching(1 + rotation, 1);
suitPolygon.addHatching(-1 + rotation, 1);
}
polygons.draw(turtle, suitPolygon);
} else {
const coverCornerR = .8 * Math.min(width, height) / 6;
const coverMargin = Math.min(width, height) * .15;
const coverOutlines = [[1, 1], [-1, 1], [-1, -1], [1, -1]].flatMap((e, i) =>
PT.arc(coverCornerR, Math.PI / 2, i*Math.PI/2, (12 * coverCornerR) | 0, true).map(pt => V.add(V.mul(e, [(width-coverMargin)/2 - coverCornerR, (height-coverMargin)/2 - coverCornerR]), pt))
).map(pt => V.trans(rot, pt));
const coverPolygon = polygons.create();
coverPolygon.addPoints(...coverOutlines.map(pt => V.add(pt, pos)));
coverPolygon.addHatching(1 + rotation, 2);
coverPolygon.addHatching(-1 + rotation, 2);
coverPolygon.addOutline();
polygons.draw(turtle, coverPolygon);
}
const cornerR = Math.min(width, height) / 6;
const outlines = [[1, 1], [-1, 1], [-1, -1], [1, -1]].flatMap((e, i) =>
PT.arc(cornerR, Math.PI / 2, i*Math.PI/2, (12 * cornerR) | 0, true).map(pt => V.add(V.mul(e, [width/2 - cornerR, height/2 - cornerR]), pt))
).map(pt => V.trans(rot, pt));
const outlinesPolygon = polygons.create();
outlinesPolygon.addPoints(...outlines.map(pt => V.add(pt, pos)));
outlinesPolygon.addOutline();
polygons.draw(turtle, outlinesPolygon);
}
clone() {
return new Card(this.suit, this.rank, this.faceUp);
}
toString() {
return this.faceUp? this.name: 'Face down';
}
}
this.Card = Card;
class Deck {
#originalCards = [];
#deck = [];
constructor(cards) {
this.#originalCards = cards;
this.reset();
}
reset() {
this.#deck = this.#originalCards.map(v => v.clone());
}
shuffle() {
this.#deck = shuffle(this.#deck);
}
getTop() {
return this.#deck.pop();
}
get length() {
return this.#deck.length;
}
take(s, r) {
const card = this.#deck.find(c => c.suit == s && c.rank == r);
if(card) {
this.remove(s, r);
}
return card;
}
remove(s, r) {
this.#deck = this.#deck.filter(c => !(c.suit == s && c.rank == r));
}
add(card) {
this.#originalCards.push(card);
this.#deck.push(card);
}
peek(n = 0) {
return this.#deck[this.#deck.length - 1 - n].clone();
}
get size() {
return this.#deck.length;
}
static standard(n = 52) { //or 32 for only seven and higher
if(n != 52 && n != 32) throw new Error(`Only 52 or 32 card decks are supported at the moment`);
return new Deck(Object.keys(SUITS).flatMap((s, si) => Object.keys(RANKS).map((v, i) => new Card(SUITS[s], RANKS[v]))));
}
}
this.Deck = Deck;
class Hand {
#cards = [];
#bet;
constructor(bet) {
this.#bet = bet;
}
add(card) {
this.#cards.push(card);
}
takeLast() {
return this.#cards.pop();
}
peek() {
return this.#cards.map(v => v.clone());
}
set bet(b) {
this.#bet = b;
}
get bet() {
return this.#bet;
}
get size() {
return this.#cards.length;
}
draw(turtle, position, widthPerCard = 13, height = 20, margin = null, rotation = 0) {
const rot = V.rot2d(rotation);
margin = margin === null? widthPerCard/13: margin;
this.#cards.forEach((e, i) => {
e.draw(turtle, V.add(position, V.trans(rot, [(widthPerCard + margin) * i, 0])), widthPerCard, height, rotation);
})
}
setFaceUp(idx, faceUp) {
this.#cards[idx].faceUp = faceUp;
}
toString() {
return this.#cards.map(c => ''+c).join(', ');
}
}
this.Hand = Hand;
return;
function getSuitSymbolPts(suit, scale) {
switch(suit) {
case SUITS.HEARTS:
return getHeart(scale);
case SUITS.DIAMONDS:
return getDiamond(scale);
case SUITS.SPADES:
return getSpade(scale);
case SUITS.CLUBS:
return getClub(scale);
}
}
// ========== HEART ==========
function getHeart(scale) {
return getHeartPts(scale / 29).map(pt => V.add([0, -scale/12], pt));
}
// ========== DIAMOND ==========
function getDiamond(scale) {
return [
[0, -1],
[2/3, 0],
[0, 1],
[-2/3, 0],
[0, -1]
].map(pt => V.scale(pt, scale/2));
}
// ========== CLUB ==========
function getClub(scale) {
// Three circles for the clover
const r = scale / 3.9;
const rawStem = getStemPts(r).map(pt => [pt[0], r + pt[1]]);
const rightClub = getCirclePts(r, -Math.PI/2, 4*Math.PI / 3).map(pt => V.add(pt, V.scale([Math.sin(Math.PI / 3), Math.cos(Math.PI / 3)], r))).filter(pt => rawStem[0][0] < pt[0]);
const stem = rawStem.filter(pt => rightClub[rightClub.length - 1][1] < pt[1]);
return [
...getCirclePts(r, -Math.PI/2 - 2*Math.PI / 3, 4*Math.PI / 3).map(pt => [pt[0], pt[1] - r]), // Top
...rightClub,
...stem,
...rightClub.map(pt => [-pt[0], pt[1]]).reverse()
].map(pt => V.add(pt,[0, scale/50]));
}
// ========== SPADE ==========
function getSpade(scale) {
const r = scale / 31;
const rawStem = getStemPts(r*8).map(pt => V.add([0, scale/3.6], pt));
// Spade head (heart flipped vertically)
const pts = getHeartPts(r*.9).filter(pt => 0 < pt[1] || (pt[0] < -rawStem[0][0] || rawStem[0][0] < pt[0])).map(pt => [pt[0], -pt[1]]).reverse();
const stem = rawStem.filter(pt => pts[0][1] < pt[1]);
return [...pts, ...stem, pts[0]];
}
function getCirclePts(r, phase = 0, len = 2 * Math.PI) {
const pts = [];
for (let i = 0, max = Math.ceil(r * len); i <= max; i++) {
const a = len * (i / max);
pts.push([r * Math.cos(phase + a), r * Math.sin(phase + a)]);
}
return pts;
}
function getHeartPts(s) {
const halfPts = [];
for (let a = 0; a <= Math.PI; a += 0.05) {
const px = 16 * Math.pow(Math.sin(a), 3);
const py = 13 * Math.cos(a) - 5 * Math.cos(2 * a) - 2 * Math.cos(3 * a) - Math.cos(4 * a);
halfPts.push([s * px, -s * py]);
}
return [...halfPts, ...halfPts.map(pt => [-pt[0], pt[1]]).reverse()];
}
function getStemPts(r, h = r/2) {
const halfPts = getCirclePts(r, 4 * Math.PI / 6, Math.PI / 3).map(pt => [pt[0] + r*1.1, pt[1]]);
return [[r/10, -h], ...halfPts.reverse(), ...halfPts.map(pt => [-pt[0], pt[1]]).reverse(), [-r/10, -h]];
}
}
function loadEnum() {
function createEnum(...values) {
class Enum {
constructor(...values) {
values.forEach((v, i) => this[v.toUpperCase()] = 1 << i);
}
key(value) {
return values[Math.log2(value)].toUpperCase();
}
keys(value) {
const result = [];
for(let i = 0, max = values.length; i < max; i++) {
if( ((value >> i) & 1) === 1 ) result.push(values[i].toUpperCase());
}
return result;
}
index(value) {
return Math.log2(value);
}
indexes(value) {
const result = [];
for(let i = 0, max = values.length; i < max; i++) {
if( ((value >> i) & 1) === 1 ) result.push(1 << i);
}
return result;
}
get length() {
return values.length;
}
}
return new Proxy(Object.freeze(new Enum(...values)), {
get(target, name, receiver) {
if (Reflect.has(target, name)) {
let rv = Reflect.get(target, name, receiver);
if (typeof rv === "string") {
rv = rv.toUpperCase();
}
return rv;
}
throw new Error(`Unknown Enum property: ${name}`);
}
});
}
class EnumInterface {
static from(...arr) {
if(arr.length == 0) throw new Error(`Need at least one key to create an Enum`);
const keys =[...( (typeof arr[0]) == 'string'? arr: arr[0] )].map(v => v);
if(keys.some(k => (typeof k) != 'string')) throw new Error(`Cannot create Enum from other items than strings`);
return Object.freeze(new createEnum(...keys));
}
}
this.Enum = EnumInterface;
}
// Below is automatically maintained by Turtlelib 1.0
// Changes below this comment might interfere with its correct functioning.
function turtlelib_init() {
turtlelib_ns_c6665b0e9b_Jurgen_Vector_Math();
turtlelib_ns_c5f8fa95ed_Jurgen_Intersection();
turtlelib_ns_2d89bd6d64_Jurgen_Path_tools();
turtlelib_ns_80dcba45dd_Jurgen_Polygons();
turtlelib_ns_13b81fd40e_Jurgen_Randomness();
}
// Turtlelib Jurgen Vector Math v 4 - start - {"id":"c6665b0e9b","package":"Jurgen","name":"Vector Math","version":"4"}
function turtlelib_ns_c6665b0e9b_Jurgen_Vector_Math() {
/////////////////////////////////////////////////////////
// Vector functions - Created by Jurgen Westerhof 2024 //
/////////////////////////////////////////////////////////
class Vector {
static add (a,b) { return a.map((v,i)=>v+b[i]); }
static sub (a,b) { return a.map((v,i)=>v-b[i]); }
static mul (a,b) { return a.map((v,i)=>v*b[i]); }
static div (a,b) { return a.map((v,i)=>v/b[i]); }
static scale(a,s) { return a.map(v=>v*s); }
static det(m) { return m.length == 1? m[0][0]: m.length == 2 ? m[0][0]*m[1][1]-m[0][1]*m[1][0]: m[0].reduce((r,e,i) => r+(-1)**(i+2)*e*this.det(m.slice(1).map(c => c.filter((_,j) => i != j))),0); }
static angle(a) { return Math.PI - Math.atan2(a[1], -a[0]); } //compatible with turtletoy heading
static rot2d(angle) { return [[Math.cos(angle), -Math.sin(angle)], [Math.sin(angle), Math.cos(angle)]]; }
static rot3d(yaw,pitch,roll) { return [[Math.cos(yaw)*Math.cos(pitch), Math.cos(yaw)*Math.sin(pitch)*Math.sin(roll)-Math.sin(yaw)*Math.cos(roll), Math.cos(yaw)*Math.sin(pitch)*Math.cos(roll)+Math.sin(yaw)*Math.sin(roll)],[Math.sin(yaw)*Math.cos(pitch), Math.sin(yaw)*Math.sin(pitch)*Math.sin(roll)+Math.cos(yaw)*Math.cos(roll), Math.sin(yaw)*Math.sin(pitch)*Math.cos(roll)-Math.cos(yaw)*Math.sin(roll)],[-Math.sin(pitch), Math.cos(pitch)*Math.sin(roll), Math.cos(pitch)*Math.cos(roll)]]; }
static trans(matrix,a) { return a.map((v,i) => a.reduce((acc, cur, ci) => acc + cur * matrix[ci][i], 0)); }
//Mirror vector a in a ray through [0,0] with direction mirror
static mirror2d(a,mirror) { return [Math.atan2(...mirror)].map(angle => this.trans(this.rot2d(angle), this.mul([-1,1], this.trans(this.rot2d(-angle), a)))).pop(); }
static equals(a,b) { return !a.some((e, i) => e != b[i]); }
static approx(a,b,p) { return this.len(this.sub(a,b)) < (p === undefined? .001: p); }
static norm (a) { return this.scale(a,1/this.len(a)); }
static len (a) { return Math.hypot(...a); }
static lenSq (a) { return a.reduce((a,c)=>a+c**2,0); }
static lerp (a,b,t) { return a.map((v, i) => v*(1-t) + b[i]*t); }
static dist (a,b) { return Math.hypot(...this.sub(a,b)); }
static dot (a,b) { return a.reduce((a,c,i) => a+c*b[i], 0); }
static cross(...ab) { return ab[0].map((e, i) => ab.map(v => v.filter((ee, ii) => ii != i))).map((m,i) => (i%2==0?-1:1)*this.det(m)); }
static clamp(a,min,max) { return a.map((e,i) => Math.min(Math.max(e, min[i]), max[i])) };
static rotateClamp(a,min,max) { return a.map((e,i) => {const d = max[i]-min[i];if(d == 0) return min[i];while(e < min[i]) { e+=d; }while(e > max[i]) { e-=d; }return e;});
}
}
this.V = Vector;
}
// Turtlelib Jurgen Vector Math v 4 - end
// Turtlelib Jurgen Intersection v 4 - start - {"id":"c5f8fa95ed","package":"Jurgen","name":"Intersection","version":"4"}
function turtlelib_ns_c5f8fa95ed_Jurgen_Intersection() {
///////////////////////////////////////////////////////////////
// Intersection functions - Created by Jurgen Westerhof 2024 //
///////////////////////////////////////////////////////////////
class Intersection {
//a-start, a-direction, b-start, b-direction
//returns false on no intersection or [[intersection:x,y], scalar a-direction, scalar b-direction
static info(as, ad, bs, bd) { const d = V.sub(bs, as), det = -V.det([bd, ad]); if(det === 0) return false; const res = [V.det([d, bd]) / det, V.det([d, ad]) / det]; return [V.add(as, V.scale(ad, res[0])), ...res]; }
static ray(a, b, c, d) { return this.info(a, b, c, d); }
static segment(a,b,c,d, inclusiveStart = true, inclusiveEnd = true) { const i = this.info(a, V.sub(b, a), c, V.sub(d, c)); return i === false? false: ( (inclusiveStart? 0<=i[1] && 0<=i[2]: 0<i[1] && 0<i[2]) && (inclusiveEnd? i[1]<=1 && i[2]<=1: i[1]<1 && i[2]<1) )?i[0]:false;}
static tour(tour, segmentStart, segmentDirection) { return tour.map((e, i, a) => [i, this.info(e, V.sub(a[(i+1)%a.length], e), segmentStart, segmentDirection)]).filter(e => e[1] !== false && 0 <= e[1][1] && e[1][1] <= 1).filter(e => 0 <= e[1][2]).map(e => ({position: e[1][0],tourIndex: e[0],tourSegmentPortion: e[1][1],segmentPortion: e[1][2],}));}
static inside(tour, pt) { return tour.map((e,i,a) => this.segment(e, a[(i+1)%a.length], pt, [Number.MAX_SAFE_INTEGER, 0], true, false)).filter(e => e !== false).length % 2 == 1; }
static circles(centerA, radiusA, centerB, radiusB) {const result = {intersect_count: 0,intersect_occurs: true,one_is_in_other: false,are_equal: false,point_1: [null, null],point_2: [null, null],};const dx = centerB[0] - centerA[0];const dy = centerB[1] - centerA[1];const dist = Math.hypot(dy, dx);if (dist > radiusA + radiusB) {result.intersect_occurs = false;}if (dist < Math.abs(radiusA - radiusB) && !N.approx(dist, Math.abs(radiusA - radiusB))) {result.intersect_occurs = false;result.one_is_in_other = true;}if (V.approx(centerA, centerB) && radiusA === radiusB) {result.are_equal = true;}if (result.intersect_occurs) {const centroid = (radiusA**2 - radiusB**2 + dist * dist) / (2.0 * dist);const x2 = centerA[0] + (dx * centroid) / dist;const y2 = centerA[1] + (dy * centroid) / dist;const prec = 10000;const h = (Math.round(radiusA**2 * prec)/prec - Math.round(centroid**2 * prec)/prec)**.5;const rx = -dy * (h / dist);const ry = dx * (h / dist);result.point_1 = [x2 + rx, y2 + ry];result.point_2 = [x2 - rx, y2 - ry];if (result.are_equal) {result.intersect_count = Infinity;} else if (V.equals(result.point_1, result.point_2)) {result.intersect_count = 1;} else {result.intersect_count = 2;}}return result;}
}
this.Intersection = Intersection;
}
// Turtlelib Jurgen Intersection v 4 - end
// Turtlelib Jurgen Path tools v 3 - start - {"id":"2d89bd6d64","package":"Jurgen","name":"Path tools","version":"3"}
function turtlelib_ns_2d89bd6d64_Jurgen_Path_tools() {
///////////////////////////////////////////////////////
// Path functions - Created by Jurgen Westerhof 2024 //
///////////////////////////////////////////////////////
class PathTools {
static bezier(p1, cp1, cp2, p2, steps = null) {steps = (steps === null? Math.max(3, (V.len(V.sub(cp1, p1)) + V.len(V.sub(cp2, cp1)) + V.len(V.sub(p2, cp2))) | 0): steps) - 1; return Array.from({length: steps + 1}).map((v, i, a, f = i/steps) => [[V.lerp(p1, cp1, f),V.lerp(cp1, cp2, f),V.lerp(cp2, p2, f)]].map(v => V.lerp(V.lerp(v[0], v[1], f), V.lerp(v[1], v[2], f), f))[0]);}
// https://stackoverflow.com/questions/18655135/divide-bezier-curve-into-two-equal-halves#18681336
static splitBezier(p1, cp1, cp2, p2, t=.5) {const e = V.lerp(p1, cp1, t);const f = V.lerp(cp1, cp2, t);const g = V.lerp(cp2, p2, t);const h = V.lerp(e, f, t);const j = V.lerp(f, g, t);const k = V.lerp(h, j, t);return [[p1, e, h, k], [k, j, g, p2]];}
static circular(radius,verticeCount,rotation=0) {return Array.from({length: verticeCount}).map((e,i,a,f=i*2*Math.PI/verticeCount+rotation) => [radius*Math.cos(f),radius*Math.sin(f)])}
static circle(r){return this.circular(r,Math.max(12, (r*10)|0));}
static arc(radius, extend = 2 * Math.PI, clockWiseStart = 0, steps = null, includeLast = false) { return [steps == null? (radius*extend+1)|0: steps].map(steps => Array.from({length: steps}).map((v, i, a) => [radius * Math.cos(clockWiseStart + extend*i/(a.length-(includeLast?1:0))), radius * Math.sin(clockWiseStart + extend*i/(a.length-(includeLast?1:0)))])).pop(); }
static draw(turtle, path) {path.forEach((pt, i) => turtle[i==0?'jump':'goto'](pt));}
static drawTour(turtle, path) {this.draw(turtle, path.concat([path[0]]));}
static drawPoint(turtle, pt, r = .1) {this.drawTour(turtle, this.circle(r).map(e => V.add(e, pt)));}
static drawArrow(turtle, s, d, width = 6, length = 3) {turtle.jump(s);const arrowHeadBase = V.add(s,d);turtle.goto(arrowHeadBase);turtle.goto(V.add(arrowHeadBase, V.trans(V.rot2d(-V.angle(d)), [-length, width/2])));turtle.jump(V.add(arrowHeadBase, V.trans(V.rot2d(-V.angle(d)), [-length, -width/2])));turtle.goto(arrowHeadBase);}
static circlesTangents(c1_center, c1_radius, c2_center, c2_radius, internal = false) {let middle_circle = [V.scale(V.sub(c1_center, c2_center), .5)].map(hwp => [V.add(c2_center, hwp), V.len(hwp)]).pop();if(!internal && c1_radius == c2_radius) {let target = V.sub(c2_center, c1_center);let scaledTarget = V.scale(target, c1_radius/V.len(target));let partResult = [V.add(c1_center, V.trans(V.rot2d(Math.PI/2), scaledTarget)),V.add(c1_center, V.trans(V.rot2d(Math.PI/-2), scaledTarget))];return [partResult,partResult.map(pt => V.add(pt, target))]}let swap = !internal && c2_radius > c1_radius;if(swap) {let t = [[...c1_center], c1_radius];c1_center = c2_center;c1_radius = c2_radius;c2_center = t[0];c2_radius = t[1];}let internal_waypoints = Intersection.circles(c1_center, c1_radius + (internal?c2_radius:-c2_radius), ...middle_circle);if(!internal_waypoints.intersect_occurs) return [];const circlePointAtDirection2 = (circle_center, radius, direction) => V.add(circle_center, V.scale(direction, radius/V.len(direction)));const result = [[circlePointAtDirection2(c1_center, c1_radius, V.sub(internal_waypoints.point_1, c1_center)),circlePointAtDirection2(c1_center, c1_radius, V.sub(internal_waypoints.point_2, c1_center))],[circlePointAtDirection2(c2_center, c2_radius, internal?V.sub(c1_center, internal_waypoints.point_1):V.sub(internal_waypoints.point_1, c1_center)),circlePointAtDirection2(c2_center, c2_radius, internal?V.sub(c1_center, internal_waypoints.point_2):V.sub(internal_waypoints.point_2, c1_center))]];return swap? [[result[1][1],result[1][0]],[result[0][1],result[0][0]]]: result;}
static vectors(path) {return Array.from({length: path.length - 1}).map((e, i) => V.sub(path[i+1], path[i]));}
static path(vectors) {return vectors.reduce((a,c) => a.length==0?[c]:[...a, V.add(c, a[a.length-1])], []);}
static redistributeLinear(path, length = .5) {const p = path.map(pt => [...pt]);const result = [[...p[0]]];let pointer = 1;doneAll: while(pointer < p.length) {let l = length;while(pointer < p.length) {const distance = V.len(V.sub(p[pointer], p[pointer - 1]));if(distance < l) {l-=distance;pointer++;continue;}if(distance == l) {if(pointer < p.length - 1) result.push([...p[pointer]]);pointer++;break doneAll;}if(l < distance) {const newPoint = V.lerp(p[pointer-1], p[pointer], l/distance);if(pointer < p.length - 1) result.push([...newPoint]);p[pointer - 1] = newPoint;break;}}}result.push(p.pop());return result;}
static length(path) { return this.lengths(path).reduce((c, a) => a + c, 0); }
static lengths(path) { return path.map((e, i, a) => V.len(V.sub(e, a[(i+1)%a.length]))).filter((e, i, a) => i < a.length - 1); }
static intersectInfoRay(path, origin, direction) {const vectors = this.vectors(path);const ri = vectors.map((e, i) => [i, Intersection.info(origin, direction, path[i], e)]).filter(e => 0 <= e[1][2] && e[1][2] <= 1 && 0 < e[1][1]).sort(e => e[1][1]);if(ri.length == 0) return false;const hit = ri[0];const lengths = this.lengths(path);const length = lengths.reduce((a, c) => a + c, 0);let l = 0;for(let i = 0; i < hit[0]; i++) {l += lengths[i];}return [hit[1][0], (l + (lengths[hit[0]] * hit[1][2])) / length, hit[1][1]];}
static lerp(path, part) {if(part < 0 || 1 < part) throw new Error('Range of part is 0 to 1, got ' + path);const lengths = this.lengths(path);const length = lengths.reduce((a, c) => a + c, 0);let l = length * part;for(let i = 0; i < lengths.length; i++) {if(lengths[i] < l) {l-=lengths[i];continue;}return V.lerp(path[i], path[i+1], l / V.len(V.sub(path[i+1], path[i])));}return [...path[path.length - 1]];}
static boundingBox(path) { return path.reduce((a, c) => [[Math.min(c[0], a[0][0]), Math.min(c[1], a[0][1])],[Math.max(c[0], a[1][0]), Math.max(c[1], a[1][1])]], [path[0], path[0]]); }
}
this.PT = PathTools;
}
// Turtlelib Jurgen Path tools v 3 - end
// Turtlelib Jurgen Polygons v 1 - start - {"id":"80dcba45dd","package":"Jurgen","name":"Polygons","version":"1"}
function turtlelib_ns_80dcba45dd_Jurgen_Polygons() {
////////////////////////////////////////////////////////////////
// Polygon Clipping utility code - Created by Reinder Nijhoff 2019
// (Polygon binning by Lionel Lemarie 2021) https://turtletoy.net/turtle/95f33bd383
// (Delegated Hatching by Jurgen Westerhof 2024) https://turtletoy.net/turtle/d068ad6040
// (Deferred Polygon Drawing by Jurgen Westerhof 2024) https://turtletoy.net/turtle/6f3d2bc0b5
// https://turtletoy.net/turtle/a5befa1f8d
//
// const polygons = new Polygons();
// const p = polygons.create();
// polygons.draw(turtle, p);
// polygons.list();
// polygons.startDeferSession();
// polygons.stopDeferring();
// polygons.finalizeDeferSession(turtle);
//
// p.addPoints(...[[x,y],]);
// p.addSegments(...[[x,y],]);
// p.addOutline();
// p.addHatching(angle, distance); OR p.addHatching(HatchObject); where HatchObject has a method 'hatch(PolygonClass, thisPolygonInstance)'
// p.inside([x,y]);
// p.boolean(polygon, diff = true);
// p.segment_intersect([x,y], [x,y], [x,y], [x,y]);
////////////////////////////////////////////////////////////////
function Polygons(){const t=[],s=25,e=Array.from({length:s**2},t=>[]),n=class{constructor(){this.cp=[],this.dp=[],this.aabb=[]}addPoints(...t){let s=1e5,e=-1e5,n=1e5,h=-1e5;(this.cp=[...this.cp,...t]).forEach(t=>{s=Math.min(s,t[0]),e=Math.max(e,t[0]),n=Math.min(n,t[1]),h=Math.max(h,t[1])}),this.aabb=[s,n,e,h]}addSegments(...t){t.forEach(t=>this.dp.push(t))}addOutline(){for(let t=0,s=this.cp.length;t<s;t++)this.dp.push(this.cp[t],this.cp[(t+1)%s])}draw(t){for(let s=0,e=this.dp.length;s<e;s+=2)t.jump(this.dp[s]),t.goto(this.dp[s+1])}addHatching(t, s) {if(typeof t == 'object') return t.hatch(n, this);const e=new n;e.cp.push([-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]);const h=Math.sin(t)*s,o=Math.cos(t)*s,a=200*Math.sin(t),i=200*Math.cos(t);for(let t=.5;t<150/s;t++) {e.dp.push([h*t+i,o*t-a],[h*t-i,o*t+a]);e.dp.push([-h*t+i,-o*t-a],[-h*t-i,-o*t+a]);}e.boolean(this,!1);this.dp=[...this.dp,...e.dp]}inside(t){let s=0;for(let e=0,n=this.cp.length;e<n;e++)this.segment_intersect(t,[.1,-1e3],this.cp[e],this.cp[(e+1)%n])&&s++;return 1&s}boolean(t,s=!0){const e=[];for(let n=0,h=this.dp.length;n<h;n+=2){const h=this.dp[n],o=this.dp[n+1],a=[];for(let s=0,e=t.cp.length;s<e;s++){const n=this.segment_intersect(h,o,t.cp[s],t.cp[(s+1)%e]);!1!==n&&a.push(n)}if(0===a.length)s===!t.inside(h)&&e.push(h,o);else{a.push(h,o);const n=o[0]-h[0],i=o[1]-h[1];a.sort((t,s)=>(t[0]-h[0])*n+(t[1]-h[1])*i-(s[0]-h[0])*n-(s[1]-h[1])*i);for(let n=0;n<a.length-1;n++)(a[n][0]-a[n+1][0])**2+(a[n][1]-a[n+1][1])**2>=.001&&s===!t.inside([(a[n][0]+a[n+1][0])/2,(a[n][1]+a[n+1][1])/2])&&e.push(a[n],a[n+1])}}return(this.dp=e).length>0}segment_intersect(t,s,e,n){const h=(n[1]-e[1])*(s[0]-t[0])-(n[0]-e[0])*(s[1]-t[1]);if(0===h)return!1;const o=((n[0]-e[0])*(t[1]-e[1])-(n[1]-e[1])*(t[0]-e[0]))/h,a=((s[0]-t[0])*(t[1]-e[1])-(s[1]-t[1])*(t[0]-e[0]))/h;return o>=0&&o<=1&&a>=0&&a<=1&&[t[0]+o*(s[0]-t[0]),t[1]+o*(s[1]-t[1])]}};const y=function(n,j=[]){const h={},o=200/s;for(var a=0;a<s;a++){const c=a*o-100,r=[0,c,200,c+o];if(!(n[3]<r[1]||n[1]>r[3]))for(var i=0;i<s;i++){const c=i*o-100;r[0]=c,r[2]=c+o,n[0]>r[2]||n[2]<r[0]||e[i+a*s].forEach(s=>{const e=t[s];n[3]<e.aabb[1]||n[1]>e.aabb[3]||n[0]>e.aabb[2]||n[2]<e.aabb[0]||j.includes(s)||(h[s]=1)})}}return Array.from(Object.keys(h),s=>t[s])};return{list:()=>t,create:()=>new n,draw:(n,h,o=!0)=>{rpl=y(h.aabb, this.dei === undefined? []: Array.from({length: t.length - this.dei}).map((e, i) => this.dsi + i));for(let t=0;t<rpl.length&&h.boolean(rpl[t]);t++);const td=n.isdown();if(this.dsi!==undefined&&this.dei===undefined)n.pu();h.draw(n),o&&function(n){t.push(n);const h=t.length-1,o=200/s;e.forEach((t,e)=>{const a=e%s*o-100,i=(e/s|0)*o-100,c=[a,i,a+o,i+o];c[3]<n.aabb[1]||c[1]>n.aabb[3]||c[0]>n.aabb[2]||c[2]<n.aabb[0]||t.push(h)})}(h);if(td)n.pd();},startDeferSession:()=>{if(this.dei!==undefined)throw new Error('Finalize deferring before starting new session');this.dsi=t.length;},stopDeferring:()=>{if(this.dsi === undefined)throw new Error('Start deferring before stopping');this.dei=t.length;},finalizeDeferSession:(n)=>{if(this.dei===undefined)throw new Error('Stop deferring before finalizing');for(let i=this.dsi;i<this.dei;i++) {rpl = y(t[i].aabb,Array.from({length:this.dei-this.dsi+1}).map((e,j)=>i+j));for(let j=0;j<rpl.length&&t[i].boolean(rpl[j]);j++);t[i].draw(n);}this.dsi=undefined;this.dei=undefined;}}}
this.Polygons = Polygons;
}
// Turtlelib Jurgen Polygons v 1 - end
// Turtlelib Jurgen Randomness v 2 - start - {"id":"13b81fd40e","package":"Jurgen","name":"Randomness","version":"2"}
function turtlelib_ns_13b81fd40e_Jurgen_Randomness() {
///////////////////////////////////////////////////////////////
// Pseudorandom functions - Created by Jurgen Westerhof 2024 //
///////////////////////////////////////////////////////////////
class Random {
static #apply(seed) {
// Seedable random number generator by David Bau: http://davidbau.com/archives/2010/01/30/random_seeds_coded_hints_and_quintillions.html
!function(a,b,c,d,e,f,g,h,i){function j(a){var b,c=a.length,e=this,f=0,g=e.i=e.j=0,h=e.S=[];for(c||(a=[c++]);d>f;)h[f]=f++;for(f=0;d>f;f++)h[f]=h[g=s&g+a[f%c]+(b=h[f])],h[g]=b;(e.g=function(a){for(var b,c=0,f=e.i,g=e.j,h=e.S;a--;)b=h[f=s&f+1],c=c*d+h[s&(h[f]=h[g=s&g+b])+(h[g]=b)];return e.i=f,e.j=g,c})(d)}function k(a,b){var c,d=[],e=typeof a;if(b&&"object"==e)for(c in a)try{d.push(k(a[c],b-1))}catch(f){}return d.length?d:"string"==e?a:a+"\0"}function l(a,b){for(var c,d=a+"",e=0;e<d.length;)b[s&e]=s&(c^=19*b[s&e])+d.charCodeAt(e++);return n(b)}function m(c){try{return o?n(o.randomBytes(d)):(a.crypto.getRandomValues(c=new Uint8Array(d)),n(c))}catch(e){return[+new Date,a,(c=a.navigator)&&c.plugins,a.screen,n(b)]}}function n(a){return String.fromCharCode.apply(0,a)}var o,p=c.pow(d,e),q=c.pow(2,f),r=2*q,s=d-1,t=c["seed"+i]=function(a,f,g){var h=[];f=1==f?{entropy:!0}:f||{};var o=l(k(f.entropy?[a,n(b)]:null==a?m():a,3),h),s=new j(h);return l(n(s.S),b),(f.pass||g||function(a,b,d){return d?(c[i]=a,b):a})(function(){for(var a=s.g(e),b=p,c=0;q>a;)a=(a+c)*d,b*=d,c=s.g(1);for(;a>=r;)a/=2,b/=2,c>>>=1;return(a+c)/b},o,"global"in f?f.global:this==c)};if(l(c[i](),b),g&&g.exports){g.exports=t;try{o=require("crypto")}catch(u){}}else h&&h.amd&&h(function(){return t})}(this,[],Math,256,6,52,"object"==typeof module&&module,"function"==typeof define&&define,"random");
Math.seedrandom(seed);
}
static seedRandom() { this.#apply(new Date().getMilliseconds()); }
static seedDaily() { this.#apply(new Date().toDateString()); }
static seed(seed) { this.#apply(seed); }
static getInt(min, max) { if(max == undefined) {max = min + 1; min = 0; } const [mi, ma] = [Math.min(min, max), Math.max(min, max)]; return (mi + Math.random() * (ma - mi)) | 0;}
static get(min, max) {if(min == undefined) {return Math.random();}if(max == undefined) {max = min;min = 0;}const [mi, ma] = [Math.min(min, max), Math.max(min, max)];return mi + Math.random() * (ma - mi);}
static fraction(whole) { return this.get(0, whole); }
static getAngle(l = 1) { return l * this.get(0, 2*Math.PI); }
// Standard Normal variate using Box-Muller transform.
static getGaussian(mean=.5, stdev=.1) {const u = 1 - this.get(); /* Converting [0,1) to (0,1] */const v = this.get();const z = ( -2.0 * Math.log( u ) )**.5 * Math.cos( 2.0 * Math.PI * v );/* Transform to the desired mean and standard deviation: */return z * stdev + mean;}
static skew(value, skew = 0) { /*skew values (from 0 to 1) by a skew from -1 to 1, respectively right and left skewed (resp more values to left or to right), 0 is not skewed*/return (skew < 0)? value - (this.skew(1-value, -skew) - (1-value)): Math.pow(value, 1-Math.abs(skew));}
static getNormalDistributed(skew = 0) { /*skew values (from 0 to 1) by a skew from -1 to 1, respectively right and left skewed (resp more values to left or to right), 0 is not skewed*/let v = -1;while(v < 0 || 1 <= v) { v = this.getGaussian(.5, .1) };return this.skew(v, skew);}
}
this.R = Random;
}
// Turtlelib Jurgen Randomness v 2 - end