From a003501058a951e512613a4ad640ee42d19a53b2 Mon Sep 17 00:00:00 2001 From: Pedro Santayana Date: Thu, 2 Oct 2025 19:51:48 -0300 Subject: [PATCH 1/6] wip3 --- app/games/Game.tsx | 2 +- app/games/flappy-bird-js/Game.tsx | 308 ++++++++++++++++++++++++++++++ app/routes.ts | 5 +- app/routes/Home.tsx | 2 +- tsconfig.json | 4 +- 5 files changed, 315 insertions(+), 6 deletions(-) create mode 100644 app/games/flappy-bird-js/Game.tsx diff --git a/app/games/Game.tsx b/app/games/Game.tsx index 4b5837d..eb5e10c 100644 --- a/app/games/Game.tsx +++ b/app/games/Game.tsx @@ -92,7 +92,7 @@ export default function Game() { }); // Game Loop - app.ticker.add(() => { + app.ticker.add((ticker) => { Matter.Engine.update(engine, 1000 / 60); // Sync Matter bodies to Pixi graphics diff --git a/app/games/flappy-bird-js/Game.tsx b/app/games/flappy-bird-js/Game.tsx new file mode 100644 index 0000000..cb78bdb --- /dev/null +++ b/app/games/flappy-bird-js/Game.tsx @@ -0,0 +1,308 @@ +"use client"; +import { + Bodies, + Body, + Composite, + Engine, + Events, +// IEventCollision, +// IEventTimestamped, + Render, + Runner, +} from "matter-js"; + + +import { useEffect, useRef } from "react"; +import * as PIXI from "pixi.js"; +import Matter from "matter-js"; + +export default function Game() { + const containerRef = useRef(null); + + useEffect(() => { +interface NetInput { + pipeDistance: number; + pipeY: number; + birdVelocity: number; + birdY: number; + weights: number[]; +} + +interface Individual { + body: Body; + genes: number[]; + time: number; + isAlive: boolean; +} + +const TRAINNING_TIME = 30 * 1000; +const POPULATION_SIZE = 1000; +const SCALE = 1; + +const engine = Engine.create(); +const render = Render.create({ + element: document.body, + engine: engine, + options: { + width: 500, + height: 500, + }, +}); + +const playerCategory = 0x0001; +const obsCategory = 0x0002; + +const bot = Bodies.rectangle(50, 50, 50, 400, { + inertia: Infinity, + friction: 0, + collisionFilter: { category: obsCategory }, + label: "obstacle", +}); + +const top = Bodies.rectangle(50, 50, 50, 400, { + inertia: Infinity, + friction: 0, + label: "obstacle", + collisionFilter: { category: obsCategory }, +}); + +engine.gravity.scale = 0; + +Composite.add(engine.world, [bot, top]); + +Render.run(render); + +const runner = Runner.create(); +Runner.run(runner, engine); + +reset(); + +let population: Individual[] = []; + +Array.from({ length: POPULATION_SIZE }).forEach((_) => createIndividual()); + +let cycles = 1; +let points = 0; +let randomHeight = 0; + +Events.on(engine, "beforeUpdate", loop); +Events.on(engine, "collisionStart", collisionHandler); + +document.getElementById("timeScale")!.onchange = (event) => { + event.preventDefault(); + const input: HTMLInputElement = document.getElementById("timeScale")! as HTMLInputElement; + + engine.timing.timeScale = 10 * parseInt(input.value) / 100; +} + + +function collisionHandler(event: IEventCollision) { + event.pairs.forEach((pair) => { + const body = pair.bodyB; + const individual = population.find( + (individual) => individual.body === body + ); + + if (individual) removeIndividual(individual); + }); +} + +function loop(event: IEventTimestamped) { + population.forEach((individual) => { + Body.applyForce(individual.body, individual.body.position, { + x: 0, + y: individual.body.mass * 0.001 * SCALE, + }); + }); + + Body.setVelocity(top, { x: -2 * SCALE, y: 0 }); + Body.setVelocity(bot, { x: -2 * SCALE, y: 0 }); + + //Body.setPosition(top, { ...top.position, y: }) + + if (top.position.x < 50) { + reset(); + + document.getElementById("points")!.innerText = (++points).toString(); + } + + population.filter(individual => individual.isAlive).forEach(individual => { + if (individual.body.position.y > 500 || individual.body.position.y < 0) { + removeIndividual(individual) + } + }) + + population + .filter(individual => individual.isAlive) + .forEach(individual => individual.time = event.timestamp) + + population.forEach((individual) => { + const shouldJump = individualShouldJump(individual); + + if (shouldJump) jump(individual.body); + }); + + const isAllDead = population.every(individual => !individual.isAlive); + + if (isAllDead) { + //ciclamos + console.log("ciclo") + points = 0; + document.getElementById("points")!.innerText = (points).toString(); + cycles++; + reset(); + + // limpar: todo: tem jeito melhor + population + .filter((individual) => individual.isAlive) + .forEach((individual) => removeIndividual(individual)); + + population = getNewPopulation(); + } +} + +function getRandomInt(min: number, max: number) { + min = Math.ceil(min); // Ensure min is an integer + max = Math.floor(max); // Ensure max is an integer + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function getNewPopulation() { + const sorted = population.toSorted((a, b) => getFitness(b) - getFitness(a)); + + const fittests = Math.floor(POPULATION_SIZE * 0.1); + + const newPopulation = []; + + Array.from({length: fittests}).forEach((_, index) => newPopulation.push(createIndividual(sorted[index].genes))); + + for (let i = 0; i < POPULATION_SIZE - fittests; i++) { + const r1 = getRandomInt(0, Math.floor(POPULATION_SIZE * 0.5)); + const ind1 = sorted[r1]; + + const r2 = getRandomInt(0, Math.floor(POPULATION_SIZE * 0.5)); + const ind2 = sorted[r2]; + + //mate + const newGenes = mate(ind1, ind2); + + newPopulation.push(createIndividual(newGenes)); + } + + return newPopulation; +} + +function mate(individual1: Individual, individual2: Individual): number[] { + const newGenes = []; + + for (let i = 0; i < 4; i++) { + let p = Math.random(); + if (p < 0.45) newGenes.push(individual1.genes[i]); + else if (p < 0.9) newGenes.push(individual2.genes[i]); + else newGenes.push(getRandomArbitrary(-1, 1)); + } + + return newGenes; +} + +function reset() { + const rh = getRandomInt(125, 375); + + Body.setPosition(top, { y: rh + 300, x: 525 }); + Body.setPosition(bot, { y: rh - 300, x: 525 }); + + Body.setVelocity(top, { x: 0, y: 0 }); + Body.setVelocity(bot, { x: 0, y: 0 }); +} + +function jump(body: Body) { + Body.setVelocity(body, { x: body.velocity.x, y: -4 * SCALE }); +} + +function getRandomArbitrary(min: number, max: number) { + return Math.random() * (max - min) + min; +} + +function individualShouldJump(individual: Individual) { + const pipeDistance = top.position.x; + const pipeY = top.position.y + 300; + const birdVelocity = individual.body.velocity.y; + const birdY = individual.body.position.y; + + if (population.findIndex(indi => indi === individual) === 0) { + document.getElementById("pipeDistance")!.innerText = pipeDistance.toString(); + document.getElementById("pipeY")!.innerText = pipeY.toString(); + document.getElementById("birdVelocity")!.innerText = birdVelocity.toString(); + document.getElementById("birdY")!.innerText = birdY.toString(); + document.getElementById("time")!.innerText = individual.time.toString(); + } + + const shouldJump = calculateJump({ + pipeDistance, + pipeY, + birdVelocity, + birdY, + weights: individual.genes, + }); + + return shouldJump; +} + +function calculateJump({ + pipeDistance, + pipeY, + birdVelocity, + birdY, + weights, +}: NetInput) { + const inputs = [pipeDistance, pipeY, birdVelocity, birdY]; + + const sum = inputs.reduce( + (acc, input, index) => acc + input * weights[index], + 0 + ); + + return sum >= 0; +} + +function createIndividual(genes1?: number[]): Individual { + const body = Bodies.rectangle(50, 50, 50, 50, { + inertia: Infinity, + collisionFilter: { category: playerCategory, group: -1 }, + label: "individual", + }); + + Composite.add(engine.world, [body]); + + const genes = []; + + genes.push(getRandomArbitrary(-1, 1)); + genes.push(getRandomArbitrary(-1, 1)); + genes.push(getRandomArbitrary(-1, 1)); + genes.push(getRandomArbitrary(-1, 1)); + + const individual = { body, genes: genes1 || genes, time: 0, isAlive: true }; + + population.push(individual); + + return individual; +} + +function getFitness(individual: Individual) { + return individual.time - TRAINNING_TIME; +} + +function removeIndividual(individual: Individual) { + individual.isAlive = false; + + Composite.remove(engine.world, [individual.body]); +} + + + return () => { + // app.destroy(true, true); + }; + }, []); + + return
; +} diff --git a/app/routes.ts b/app/routes.ts index f0bb28f..3b70edf 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -1,3 +1,4 @@ -import { type RouteConfig, index } from "@react-router/dev/routes"; +import { type RouteConfig, index, route } from "@react-router/dev/routes"; +import Game from "./games/Game"; -export default [index("routes/Home.tsx")] satisfies RouteConfig; +export default [index("routes/Home.tsx"), route("/games/flappy", "games/Game.tsx")] satisfies RouteConfig; diff --git a/app/routes/Home.tsx b/app/routes/Home.tsx index a5bf995..469d56a 100644 --- a/app/routes/Home.tsx +++ b/app/routes/Home.tsx @@ -1,4 +1,4 @@ - import type { Route } from "./+types/Home"; +import type { Route } from "./+types/Home"; import Game from "~/games/Game"; export function meta({}: Route.MetaArgs) { diff --git a/tsconfig.json b/tsconfig.json index dc391a4..f34b4a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,9 +6,9 @@ ".react-router/types/**/*" ], "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], + "lib": ["DOM", "DOM.Iterable", "ES2023"], "types": ["node", "vite/client"], - "target": "ES2022", + "target": "ES2023", "module": "ES2022", "moduleResolution": "bundler", "jsx": "react-jsx", From 202db0d76224aa411606641d847d837d84b30689 Mon Sep 17 00:00:00 2001 From: Pedro Santayana Date: Wed, 8 Oct 2025 17:43:58 -0300 Subject: [PATCH 2/6] wip --- app/games/flappy-bird-js/Game.tsx | 474 +++++++++--------- .../flappy-bird-js/assets/background-day.png | Bin 0 -> 7026 bytes app/games/flappy-bird-js/assets/base.png | Bin 0 -> 470 bytes .../flappy-bird-js/assets/pipe-green.png | Bin 0 -> 2527 bytes .../assets/yellowbird-downflap.png | Bin 0 -> 426 bytes app/routes.ts | 2 +- 6 files changed, 247 insertions(+), 229 deletions(-) create mode 100644 app/games/flappy-bird-js/assets/background-day.png create mode 100644 app/games/flappy-bird-js/assets/base.png create mode 100644 app/games/flappy-bird-js/assets/pipe-green.png create mode 100644 app/games/flappy-bird-js/assets/yellowbird-downflap.png diff --git a/app/games/flappy-bird-js/Game.tsx b/app/games/flappy-bird-js/Game.tsx index cb78bdb..cdbaed2 100644 --- a/app/games/flappy-bird-js/Game.tsx +++ b/app/games/flappy-bird-js/Game.tsx @@ -1,308 +1,326 @@ "use client"; -import { +import Matter from "matter-js"; + +const { Bodies, Body, Composite, Engine, Events, -// IEventCollision, -// IEventTimestamped, Render, Runner, -} from "matter-js"; - +} = Matter; import { useEffect, useRef } from "react"; -import * as PIXI from "pixi.js"; -import Matter from "matter-js"; export default function Game() { - const containerRef = useRef(null); - - useEffect(() => { -interface NetInput { - pipeDistance: number; - pipeY: number; - birdVelocity: number; - birdY: number; - weights: number[]; + const containerRef = useRef(null); + + useEffect(() => { + const f = game(); + + return () => { + f(); + }; + }, []); + + return <> +

pipeDistance:

+

pipeY:

+

birdVelocity:

+

birdY:

+

time:

+

POINTS:

+

timeScale:

+
+ ; } -interface Individual { - body: Body; - genes: number[]; - time: number; - isAlive: boolean; -} -const TRAINNING_TIME = 30 * 1000; -const POPULATION_SIZE = 1000; -const SCALE = 1; +function game() { + interface NetInput { + pipeDistance: number; + pipeY: number; + birdVelocity: number; + birdY: number; + weights: number[]; + } + + interface Individual { + body: Matter.Body; + genes: number[]; + time: number; + isAlive: boolean; + } -const engine = Engine.create(); -const render = Render.create({ - element: document.body, - engine: engine, - options: { - width: 500, - height: 500, - }, -}); + const TRAINNING_TIME = 30 * 1000; + const POPULATION_SIZE = 1000; + const SCALE = 1; + + const engine = Engine.create(); + const render = Render.create({ + element: document.body, + engine: engine, + options: { + width: 500, + height: 500, + }, + }); -const playerCategory = 0x0001; -const obsCategory = 0x0002; + const playerCategory = 0x0001; + const obsCategory = 0x0002; -const bot = Bodies.rectangle(50, 50, 50, 400, { - inertia: Infinity, - friction: 0, - collisionFilter: { category: obsCategory }, - label: "obstacle", -}); + const bot = Bodies.rectangle(50, 50, 50, 400, { + inertia: Infinity, + friction: 0, + collisionFilter: { category: obsCategory }, + label: "obstacle", + }); -const top = Bodies.rectangle(50, 50, 50, 400, { - inertia: Infinity, - friction: 0, - label: "obstacle", - collisionFilter: { category: obsCategory }, -}); + const top = Bodies.rectangle(50, 50, 50, 400, { + inertia: Infinity, + friction: 0, + label: "obstacle", + collisionFilter: { category: obsCategory }, + }); -engine.gravity.scale = 0; + engine.gravity.scale = 0; -Composite.add(engine.world, [bot, top]); + Composite.add(engine.world, [bot, top]); -Render.run(render); + Render.run(render); -const runner = Runner.create(); -Runner.run(runner, engine); + const runner = Runner.create(); + Runner.run(runner, engine); -reset(); + reset(); -let population: Individual[] = []; + let population: Individual[] = []; -Array.from({ length: POPULATION_SIZE }).forEach((_) => createIndividual()); + Array.from({ length: POPULATION_SIZE }).forEach((_) => createIndividual()); -let cycles = 1; -let points = 0; -let randomHeight = 0; + let cycles = 1; + let points = 0; + let randomHeight = 0; -Events.on(engine, "beforeUpdate", loop); -Events.on(engine, "collisionStart", collisionHandler); + Events.on(engine, "beforeUpdate", loop); + Events.on(engine, "collisionStart", collisionHandler); -document.getElementById("timeScale")!.onchange = (event) => { - event.preventDefault(); - const input: HTMLInputElement = document.getElementById("timeScale")! as HTMLInputElement; + document.getElementById("timeScale")!.onchange = (event) => { + event.preventDefault(); + const input: HTMLInputElement = document.getElementById("timeScale")! as HTMLInputElement; - engine.timing.timeScale = 10 * parseInt(input.value) / 100; -} + engine.timing.timeScale = 10 * parseInt(input.value) / 100; + } -function collisionHandler(event: IEventCollision) { - event.pairs.forEach((pair) => { - const body = pair.bodyB; - const individual = population.find( - (individual) => individual.body === body - ); - - if (individual) removeIndividual(individual); - }); -} + function collisionHandler(event: Matter.IEventCollision) { + event.pairs.forEach((pair) => { + const body = pair.bodyB; + const individual = population.find( + (individual) => individual.body === body + ); -function loop(event: IEventTimestamped) { - population.forEach((individual) => { - Body.applyForce(individual.body, individual.body.position, { - x: 0, - y: individual.body.mass * 0.001 * SCALE, + if (individual) removeIndividual(individual); }); - }); + } - Body.setVelocity(top, { x: -2 * SCALE, y: 0 }); - Body.setVelocity(bot, { x: -2 * SCALE, y: 0 }); + function loop(event: Matter.IEventTimestamped) { + population.forEach((individual) => { + Body.applyForce(individual.body, individual.body.position, { + x: 0, + y: individual.body.mass * 0.001 * SCALE, + }); + }); - //Body.setPosition(top, { ...top.position, y: }) + Body.setVelocity(top, { x: -2 * SCALE, y: 0 }); + Body.setVelocity(bot, { x: -2 * SCALE, y: 0 }); - if (top.position.x < 50) { - reset(); - - document.getElementById("points")!.innerText = (++points).toString(); - } - - population.filter(individual => individual.isAlive).forEach(individual => { - if (individual.body.position.y > 500 || individual.body.position.y < 0) { - removeIndividual(individual) + //Body.setPosition(top, { ...top.position, y: }) + + if (top.position.x < 50) { + reset(); + + document.getElementById("points")!.innerText = (++points).toString(); } - }) - - population - .filter(individual => individual.isAlive) - .forEach(individual => individual.time = event.timestamp) - - population.forEach((individual) => { - const shouldJump = individualShouldJump(individual); - - if (shouldJump) jump(individual.body); - }); - - const isAllDead = population.every(individual => !individual.isAlive); - - if (isAllDead) { - //ciclamos - console.log("ciclo") - points = 0; - document.getElementById("points")!.innerText = (points).toString(); - cycles++; - reset(); - - // limpar: todo: tem jeito melhor + + population.filter(individual => individual.isAlive).forEach(individual => { + if (individual.body.position.y > 500 || individual.body.position.y < 0) { + removeIndividual(individual) + } + }) + population - .filter((individual) => individual.isAlive) - .forEach((individual) => removeIndividual(individual)); - - population = getNewPopulation(); + .filter(individual => individual.isAlive) + .forEach(individual => individual.time = event.timestamp) + + population.forEach((individual) => { + const shouldJump = individualShouldJump(individual); + + if (shouldJump) jump(individual.body); + }); + + const isAllDead = population.every(individual => !individual.isAlive); + + if (isAllDead) { + //ciclamos + console.log("ciclo") + points = 0; + document.getElementById("points")!.innerText = (points).toString(); + cycles++; + reset(); + + // limpar: todo: tem jeito melhor + population + .filter((individual) => individual.isAlive) + .forEach((individual) => removeIndividual(individual)); + + population = getNewPopulation(); + } } -} -function getRandomInt(min: number, max: number) { - min = Math.ceil(min); // Ensure min is an integer - max = Math.floor(max); // Ensure max is an integer - return Math.floor(Math.random() * (max - min + 1)) + min; -} + function getRandomInt(min: number, max: number) { + min = Math.ceil(min); // Ensure min is an integer + max = Math.floor(max); // Ensure max is an integer + return Math.floor(Math.random() * (max - min + 1)) + min; + } -function getNewPopulation() { - const sorted = population.toSorted((a, b) => getFitness(b) - getFitness(a)); + function getNewPopulation() { + const sorted = population.toSorted((a, b) => getFitness(b) - getFitness(a)); - const fittests = Math.floor(POPULATION_SIZE * 0.1); + const fittests = Math.floor(POPULATION_SIZE * 0.1); - const newPopulation = []; + const newPopulation = []; - Array.from({length: fittests}).forEach((_, index) => newPopulation.push(createIndividual(sorted[index].genes))); + Array.from({ length: fittests }).forEach((_, index) => newPopulation.push(createIndividual(sorted[index].genes))); - for (let i = 0; i < POPULATION_SIZE - fittests; i++) { - const r1 = getRandomInt(0, Math.floor(POPULATION_SIZE * 0.5)); - const ind1 = sorted[r1]; + for (let i = 0; i < POPULATION_SIZE - fittests; i++) { + const r1 = getRandomInt(0, Math.floor(POPULATION_SIZE * 0.5)); + const ind1 = sorted[r1]; - const r2 = getRandomInt(0, Math.floor(POPULATION_SIZE * 0.5)); - const ind2 = sorted[r2]; + const r2 = getRandomInt(0, Math.floor(POPULATION_SIZE * 0.5)); + const ind2 = sorted[r2]; - //mate - const newGenes = mate(ind1, ind2); + //mate + const newGenes = mate(ind1, ind2); - newPopulation.push(createIndividual(newGenes)); + newPopulation.push(createIndividual(newGenes)); + } + + return newPopulation; } - return newPopulation; -} + function mate(individual1: Individual, individual2: Individual): number[] { + const newGenes = []; -function mate(individual1: Individual, individual2: Individual): number[] { - const newGenes = []; + for (let i = 0; i < 4; i++) { + let p = Math.random(); + if (p < 0.45) newGenes.push(individual1.genes[i]); + else if (p < 0.9) newGenes.push(individual2.genes[i]); + else newGenes.push(getRandomArbitrary(-1, 1)); + } - for (let i = 0; i < 4; i++) { - let p = Math.random(); - if (p < 0.45) newGenes.push(individual1.genes[i]); - else if (p < 0.9) newGenes.push(individual2.genes[i]); - else newGenes.push(getRandomArbitrary(-1, 1)); + return newGenes; } - return newGenes; -} + function reset() { + const rh = getRandomInt(125, 375); -function reset() { - const rh = getRandomInt(125, 375); + Body.setPosition(top, { y: rh + 300, x: 525 }); + Body.setPosition(bot, { y: rh - 300, x: 525 }); - Body.setPosition(top, { y: rh + 300, x: 525 }); - Body.setPosition(bot, { y: rh - 300, x: 525 }); + Body.setVelocity(top, { x: 0, y: 0 }); + Body.setVelocity(bot, { x: 0, y: 0 }); + } - Body.setVelocity(top, { x: 0, y: 0 }); - Body.setVelocity(bot, { x: 0, y: 0 }); -} + function jump(body: Matter.Body) { + Body.setVelocity(body, { x: body.velocity.x, y: -4 * SCALE }); + } -function jump(body: Body) { - Body.setVelocity(body, { x: body.velocity.x, y: -4 * SCALE }); -} + function getRandomArbitrary(min: number, max: number) { + return Math.random() * (max - min) + min; + } -function getRandomArbitrary(min: number, max: number) { - return Math.random() * (max - min) + min; -} + function individualShouldJump(individual: Individual) { + const pipeDistance = top.position.x; + const pipeY = top.position.y + 300; + const birdVelocity = individual.body.velocity.y; + const birdY = individual.body.position.y; + + if (population.findIndex(indi => indi === individual) === 0) { + document.getElementById("pipeDistance")!.innerText = pipeDistance.toString(); + document.getElementById("pipeY")!.innerText = pipeY.toString(); + document.getElementById("birdVelocity")!.innerText = birdVelocity.toString(); + document.getElementById("birdY")!.innerText = birdY.toString(); + document.getElementById("time")!.innerText = individual.time.toString(); + } + + const shouldJump = calculateJump({ + pipeDistance, + pipeY, + birdVelocity, + birdY, + weights: individual.genes, + }); -function individualShouldJump(individual: Individual) { - const pipeDistance = top.position.x; - const pipeY = top.position.y + 300; - const birdVelocity = individual.body.velocity.y; - const birdY = individual.body.position.y; - - if (population.findIndex(indi => indi === individual) === 0) { - document.getElementById("pipeDistance")!.innerText = pipeDistance.toString(); - document.getElementById("pipeY")!.innerText = pipeY.toString(); - document.getElementById("birdVelocity")!.innerText = birdVelocity.toString(); - document.getElementById("birdY")!.innerText = birdY.toString(); - document.getElementById("time")!.innerText = individual.time.toString(); + return shouldJump; } - const shouldJump = calculateJump({ + function calculateJump({ pipeDistance, pipeY, birdVelocity, birdY, - weights: individual.genes, - }); + weights, + }: NetInput) { + const inputs = [pipeDistance, pipeY, birdVelocity, birdY]; - return shouldJump; -} - -function calculateJump({ - pipeDistance, - pipeY, - birdVelocity, - birdY, - weights, -}: NetInput) { - const inputs = [pipeDistance, pipeY, birdVelocity, birdY]; - - const sum = inputs.reduce( - (acc, input, index) => acc + input * weights[index], - 0 - ); - - return sum >= 0; -} + const sum = inputs.reduce( + (acc, input, index) => acc + input * weights[index], + 0 + ); -function createIndividual(genes1?: number[]): Individual { - const body = Bodies.rectangle(50, 50, 50, 50, { - inertia: Infinity, - collisionFilter: { category: playerCategory, group: -1 }, - label: "individual", - }); + return sum >= 0; + } - Composite.add(engine.world, [body]); + function createIndividual(genes1?: number[]): Individual { + const body = Bodies.rectangle(50, 50, 50, 50, { + inertia: Infinity, + collisionFilter: { category: playerCategory, group: -1 }, + label: "individual", + }); - const genes = []; + Composite.add(engine.world, [body]); - genes.push(getRandomArbitrary(-1, 1)); - genes.push(getRandomArbitrary(-1, 1)); - genes.push(getRandomArbitrary(-1, 1)); - genes.push(getRandomArbitrary(-1, 1)); + const genes = []; - const individual = { body, genes: genes1 || genes, time: 0, isAlive: true }; + genes.push(getRandomArbitrary(-1, 1)); + genes.push(getRandomArbitrary(-1, 1)); + genes.push(getRandomArbitrary(-1, 1)); + genes.push(getRandomArbitrary(-1, 1)); - population.push(individual); + const individual = { body, genes: genes1 || genes, time: 0, isAlive: true }; - return individual; -} + population.push(individual); -function getFitness(individual: Individual) { - return individual.time - TRAINNING_TIME; -} - -function removeIndividual(individual: Individual) { - individual.isAlive = false; + return individual; + } - Composite.remove(engine.world, [individual.body]); -} + function getFitness(individual: Individual) { + return individual.time - TRAINNING_TIME; + } + function removeIndividual(individual: Individual) { + individual.isAlive = false; - return () => { - // app.destroy(true, true); - }; - }, []); + Composite.remove(engine.world, [individual.body]); + } - return
; -} + return () => { + Engine.clear(engine); + Render.stop(render); + Runner.stop(runner); + render.canvas.remove(); + render.textures = {}; + } +} \ No newline at end of file diff --git a/app/games/flappy-bird-js/assets/background-day.png b/app/games/flappy-bird-js/assets/background-day.png new file mode 100644 index 0000000000000000000000000000000000000000..afcb5ec982a3de74cb9186f6af3ba329d63a22bb GIT binary patch literal 7026 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 b$3|uR-wMAWen9IL0NxiM9{6v+|M9;7ImZIa literal 0 HcmV?d00001 diff --git a/app/games/flappy-bird-js/assets/base.png b/app/games/flappy-bird-js/assets/base.png new file mode 100644 index 0000000000000000000000000000000000000000..c374f2b34f2d8a75f74641775970314b462cb337 GIT binary patch literal 470 zcmeAS@N?(olHy`uVBq!ia0y~yUG}$vdpD;3|NsAJp`$ub4P%nGy9>KmV#jMBhqJ&VvY3H^?+6GpPSxg< z1`4v5c>21sKW64(U=n`*OLYfOXql&rV@SoVx91!=nHU*Z1GOU#a)oVZ5`81@cw_el z_QdChXJ3}N|8|*uxqK%3FN-Rz*Q7`oU(j#<^uFNA z%(zmZi&aZpBT7;dOH!?pi&B9UgOP!urLKXouAyOwp@o%!k(HsPwt<FVdQ&MBb@03g?$h5!Hn literal 0 HcmV?d00001 diff --git a/app/games/flappy-bird-js/assets/pipe-green.png b/app/games/flappy-bird-js/assets/pipe-green.png new file mode 100644 index 0000000000000000000000000000000000000000..4664401037bdb50a709674c0a23862fa3aa294a4 GIT binary patch literal 2527 zcmZ`)dpy(YAOB*GMLLKYAqhV%YL=WVb;!mj46EF8w>ipfF?Y@85{o%=IVJaEDwkZF za z9fAYu#^16N$nDmDlZKNBkx0H{fq`sN`Hay2ikofQB?Y63uf0s!pnfw%www-EsF z#T@`pxd3oDF!M%-8EBM?yMo8;?d{>unYT^`ch7|nEX9njB}{K8efpj{y^}Puo-iPY zX&Cl@`qs0UR(e;FWL2TDt zc*8KUnCqO&wx@vkg+@U+XZ;wbJyO7Y&HkmBuKDoRso)}xQ(zA0gYW=-Xj-G|l-8-B z#$kVY_2sU4a_eMJ5eIaz3(7s~mw|9kfzNCw&FmzNu2BXAu{r-bn?@8~2+A>--cIhC zBU8#u19J_WXqwatOjeWaBc5yBAc4ydo!F#t_@VWKgi@X>vk`auA&S62xF%`SSeUFv zT>TJXVl%OCDTXr>TH4{lY_bi@GbAz)k2+lXmZIy12vim(EDz-d=2u`!c`kKBMD9%J z?L5##kIF*#FGbf65yKv$n2lgXm+?*7_y$;x#O0IRlED(_i4B@-vUXOZZJ!{9$k3}B zBtGhJ&1%BYD$xCcXl4_RT7f2JAnFH+oEcJShbwo6bS+*TY>K7EE3kKf2+7Xc9x$FC z@%@*0UN%Pq$>J(lkn9V>I)(zk0q(y;Br8!;4EIZiOARdzm}I5YsaIaocC52;$qijI_dY+>uPFN zS5s2X__5jV@>O8x!f%Cy5dQkM!(uHgNF6@q_L8))(#M7DtE%et4Mq9+p1NU1zFr9^ zspj8jM-I=i!vwn<9Med4R8-&uKw6uB=8WZj`Xm^Se_ST=U0zjz8@$W8mwsM3TsZM6 zbwxPY9CtJYrWmm@!{?Z%$66SQLq?5a0~&vg_`1HZwDdOZM^kUUwDg6zi@$7rq`mPe zhDyF(P*%Zo-)N~DRlaw<>h zfzzt0sLABOw#LOUNej6`UZ(;? zLzLgs8@JtOs13u*zk+^v-{&CXrp>f=ew-%?&zTguBb0`Vp6Qkyq+)EP@L%H&AvwocYc)5()sd|MNv8?16Tm3JXwXS`Ga8^}YS+T{haZPy#lL5eUp?rBYp=U4!que5CRy9$jN+p@CF{!POl` z@(A+3iSf!nCmhNZe)S2~uv0M`z%T%$s?|w|2UP zpM0kwfURtA!oq75S9GY$6*bO9y2@;Bbrm*b$SUAj#?>1rMSs{3*~N2Sm-in9D(&dp zkMHLdhq&w$mi2d6M?YHj88CZuELD3qH|MoFzoUk-bF4Te;3-y@AX?&I7fwx?)l$=R zZt37NbV?@ZUI!f=Eo7a-j!Pzg{;l3$f!EGD zW<^Hz`e52x$#ieP*2wUnzH+x_&+z>8*@bB?^mXN@?RIhX>b`s~oKzE;&)_qEIA5To~k)_UG zg(BNua0kqB#&bx>6YUN5RJ)|Oa-iOjj=i-wzpjwT8#7jIbYN_Ykc<}r7_T{j~Jn*qbMU_s_drxf##oOAm92yYtWPwdU-0o;} z_4gEk%~SJg)wW2;qU;i{{g%#uC#^BBUk-(NdHix{5M6yl>H^mVHa=v%_t=U~OeZdf z6VqMyjbzQ(EXyugwpKzU{xmw(ELVL}q*;z6>3Ll=Op}oKqx@vs&*SfdQ?Wv?E${=? zBw-v#-X0_$lvjuk2mlhHXQ+!fqpPQf*E@$oAW?dTClLlH1cDDY0k@DU)Otn z{rdG|yYcmBXZ~0)JZRAQ|NsAjlBe20X~vQuzhDN3XE)M7oFs2|7ml`jQ>%a+&H|6f zVg?3oVGw3ym^DWND9B#o>Fdh=n3;!xN%;9M)g3^gEKe855Q)pZ7Xx_@DDbdch_c|` z+Rz#`qc!rtzdq>=sjVsBUE?z}5|%k^4ByAJ!`ka$T!g!ToEOuzDa{u&IhFo(WoYXz zDx9oRkXI(;$Etnt%hpG8VzmpM7VpkfwA$Ns>~Px5;^<#p`}Q%EMVg;vcHSliv{kjl zHKHUXu_Vlzw{7+P2v7+D!wY8#kY85lg&vOJ8UAvZrIGp!Q0 Uh6&}3^*{{_p00i_>zopr0M`A6wg3PC literal 0 HcmV?d00001 diff --git a/app/routes.ts b/app/routes.ts index 3b70edf..3e1239b 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -1,4 +1,4 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes"; import Game from "./games/Game"; -export default [index("routes/Home.tsx"), route("/games/flappy", "games/Game.tsx")] satisfies RouteConfig; +export default [index("routes/Home.tsx"), route("/games/flappy", "games/flappy-bird-js/Game.tsx")] satisfies RouteConfig; From d7de99868e3a249f5af03d1e4611ee811aaf355f Mon Sep 17 00:00:00 2001 From: Pedro Santayana Date: Wed, 8 Oct 2025 18:27:38 -0300 Subject: [PATCH 3/6] wip --- app/games/flappy-bird-js/Game.tsx | 16 ++++++++++------ .../{yellowbird-downflap.png => bird.png} | Bin app/routes.ts | 1 - 3 files changed, 10 insertions(+), 7 deletions(-) rename app/games/flappy-bird-js/assets/{yellowbird-downflap.png => bird.png} (100%) diff --git a/app/games/flappy-bird-js/Game.tsx b/app/games/flappy-bird-js/Game.tsx index cdbaed2..82a60f3 100644 --- a/app/games/flappy-bird-js/Game.tsx +++ b/app/games/flappy-bird-js/Game.tsx @@ -17,11 +17,9 @@ export default function Game() { const containerRef = useRef(null); useEffect(() => { - const f = game(); + const end = start(); - return () => { - f(); - }; + return () => end(); }, []); return <> @@ -36,8 +34,7 @@ export default function Game() { ; } - -function game() { +function start() { interface NetInput { pipeDistance: number; pipeY: number; @@ -288,6 +285,13 @@ function game() { inertia: Infinity, collisionFilter: { category: playerCategory, group: -1 }, label: "individual", + render: { + sprite: { + texture: './assets/bird.png', // Example image URL + xScale: 1, + yScale: 1 + } + } }); Composite.add(engine.world, [body]); diff --git a/app/games/flappy-bird-js/assets/yellowbird-downflap.png b/app/games/flappy-bird-js/assets/bird.png similarity index 100% rename from app/games/flappy-bird-js/assets/yellowbird-downflap.png rename to app/games/flappy-bird-js/assets/bird.png diff --git a/app/routes.ts b/app/routes.ts index 3e1239b..6571c19 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -1,4 +1,3 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes"; -import Game from "./games/Game"; export default [index("routes/Home.tsx"), route("/games/flappy", "games/flappy-bird-js/Game.tsx")] satisfies RouteConfig; From d2e481bba6bc9e44e798cdfa45826789a7dd44aa Mon Sep 17 00:00:00 2001 From: Pedro Santayana Date: Wed, 22 Oct 2025 16:44:17 -0300 Subject: [PATCH 4/6] wip --- app/games/flappy-bird-js/Game.tsx | 430 +++++++++++++++++------------- 1 file changed, 243 insertions(+), 187 deletions(-) diff --git a/app/games/flappy-bird-js/Game.tsx b/app/games/flappy-bird-js/Game.tsx index 82a60f3..a07c3a0 100644 --- a/app/games/flappy-bird-js/Game.tsx +++ b/app/games/flappy-bird-js/Game.tsx @@ -13,318 +13,374 @@ const { import { useEffect, useRef } from "react"; -export default function Game() { +export default function Jogo() { const containerRef = useRef(null); useEffect(() => { - const end = start(); - - return () => end(); - }, []); - - return <> -

pipeDistance:

-

pipeY:

-

birdVelocity:

-

birdY:

-

time:

-

POINTS:

-

timeScale:

-
- ; + const finalizar = iniciar(containerRef!); + + return () => finalizar(); + }, [containerRef]); + + return ( +
+

Flappy Bird

+ +
+
+

Distância do Cano:

+

Y do Cano:

+

Velocidade do Pássaro:

+

Y do Pássaro:

+

Tempo:

+

PONTOS:

+
+ + +
+
+
+
+ Este código implementa um agente treinado para jogar Flappy Bird utilizando um perceptron de uma camada e um algoritmo genético. O agente é representado por indivíduos em uma população, onde cada indivíduo possui um conjunto de genes que define seu comportamento. A cada ciclo, os indivíduos tomam decisões baseadas em entradas do ambiente e são avaliados com base em seu desempenho. + +

+ Estrutura do Código: +
+ O código utiliza a biblioteca Matter.js para simular a física do jogo. Por exemplo, os canos e os pássaros são representados como corpos físicos: + + Esses corpos são adicionados ao mundo da simulação: + + +

+ Decisão do Agente: +
+ Cada indivíduo decide se deve "pular" com base em um perceptron de uma camada. As entradas para o perceptron incluem a distância do cano, a posição do cano, a velocidade do pássaro e a posição do pássaro: + + Se a soma ponderada das entradas for maior ou igual a zero, o agente decide pular. + +

+ Algoritmo Genético: +
+ Após cada ciclo, os indivíduos são avaliados com base em seu tempo de sobrevivência: + + Os indivíduos mais aptos são selecionados para reprodução, e novos indivíduos são criados por cruzamento e mutação: + + +

+ Treinamento e Evolução: +
+ O treinamento ocorre ao longo de múltiplos ciclos. Quando todos os indivíduos morrem, uma nova população é gerada: + + Isso permite que o algoritmo genético refine os genes dos indivíduos ao longo do tempo. + +

+ Interatividade: +
+ O código também permite ajustar a escala de tempo da simulação: + + Além disso, informações como a distância do cano e a posição do pássaro são exibidas na interface para facilitar a análise do comportamento do agente. +
+
+ ); } -function start() { - interface NetInput { - pipeDistance: number; - pipeY: number; - birdVelocity: number; - birdY: number; - weights: number[]; + +function iniciar(containerRef: React.RefObject) { + interface EntradaRede { + distanciaCano: number; + canoY: number; + velocidadePassaro: number; + passaroY: number; + pesos: number[]; } - interface Individual { - body: Matter.Body; + interface Individuo { + corpo: Matter.Body; genes: number[]; - time: number; - isAlive: boolean; + tempo: number; + estaVivo: boolean; } - const TRAINNING_TIME = 30 * 1000; - const POPULATION_SIZE = 1000; - const SCALE = 1; + const TEMPO_TREINAMENTO = 30 * 1000; + const TAMANHO_POPULACAO = 1000; + const ESCALA = 1; - const engine = Engine.create(); - const render = Render.create({ - element: document.body, - engine: engine, + const motor = Engine.create(); + const renderizador = Render.create({ + element: containerRef.current!, + engine: motor, options: { width: 500, height: 500, }, }); - const playerCategory = 0x0001; - const obsCategory = 0x0002; + const categoriaJogador = 0x0001; + const categoriaObstaculo = 0x0002; - const bot = Bodies.rectangle(50, 50, 50, 400, { + const canoInferior = Bodies.rectangle(50, 50, 50, 400, { inertia: Infinity, friction: 0, - collisionFilter: { category: obsCategory }, - label: "obstacle", + collisionFilter: { category: categoriaObstaculo }, + label: "obstaculo", }); - const top = Bodies.rectangle(50, 50, 50, 400, { + const canoSuperior = Bodies.rectangle(50, 50, 50, 400, { inertia: Infinity, friction: 0, - label: "obstacle", - collisionFilter: { category: obsCategory }, + label: "obstaculo", + collisionFilter: { category: categoriaObstaculo }, }); - engine.gravity.scale = 0; + motor.gravity.scale = 0; - Composite.add(engine.world, [bot, top]); + Composite.add(motor.world, [canoInferior, canoSuperior]); - Render.run(render); + Render.run(renderizador); - const runner = Runner.create(); - Runner.run(runner, engine); + const executor = Runner.create(); + Runner.run(executor, motor); - reset(); + reiniciar(); - let population: Individual[] = []; + let populacao: Individuo[] = []; - Array.from({ length: POPULATION_SIZE }).forEach((_) => createIndividual()); + Array.from({ length: TAMANHO_POPULACAO }).forEach((_) => criarIndividuo()); - let cycles = 1; - let points = 0; - let randomHeight = 0; + let ciclos = 1; + let pontos = 0; - Events.on(engine, "beforeUpdate", loop); - Events.on(engine, "collisionStart", collisionHandler); + Events.on(motor, "beforeUpdate", loop); + Events.on(motor, "collisionStart", tratarColisao); - document.getElementById("timeScale")!.onchange = (event) => { - event.preventDefault(); - const input: HTMLInputElement = document.getElementById("timeScale")! as HTMLInputElement; + document.getElementById("escalaTempo")!.onchange = (evento) => { + evento.preventDefault(); + const entrada: HTMLInputElement = document.getElementById("escalaTempo")! as HTMLInputElement; - engine.timing.timeScale = 10 * parseInt(input.value) / 100; + motor.timing.timeScale = 10 * parseInt(entrada.value) / 100; } - - function collisionHandler(event: Matter.IEventCollision) { - event.pairs.forEach((pair) => { - const body = pair.bodyB; - const individual = population.find( - (individual) => individual.body === body + function tratarColisao(evento: Matter.IEventCollision) { + evento.pairs.forEach((par) => { + const corpo = par.bodyB; + const individuo = populacao.find( + (individuo) => individuo.corpo === corpo ); - if (individual) removeIndividual(individual); + if (individuo) removerIndividuo(individuo); }); } - function loop(event: Matter.IEventTimestamped) { - population.forEach((individual) => { - Body.applyForce(individual.body, individual.body.position, { + function loop(evento: Matter.IEventTimestamped) { + populacao.forEach((individuo) => { + Body.applyForce(individuo.corpo, individuo.corpo.position, { x: 0, - y: individual.body.mass * 0.001 * SCALE, + y: individuo.corpo.mass * 0.001 * ESCALA, }); }); - Body.setVelocity(top, { x: -2 * SCALE, y: 0 }); - Body.setVelocity(bot, { x: -2 * SCALE, y: 0 }); - - //Body.setPosition(top, { ...top.position, y: }) + Body.setVelocity(canoSuperior, { x: -2 * ESCALA, y: 0 }); + Body.setVelocity(canoInferior, { x: -2 * ESCALA, y: 0 }); - if (top.position.x < 50) { - reset(); + if (canoSuperior.position.x < 50) { + reiniciar(); - document.getElementById("points")!.innerText = (++points).toString(); + document.getElementById("pontos")!.innerText = (++pontos).toString(); } - population.filter(individual => individual.isAlive).forEach(individual => { - if (individual.body.position.y > 500 || individual.body.position.y < 0) { - removeIndividual(individual) + populacao.filter(individuo => individuo.estaVivo).forEach(individuo => { + if (individuo.corpo.position.y > 500 || individuo.corpo.position.y < 0) { + removerIndividuo(individuo) } }) - population - .filter(individual => individual.isAlive) - .forEach(individual => individual.time = event.timestamp) + populacao + .filter(individuo => individuo.estaVivo) + .forEach(individuo => individuo.tempo = evento.timestamp) - population.forEach((individual) => { - const shouldJump = individualShouldJump(individual); + populacao.forEach((individuo) => { + const devePular = individuoDevePular(individuo); - if (shouldJump) jump(individual.body); + if (devePular) pular(individuo.corpo); }); - const isAllDead = population.every(individual => !individual.isAlive); + const todosMortos = populacao.every(individuo => !individuo.estaVivo); - if (isAllDead) { - //ciclamos + if (todosMortos) { console.log("ciclo") - points = 0; - document.getElementById("points")!.innerText = (points).toString(); - cycles++; - reset(); + pontos = 0; + document.getElementById("pontos")!.innerText = (pontos).toString(); + ciclos++; + reiniciar(); - // limpar: todo: tem jeito melhor - population - .filter((individual) => individual.isAlive) - .forEach((individual) => removeIndividual(individual)); + populacao + .filter((individuo) => individuo.estaVivo) + .forEach((individuo) => removerIndividuo(individuo)); - population = getNewPopulation(); + populacao = novaPopulacao(); } } - function getRandomInt(min: number, max: number) { - min = Math.ceil(min); // Ensure min is an integer - max = Math.floor(max); // Ensure max is an integer - return Math.floor(Math.random() * (max - min + 1)) + min; - } - - function getNewPopulation() { - const sorted = population.toSorted((a, b) => getFitness(b) - getFitness(a)); + function novaPopulacao() { + const ordenados = populacao.toSorted((a, b) => obterFitness(b) - obterFitness(a)); - const fittests = Math.floor(POPULATION_SIZE * 0.1); + const maisAptos = Math.floor(TAMANHO_POPULACAO * 0.1); - const newPopulation = []; + const novaPopulacao = []; - Array.from({ length: fittests }).forEach((_, index) => newPopulation.push(createIndividual(sorted[index].genes))); + Array.from({ length: maisAptos }).forEach((_, index) => novaPopulacao.push(criarIndividuo(ordenados[index].genes))); - for (let i = 0; i < POPULATION_SIZE - fittests; i++) { - const r1 = getRandomInt(0, Math.floor(POPULATION_SIZE * 0.5)); - const ind1 = sorted[r1]; + for (let i = 0; i < TAMANHO_POPULACAO - maisAptos; i++) { + const r1 = obterAleatorio(0, Math.floor(TAMANHO_POPULACAO * 0.5)); + const ind1 = ordenados[r1]; - const r2 = getRandomInt(0, Math.floor(POPULATION_SIZE * 0.5)); - const ind2 = sorted[r2]; + const r2 = obterAleatorio(0, Math.floor(TAMANHO_POPULACAO * 0.5)); + const ind2 = ordenados[r2]; - //mate - const newGenes = mate(ind1, ind2); + const novosGenes = cruzar(ind1, ind2); - newPopulation.push(createIndividual(newGenes)); + novaPopulacao.push(criarIndividuo(novosGenes)); } - return newPopulation; + return novaPopulacao; } - function mate(individual1: Individual, individual2: Individual): number[] { - const newGenes = []; + function cruzar(individuo1: Individuo, individuo2: Individuo): number[] { + const novosGenes = []; for (let i = 0; i < 4; i++) { let p = Math.random(); - if (p < 0.45) newGenes.push(individual1.genes[i]); - else if (p < 0.9) newGenes.push(individual2.genes[i]); - else newGenes.push(getRandomArbitrary(-1, 1)); + if (p < 0.45) novosGenes.push(individuo1.genes[i]); + else if (p < 0.9) novosGenes.push(individuo2.genes[i]); + else novosGenes.push(obterAleatorio(-1, 1)); } - return newGenes; + return novosGenes; } - function reset() { - const rh = getRandomInt(125, 375); + function reiniciar() { + const alturaAleatoria = obterAleatorio(125, 375); - Body.setPosition(top, { y: rh + 300, x: 525 }); - Body.setPosition(bot, { y: rh - 300, x: 525 }); + Body.setPosition(canoSuperior, { y: alturaAleatoria + 300, x: 525 }); + Body.setPosition(canoInferior, { y: alturaAleatoria - 300, x: 525 }); - Body.setVelocity(top, { x: 0, y: 0 }); - Body.setVelocity(bot, { x: 0, y: 0 }); + Body.setVelocity(canoSuperior, { x: 0, y: 0 }); + Body.setVelocity(canoInferior, { x: 0, y: 0 }); } - function jump(body: Matter.Body) { - Body.setVelocity(body, { x: body.velocity.x, y: -4 * SCALE }); + function pular(corpo: Matter.Body) { + Body.setVelocity(corpo, { x: corpo.velocity.x, y: -4 * ESCALA }); } - function getRandomArbitrary(min: number, max: number) { + function obterAleatorio(min: number, max: number) { return Math.random() * (max - min) + min; } - function individualShouldJump(individual: Individual) { - const pipeDistance = top.position.x; - const pipeY = top.position.y + 300; - const birdVelocity = individual.body.velocity.y; - const birdY = individual.body.position.y; - - if (population.findIndex(indi => indi === individual) === 0) { - document.getElementById("pipeDistance")!.innerText = pipeDistance.toString(); - document.getElementById("pipeY")!.innerText = pipeY.toString(); - document.getElementById("birdVelocity")!.innerText = birdVelocity.toString(); - document.getElementById("birdY")!.innerText = birdY.toString(); - document.getElementById("time")!.innerText = individual.time.toString(); + function individuoDevePular(individuo: Individuo) { + const distanciaCano = canoSuperior.position.x; + const canoY = canoSuperior.position.y + 300; + const velocidadePassaro = individuo.corpo.velocity.y; + const passaroY = individuo.corpo.position.y; + + if (populacao.findIndex(indi => indi === individuo) === 0) { + document.getElementById("distanciaCano")!.innerText = distanciaCano.toString(); + document.getElementById("canoY")!.innerText = canoY.toString(); + document.getElementById("velocidadePassaro")!.innerText = velocidadePassaro.toString(); + document.getElementById("passaroY")!.innerText = passaroY.toString(); + document.getElementById("tempo")!.innerText = individuo.tempo.toString(); } - const shouldJump = calculateJump({ - pipeDistance, - pipeY, - birdVelocity, - birdY, - weights: individual.genes, + const devePular = calcularPulo({ + distanciaCano, + canoY, + velocidadePassaro, + passaroY, + pesos: individuo.genes, }); - return shouldJump; + return devePular; } - function calculateJump({ - pipeDistance, - pipeY, - birdVelocity, - birdY, - weights, - }: NetInput) { - const inputs = [pipeDistance, pipeY, birdVelocity, birdY]; - - const sum = inputs.reduce( - (acc, input, index) => acc + input * weights[index], + function calcularPulo({ + distanciaCano, + canoY, + velocidadePassaro, + passaroY, + pesos, + }: EntradaRede) { + const entradas = [distanciaCano, canoY, velocidadePassaro, passaroY]; + + const soma = entradas.reduce( + (acc, entrada, index) => acc + entrada * pesos[index], 0 ); - return sum >= 0; + return soma >= 0; } - function createIndividual(genes1?: number[]): Individual { - const body = Bodies.rectangle(50, 50, 50, 50, { + function criarIndividuo(genes1?: number[]): Individuo { + const corpo = Bodies.rectangle(50, 50, 50, 50, { inertia: Infinity, - collisionFilter: { category: playerCategory, group: -1 }, - label: "individual", + collisionFilter: { category: categoriaJogador, group: -1 }, + label: "individuo", render: { sprite: { - texture: './assets/bird.png', // Example image URL + texture: './assets/passaro.png', xScale: 1, yScale: 1 } } }); - Composite.add(engine.world, [body]); + Composite.add(motor.world, [corpo]); const genes = []; - genes.push(getRandomArbitrary(-1, 1)); - genes.push(getRandomArbitrary(-1, 1)); - genes.push(getRandomArbitrary(-1, 1)); - genes.push(getRandomArbitrary(-1, 1)); + genes.push(obterAleatorio(-1, 1)); + genes.push(obterAleatorio(-1, 1)); + genes.push(obterAleatorio(-1, 1)); + genes.push(obterAleatorio(-1, 1)); - const individual = { body, genes: genes1 || genes, time: 0, isAlive: true }; + const individuo = { corpo, genes: genes1 || genes, tempo: 0, estaVivo: true }; - population.push(individual); + populacao.push(individuo); - return individual; + return individuo; } - function getFitness(individual: Individual) { - return individual.time - TRAINNING_TIME; + function obterFitness(individuo: Individuo) { + return individuo.tempo - TEMPO_TREINAMENTO; } - function removeIndividual(individual: Individual) { - individual.isAlive = false; + function removerIndividuo(individuo: Individuo) { + individuo.estaVivo = false; - Composite.remove(engine.world, [individual.body]); + Composite.remove(motor.world, [individuo.corpo]); } return () => { - Engine.clear(engine); - Render.stop(render); - Runner.stop(runner); - render.canvas.remove(); - render.textures = {}; + Engine.clear(motor); + Render.stop(renderizador); + Runner.stop(executor); + renderizador.canvas.remove(); + renderizador.textures = {}; } } \ No newline at end of file From 0154e708a881312bff0ec94626924c82fc5076b4 Mon Sep 17 00:00:00 2001 From: Pedro Santayana Date: Sun, 26 Oct 2025 15:18:50 -0300 Subject: [PATCH 5/6] wip --- app/app.css | 1 + app/games/flappy-bird-js/Flappy.tsx | 30 ++ app/games/flappy-bird-js/Game.tsx | 386 ---------------- app/games/flappy-bird-js/Page.tsx | 31 ++ .../{pipe-green.png => pipe-green-bottom.png} | Bin .../flappy-bird-js/assets/pipe-green-top.png | Bin 0 -> 4924 bytes app/games/flappy-bird-js/content.md | 80 ++++ app/games/flappy-bird-js/game.ts | 422 ++++++++++++++++++ app/routes.ts | 2 +- package-lock.json | 162 +++++++ package.json | 4 + 11 files changed, 731 insertions(+), 387 deletions(-) create mode 100644 app/games/flappy-bird-js/Flappy.tsx delete mode 100644 app/games/flappy-bird-js/Game.tsx create mode 100644 app/games/flappy-bird-js/Page.tsx rename app/games/flappy-bird-js/assets/{pipe-green.png => pipe-green-bottom.png} (100%) create mode 100644 app/games/flappy-bird-js/assets/pipe-green-top.png create mode 100644 app/games/flappy-bird-js/content.md create mode 100644 app/games/flappy-bird-js/game.ts diff --git a/app/app.css b/app/app.css index 99345d8..4a0eb03 100644 --- a/app/app.css +++ b/app/app.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@plugin "@tailwindcss/typography"; @theme { --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, diff --git a/app/games/flappy-bird-js/Flappy.tsx b/app/games/flappy-bird-js/Flappy.tsx new file mode 100644 index 0000000..bcca195 --- /dev/null +++ b/app/games/flappy-bird-js/Flappy.tsx @@ -0,0 +1,30 @@ +import { useEffect, useRef } from "react"; +import { iniciar } from "./game"; + +export default function FlappyBird() { + const containerRef = useRef(null); + + useEffect(() => { + const finalizar = iniciar(containerRef!); + + return () => finalizar(); + }, [containerRef]); + + return ( +
+
+

Distância do Cano:

+

Y do Cano:

+

Velocidade do Pássaro:

+

Y do Pássaro:

+

Tempo:

+

Pontos:

+
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/games/flappy-bird-js/Game.tsx b/app/games/flappy-bird-js/Game.tsx deleted file mode 100644 index a07c3a0..0000000 --- a/app/games/flappy-bird-js/Game.tsx +++ /dev/null @@ -1,386 +0,0 @@ -"use client"; -import Matter from "matter-js"; - -const { - Bodies, - Body, - Composite, - Engine, - Events, - Render, - Runner, -} = Matter; - -import { useEffect, useRef } from "react"; - -export default function Jogo() { - const containerRef = useRef(null); - - useEffect(() => { - const finalizar = iniciar(containerRef!); - - return () => finalizar(); - }, [containerRef]); - - return ( -
-

Flappy Bird

- -
-
-

Distância do Cano:

-

Y do Cano:

-

Velocidade do Pássaro:

-

Y do Pássaro:

-

Tempo:

-

PONTOS:

-
- - -
-
-
-
- Este código implementa um agente treinado para jogar Flappy Bird utilizando um perceptron de uma camada e um algoritmo genético. O agente é representado por indivíduos em uma população, onde cada indivíduo possui um conjunto de genes que define seu comportamento. A cada ciclo, os indivíduos tomam decisões baseadas em entradas do ambiente e são avaliados com base em seu desempenho. - -

- Estrutura do Código: -
- O código utiliza a biblioteca Matter.js para simular a física do jogo. Por exemplo, os canos e os pássaros são representados como corpos físicos: - - Esses corpos são adicionados ao mundo da simulação: - - -

- Decisão do Agente: -
- Cada indivíduo decide se deve "pular" com base em um perceptron de uma camada. As entradas para o perceptron incluem a distância do cano, a posição do cano, a velocidade do pássaro e a posição do pássaro: - - Se a soma ponderada das entradas for maior ou igual a zero, o agente decide pular. - -

- Algoritmo Genético: -
- Após cada ciclo, os indivíduos são avaliados com base em seu tempo de sobrevivência: - - Os indivíduos mais aptos são selecionados para reprodução, e novos indivíduos são criados por cruzamento e mutação: - - -

- Treinamento e Evolução: -
- O treinamento ocorre ao longo de múltiplos ciclos. Quando todos os indivíduos morrem, uma nova população é gerada: - - Isso permite que o algoritmo genético refine os genes dos indivíduos ao longo do tempo. - -

- Interatividade: -
- O código também permite ajustar a escala de tempo da simulação: - - Além disso, informações como a distância do cano e a posição do pássaro são exibidas na interface para facilitar a análise do comportamento do agente. -
-
- ); -} - - -function iniciar(containerRef: React.RefObject) { - interface EntradaRede { - distanciaCano: number; - canoY: number; - velocidadePassaro: number; - passaroY: number; - pesos: number[]; - } - - interface Individuo { - corpo: Matter.Body; - genes: number[]; - tempo: number; - estaVivo: boolean; - } - - const TEMPO_TREINAMENTO = 30 * 1000; - const TAMANHO_POPULACAO = 1000; - const ESCALA = 1; - - const motor = Engine.create(); - const renderizador = Render.create({ - element: containerRef.current!, - engine: motor, - options: { - width: 500, - height: 500, - }, - }); - - const categoriaJogador = 0x0001; - const categoriaObstaculo = 0x0002; - - const canoInferior = Bodies.rectangle(50, 50, 50, 400, { - inertia: Infinity, - friction: 0, - collisionFilter: { category: categoriaObstaculo }, - label: "obstaculo", - }); - - const canoSuperior = Bodies.rectangle(50, 50, 50, 400, { - inertia: Infinity, - friction: 0, - label: "obstaculo", - collisionFilter: { category: categoriaObstaculo }, - }); - - motor.gravity.scale = 0; - - Composite.add(motor.world, [canoInferior, canoSuperior]); - - Render.run(renderizador); - - const executor = Runner.create(); - Runner.run(executor, motor); - - reiniciar(); - - let populacao: Individuo[] = []; - - Array.from({ length: TAMANHO_POPULACAO }).forEach((_) => criarIndividuo()); - - let ciclos = 1; - let pontos = 0; - - Events.on(motor, "beforeUpdate", loop); - Events.on(motor, "collisionStart", tratarColisao); - - document.getElementById("escalaTempo")!.onchange = (evento) => { - evento.preventDefault(); - const entrada: HTMLInputElement = document.getElementById("escalaTempo")! as HTMLInputElement; - - motor.timing.timeScale = 10 * parseInt(entrada.value) / 100; - } - - function tratarColisao(evento: Matter.IEventCollision) { - evento.pairs.forEach((par) => { - const corpo = par.bodyB; - const individuo = populacao.find( - (individuo) => individuo.corpo === corpo - ); - - if (individuo) removerIndividuo(individuo); - }); - } - - function loop(evento: Matter.IEventTimestamped) { - populacao.forEach((individuo) => { - Body.applyForce(individuo.corpo, individuo.corpo.position, { - x: 0, - y: individuo.corpo.mass * 0.001 * ESCALA, - }); - }); - - Body.setVelocity(canoSuperior, { x: -2 * ESCALA, y: 0 }); - Body.setVelocity(canoInferior, { x: -2 * ESCALA, y: 0 }); - - if (canoSuperior.position.x < 50) { - reiniciar(); - - document.getElementById("pontos")!.innerText = (++pontos).toString(); - } - - populacao.filter(individuo => individuo.estaVivo).forEach(individuo => { - if (individuo.corpo.position.y > 500 || individuo.corpo.position.y < 0) { - removerIndividuo(individuo) - } - }) - - populacao - .filter(individuo => individuo.estaVivo) - .forEach(individuo => individuo.tempo = evento.timestamp) - - populacao.forEach((individuo) => { - const devePular = individuoDevePular(individuo); - - if (devePular) pular(individuo.corpo); - }); - - const todosMortos = populacao.every(individuo => !individuo.estaVivo); - - if (todosMortos) { - console.log("ciclo") - pontos = 0; - document.getElementById("pontos")!.innerText = (pontos).toString(); - ciclos++; - reiniciar(); - - populacao - .filter((individuo) => individuo.estaVivo) - .forEach((individuo) => removerIndividuo(individuo)); - - populacao = novaPopulacao(); - } - } - - function novaPopulacao() { - const ordenados = populacao.toSorted((a, b) => obterFitness(b) - obterFitness(a)); - - const maisAptos = Math.floor(TAMANHO_POPULACAO * 0.1); - - const novaPopulacao = []; - - Array.from({ length: maisAptos }).forEach((_, index) => novaPopulacao.push(criarIndividuo(ordenados[index].genes))); - - for (let i = 0; i < TAMANHO_POPULACAO - maisAptos; i++) { - const r1 = obterAleatorio(0, Math.floor(TAMANHO_POPULACAO * 0.5)); - const ind1 = ordenados[r1]; - - const r2 = obterAleatorio(0, Math.floor(TAMANHO_POPULACAO * 0.5)); - const ind2 = ordenados[r2]; - - const novosGenes = cruzar(ind1, ind2); - - novaPopulacao.push(criarIndividuo(novosGenes)); - } - - return novaPopulacao; - } - - function cruzar(individuo1: Individuo, individuo2: Individuo): number[] { - const novosGenes = []; - - for (let i = 0; i < 4; i++) { - let p = Math.random(); - if (p < 0.45) novosGenes.push(individuo1.genes[i]); - else if (p < 0.9) novosGenes.push(individuo2.genes[i]); - else novosGenes.push(obterAleatorio(-1, 1)); - } - - return novosGenes; - } - - function reiniciar() { - const alturaAleatoria = obterAleatorio(125, 375); - - Body.setPosition(canoSuperior, { y: alturaAleatoria + 300, x: 525 }); - Body.setPosition(canoInferior, { y: alturaAleatoria - 300, x: 525 }); - - Body.setVelocity(canoSuperior, { x: 0, y: 0 }); - Body.setVelocity(canoInferior, { x: 0, y: 0 }); - } - - function pular(corpo: Matter.Body) { - Body.setVelocity(corpo, { x: corpo.velocity.x, y: -4 * ESCALA }); - } - - function obterAleatorio(min: number, max: number) { - return Math.random() * (max - min) + min; - } - - function individuoDevePular(individuo: Individuo) { - const distanciaCano = canoSuperior.position.x; - const canoY = canoSuperior.position.y + 300; - const velocidadePassaro = individuo.corpo.velocity.y; - const passaroY = individuo.corpo.position.y; - - if (populacao.findIndex(indi => indi === individuo) === 0) { - document.getElementById("distanciaCano")!.innerText = distanciaCano.toString(); - document.getElementById("canoY")!.innerText = canoY.toString(); - document.getElementById("velocidadePassaro")!.innerText = velocidadePassaro.toString(); - document.getElementById("passaroY")!.innerText = passaroY.toString(); - document.getElementById("tempo")!.innerText = individuo.tempo.toString(); - } - - const devePular = calcularPulo({ - distanciaCano, - canoY, - velocidadePassaro, - passaroY, - pesos: individuo.genes, - }); - - return devePular; - } - - function calcularPulo({ - distanciaCano, - canoY, - velocidadePassaro, - passaroY, - pesos, - }: EntradaRede) { - const entradas = [distanciaCano, canoY, velocidadePassaro, passaroY]; - - const soma = entradas.reduce( - (acc, entrada, index) => acc + entrada * pesos[index], - 0 - ); - - return soma >= 0; - } - - function criarIndividuo(genes1?: number[]): Individuo { - const corpo = Bodies.rectangle(50, 50, 50, 50, { - inertia: Infinity, - collisionFilter: { category: categoriaJogador, group: -1 }, - label: "individuo", - render: { - sprite: { - texture: './assets/passaro.png', - xScale: 1, - yScale: 1 - } - } - }); - - Composite.add(motor.world, [corpo]); - - const genes = []; - - genes.push(obterAleatorio(-1, 1)); - genes.push(obterAleatorio(-1, 1)); - genes.push(obterAleatorio(-1, 1)); - genes.push(obterAleatorio(-1, 1)); - - const individuo = { corpo, genes: genes1 || genes, tempo: 0, estaVivo: true }; - - populacao.push(individuo); - - return individuo; - } - - function obterFitness(individuo: Individuo) { - return individuo.tempo - TEMPO_TREINAMENTO; - } - - function removerIndividuo(individuo: Individuo) { - individuo.estaVivo = false; - - Composite.remove(motor.world, [individuo.corpo]); - } - - return () => { - Engine.clear(motor); - Render.stop(renderizador); - Runner.stop(executor); - renderizador.canvas.remove(); - renderizador.textures = {}; - } -} \ No newline at end of file diff --git a/app/games/flappy-bird-js/Page.tsx b/app/games/flappy-bird-js/Page.tsx new file mode 100644 index 0000000..566db02 --- /dev/null +++ b/app/games/flappy-bird-js/Page.tsx @@ -0,0 +1,31 @@ +"use client"; +import Markdown from "marked-react"; +import FlappyBird from "./Flappy"; +import Lowlight from "react-lowlight"; +//@ts-ignore +import javascript from 'highlight.js/lib/languages/javascript'; +import 'highlight.js/styles/default.css' +import content from "./content.md?raw"; + +Lowlight.registerLanguage('js', javascript); + +const renderer = { + code(snippet: string, lang: string) { + //@ts-ignore + return ; + }, +}; + +export default function Jogo() { + return ( +
+

Flappy Bird

+
+ +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/app/games/flappy-bird-js/assets/pipe-green.png b/app/games/flappy-bird-js/assets/pipe-green-bottom.png similarity index 100% rename from app/games/flappy-bird-js/assets/pipe-green.png rename to app/games/flappy-bird-js/assets/pipe-green-bottom.png diff --git a/app/games/flappy-bird-js/assets/pipe-green-top.png b/app/games/flappy-bird-js/assets/pipe-green-top.png new file mode 100644 index 0000000000000000000000000000000000000000..02be5c307eb7aa68a3cd89b52157e203885aaa20 GIT binary patch literal 4924 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 acc + entrada * pesos[index], 0); +return soma >= 0; // decide pular +``` + +Se a soma ponderada das entradas for maior ou igual a zero, o agente decide pular. + +--- + +## Algoritmo Genético + +Após cada ciclo, os indivíduos são avaliados com base em seu tempo de sobrevivência: + +```js +function obterFitness(individuo) { return individuo.tempo - TEMPO_TREINAMENTO; } +``` + +Os indivíduos mais aptos são selecionados para reprodução, e novos indivíduos são criados por cruzamento e mutação: + +```js +const novosGenes = cruzar(ind1, ind2); +novaPopulacao.push(criarIndividuo(novosGenes)); +``` + +--- + +## Treinamento e Evolução + +O treinamento ocorre ao longo de múltiplos ciclos. Quando todos os indivíduos morrem, uma nova população é gerada: + +```js +if (todosMortos) { populacao = novaPopulacao(); } +``` + +Isso permite que o algoritmo genético refine os genes dos indivíduos ao longo do tempo. + +--- + +## Interatividade (controle de tempo) + +O código permite ajustar a escala de tempo da simulação (input range): + +```js +motor.timing.timeScale = 10 * parseInt(entrada.value) / 100; +``` + +--- + +## Notas + +- A versão Markdown serve para documentação/README; para execução interativa utilize `Page.tsx` que importa o componente `Game`. +- Se você quiser que esta versão `Page.md` seja usada como página no projeto (em vez de `Page.tsx`), é necessário atualizar rotas ou conversão para MDX e configurar o build para processar arquivos Markdown/MDX. diff --git a/app/games/flappy-bird-js/game.ts b/app/games/flappy-bird-js/game.ts new file mode 100644 index 0000000..0edb803 --- /dev/null +++ b/app/games/flappy-bird-js/game.ts @@ -0,0 +1,422 @@ +import Matter from "matter-js"; +import type { RefObject } from "react"; + +import birdTexture from "./assets/bird.png" +import backgroundTexture from "./assets/background-day.png" +import pipeTextureTop from "./assets/pipe-green-top.png" +import pipeTextureBottom from "./assets/pipe-green-bottom.png" + +const { + Bodies, + Body, + Composite, + Engine, + Events, + Render, + Runner, +} = Matter; + +// --- Tipos --- +interface EntradaRede { + distanciaCano: number; + canoY: number; + velocidadePassaro: number; + passaroY: number; + pesos: number[]; +} + +// Cada gene é um array de 88 bits (4x22 bits) +interface Individuo { + corpo: Matter.Body; + genes: boolean[]; // 88 bits + tempo: number; + estaVivo: boolean; +} + +// --- Helpers para conversão bits <-> número --- +const GENE_BITS = 22; +const NUM_GENES = 4; +const TOTAL_BITS = GENE_BITS * NUM_GENES; + +// Converte 22 bits (boolean[]) para número no range [-1, 1] +function bitsParaNumero(bits: boolean[]): number { + if (bits.length !== GENE_BITS) throw new Error("bitsParaNumero: tamanho incorreto"); + // Interpreta como inteiro não-sinalizado + let val = 0; + for (let i = 0; i < GENE_BITS; i++) { + if (bits[i]) val |= (1 << (GENE_BITS - 1 - i)); + } + // Normaliza para [0, 1] + const norm = val / ((1 << GENE_BITS) - 1); + // Mapeia para [-1, 1] + return norm * 2 - 1; +} + +// Converte número no range [-1, 1] para 22 bits (boolean[]) +function numeroParaBits(num: number): boolean[] { + // Garante range + const clamped = Math.max(-1, Math.min(1, num)); + // Normaliza para [0, 1] + const norm = (clamped + 1) / 2; + // Converte para inteiro + const val = Math.round(norm * ((1 << GENE_BITS) - 1)); + // Para cada bit + const bits: boolean[] = []; + for (let i = GENE_BITS - 1; i >= 0; i--) { + bits.push(Boolean((val >> i) & 1)); + } + return bits; +} + +// --- Constantes de configuração --- +const TEMPO_TREINAMENTO = 30 * 1000; +const TAMANHO_POPULACAO = 1000; +const ESCALA = 1; +const LARGURA_CANVAS = 500; +const ALTURA_CANVAS = 320 + 125; + +// Dimensões dos sprites/corpos +const PIPE_WIDTH = 50; +const PIPE_HEIGHT = 320; +const BIRD_WIDTH = 52; +const BIRD_HEIGHT = 24; +const INTERVALO_CANO = 100; + +/** + * Classe que encapsula toda a lógica do jogo Flappy Bird. + * - Mantém o estado do motor Matter.js + * - Cria e gerencia a população de indivíduos (agentes) + * - Fornece um método de limpeza (stop) para desacoplar do componente React + */ +class FlappyGame { + private containerRef: RefObject; + private engine: Matter.Engine; + private render: Matter.Render; + private runner: Matter.Runner; + private categoriaJogador = 0x0001; + private categoriaObstaculo = 0x0002; + private canoSuperior!: Matter.Body; + private canoInferior!: Matter.Body; + private populacao: Individuo[] = []; + private ciclos = 1; + private pontos = 0; + + constructor(containerRef: RefObject) { + this.containerRef = containerRef; + + if (!containerRef?.current) { + throw new Error("containerRef is null. Provide a mounted div reference."); + } + + // Cria engine e render + this.engine = Engine.create(); + this.render = Render.create({ + element: containerRef.current, + engine: this.engine, + options: { + width: LARGURA_CANVAS, + height: ALTURA_CANVAS, + background: backgroundTexture, // background sprite + wireframes: false + }, + }); + + this.runner = Runner.create(); + + // Inicializa o mundo e os objetos + this.initWorld(); + this.start(); + + console.log("FlappyGame: iniciado"); + } + + // Inicializa canos, gravidade e adiciona ao mundo + private initWorld() { + // Cano superior (pipe) + this.canoSuperior = Bodies.rectangle(50, 50, PIPE_WIDTH, PIPE_HEIGHT, { + inertia: Infinity, + friction: 0, + collisionFilter: { category: this.categoriaObstaculo }, + label: "obstaculo", + render: { + sprite: { + texture: pipeTextureTop, + xScale: 1, + yScale: 1, + }, + }, + }); + + // Cano inferior (pipe, invertido) + this.canoInferior = Bodies.rectangle(50, 50, PIPE_WIDTH, PIPE_HEIGHT, { + inertia: Infinity, + friction: 0, + label: "obstaculo", + collisionFilter: { category: this.categoriaObstaculo }, + render: { + sprite: { + texture: pipeTextureBottom, + xScale: 1, + yScale: 1, + }, + }, + }); + + this.engine.gravity.scale = 0; + + Composite.add(this.engine.world, [this.canoSuperior, this.canoInferior]); + + Render.run(this.render); + Runner.run(this.runner, this.engine); + + // cria população inicial + for (let i = 0; i < TAMANHO_POPULACAO; i++) { + this.criarIndividuo(); + } + + // Eventos do motor + Events.on(this.engine, "beforeUpdate", (evt) => this.loop(evt)); + Events.on(this.engine, "collisionStart", (evt) => this.tratarColisao(evt)); + + // Conecta controle de escala de tempo (se existir no DOM) + const escalaEl = document.getElementById("escalaTempo") as HTMLInputElement | null; + if (escalaEl) { + escalaEl.onchange = (e) => { + e.preventDefault(); + const value = parseInt(escalaEl.value || "0"); + this.engine.timing.timeScale = 10 * value / 100; + console.log("FlappyGame: timeScale alterado ->", this.engine.timing.timeScale); + }; + } + + // posiciona os canos inicialmente + this.reiniciarCano(); + } + + // Inicia o loop (já ligado nos eventos do engine) + private start() { + console.log("FlappyGame: start runner e render"); + } + + // Para e limpa o jogo + public stop() { + console.log("FlappyGame: parando jogo e limpando recursos"); + Events.off(this.engine, "beforeUpdate"); + Events.off(this.engine, "collisionStart"); + Engine.clear(this.engine); + Render.stop(this.render); + Runner.stop(this.runner); + try { this.render.canvas.remove(); } catch { /* canvas pode já ter sido removido */ } + // limpa texturas caso existam + // @ts-ignore - matter-js internal + this.render.textures = {}; + } + + // --- Loop principal do jogo chamado antes de cada atualização --- + private loop(evento: Matter.IEventTimestamped) { + // Aplica força gravitacional simples a cada indivíduo + this.populacao.forEach((individuo) => { + Body.applyForce(individuo.corpo, individuo.corpo.position, { + x: 0, + y: individuo.corpo.mass * 0.001 * ESCALA, + }); + }); + + // Move canos + Body.setVelocity(this.canoInferior, { x: -2 * ESCALA, y: 0 }); + Body.setVelocity(this.canoSuperior, { x: -2 * ESCALA, y: 0 }); + + // Quando passar do limite, reposiciona e conta ponto + if (this.canoInferior.position.x < PIPE_WIDTH) { + this.reiniciarCano(); + this.pontos++; + this.setTextById("pontos", String(this.pontos)); + console.log("FlappyGame: cano passou -> pontos", this.pontos); + } + + // Remove indivíduos fora do Y visível + this.populacao.filter(i => i.estaVivo).forEach(i => { + if (i.corpo.position.y > ALTURA_CANVAS || i.corpo.position.y < 0) { + this.removerIndividuo(i); + } + }); + + // Atualiza tempo de sobrevivência para aptidão + this.populacao + .filter(i => i.estaVivo) + .forEach(i => i.tempo = evento.timestamp); + + // Decisão de pulo para cada indivíduo + this.populacao.forEach((individuo) => { + const devePular = this.individuoDevePular(individuo); + if (devePular) this.pular(individuo.corpo); + }); + + // Se todos morreram, evolui população + const todosMortos = this.populacao.every(i => !i.estaVivo); + if (todosMortos) { + console.log("FlappyGame: todos mortos - gerando nova população (ciclo)"); + this.pontos = 0; + this.setTextById("pontos", String(this.pontos)); + this.ciclos++; + this.reiniciarCano(); + + // Remove qualquer corpo remanescente dos vivos + this.populacao.filter(i => i.estaVivo).forEach(i => this.removerIndividuo(i)); + + this.populacao = this.novaPopulacao(); + } + } + + // --- Colisão: marca indivíduo como morto --- + private tratarColisao(evento: Matter.IEventCollision) { + evento.pairs.forEach((par) => { + const corpo = par.bodyB; + const individuo = this.populacao.find((ind) => ind.corpo === corpo); + if (individuo) { + console.log("FlappyGame: colisão detectada - removendo individuo"); + this.removerIndividuo(individuo); + } + }); + } + + // --- Geração de nova população por seleção/crossover --- + private novaPopulacao(): Individuo[] { + const ordenados = this.populacao.toSorted((a, b) => this.obterFitness(b) - this.obterFitness(a)); + const maisAptos = Math.floor(TAMANHO_POPULACAO * 0.1); + const nova: Individuo[] = []; + + // Mantém os melhores (copia os bits) + for (let i = 0; i < maisAptos; i++) { + nova.push(this.criarIndividuo([...ordenados[i].genes])); + } + + // Preenche o resto com cruzamentos + for (let i = 0; i < TAMANHO_POPULACAO - maisAptos; i++) { + const r1 = Math.floor(this.obterAleatorio(0, Math.floor(TAMANHO_POPULACAO * 0.5))); + const r2 = Math.floor(this.obterAleatorio(0, Math.floor(TAMANHO_POPULACAO * 0.5))); + const ind1 = ordenados[r1]; + const ind2 = ordenados[r2]; + const genes = this.cruzar(ind1, ind2); + nova.push(this.criarIndividuo(genes)); + } + + console.log("FlappyGame: nova população gerada"); + return nova; + } + + private cruzar(ind1: Individuo, ind2: Individuo): boolean[] { + const novosGenes: boolean[] = []; + for (let i = 0; i < TOTAL_BITS; i++) { + const p = Math.random(); + if (p < 0.45) novosGenes.push(ind1.genes[i]); + else if (p < 0.9) novosGenes.push(ind2.genes[i]); + else novosGenes.push(Math.random() < 0.5); // mutação aleatória + } + return novosGenes; + } + + // Reposiciona os canos com altura aleatória + private reiniciarCano() { + const alturaAleatoria = this.obterAleatorio(25 + INTERVALO_CANO/2, ALTURA_CANVAS - (25 + INTERVALO_CANO/2)); + Body.setPosition(this.canoSuperior, { y: alturaAleatoria - PIPE_HEIGHT/2 - INTERVALO_CANO/2, x: LARGURA_CANVAS + (PIPE_WIDTH/2) }); + Body.setPosition(this.canoInferior, { y: alturaAleatoria + PIPE_HEIGHT/2 + INTERVALO_CANO/2, x: LARGURA_CANVAS + (PIPE_WIDTH/2) }); + Body.setVelocity(this.canoSuperior, { x: 0, y: 0 }); + Body.setVelocity(this.canoInferior, { x: 0, y: 0 }); + console.log("FlappyGame: canos reiniciados, altura", alturaAleatoria); + } + + private pular(corpo: Matter.Body) { + Body.setVelocity(corpo, { x: corpo.velocity.x, y: -4 * ESCALA }); + } + + private obterAleatorio(min: number, max: number) { + return Math.random() * (max - min) + min; + } + + // Decisão do agente usando um perceptron simples + private individuoDevePular(individuo: Individuo) { + const distanciaCano = this.canoInferior.position.x; + const canoY = this.canoInferior.position.y + 300; + const velocidadePassaro = individuo.corpo.velocity.y; + const passaroY = individuo.corpo.position.y; + + // escreve dados na UI apenas para o primeiro indivíduo (para inspeção) + if (this.populacao.findIndex(indi => indi === individuo) === 0) { + this.setTextById("distanciaCano", String(distanciaCano)); + this.setTextById("canoY", String(canoY)); + this.setTextById("velocidadePassaro", String(velocidadePassaro)); + this.setTextById("passaroY", String(passaroY)); + this.setTextById("tempo", String(individuo.tempo)); + } + + // Extrai 4 números dos 88 bits + const pesos: number[] = []; + for (let i = 0; i < NUM_GENES; i++) { + const bits = individuo.genes.slice(i * GENE_BITS, (i + 1) * GENE_BITS); + pesos.push(bitsParaNumero(bits)); + } + return this.calcularPulo({ distanciaCano, canoY, velocidadePassaro, passaroY, pesos }); + } + + private calcularPulo({ distanciaCano, canoY, velocidadePassaro, passaroY, pesos }: EntradaRede) { + const entradas = [distanciaCano, canoY, velocidadePassaro, passaroY]; + const soma = entradas.reduce((acc, entrada, index) => acc + entrada * pesos[index], 0); + return soma >= 0; + } + + private criarIndividuo(genes1?: boolean[]): Individuo { + // Usa sprite do pássaro + const corpo = Bodies.rectangle(50, 50, BIRD_WIDTH, BIRD_HEIGHT, { + inertia: Infinity, + collisionFilter: { category: this.categoriaJogador, group: -1 }, + label: "individuo", + render: { + sprite: { + texture: birdTexture, + xScale: 1, + yScale: 1, + }, + }, + }); + + Composite.add(this.engine.world, [corpo]); + + let genes: boolean[]; + if (genes1) { + genes = [...genes1]; + } else { + // Gera 88 bits aleatórios + genes = Array.from({ length: TOTAL_BITS }, () => Math.random() < 0.5); + } + + const individuo: Individuo = { corpo, genes, tempo: 0, estaVivo: true }; + this.populacao.push(individuo); + return individuo; + } + + private obterFitness(individuo: Individuo) { + return individuo.tempo - TEMPO_TREINAMENTO; + } + + private removerIndividuo(individuo: Individuo) { + individuo.estaVivo = false; + Composite.remove(this.engine.world, [individuo.corpo]); + } + + // Helper para atualizar texto em labels se existirem + private setTextById(id: string, text: string) { + const el = document.getElementById(id); + if (el) el.innerText = text; + } +} + +/** + * Função de conveniência usada pelo componente React: cria FlappyGame e retorna + * uma função de limpeza para parar/limpar quando o componente desmonta. + */ +export function iniciar(containerRef: RefObject) { + const jogo = new FlappyGame(containerRef); + return () => jogo.stop(); +} + diff --git a/app/routes.ts b/app/routes.ts index 6571c19..22f8f56 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -1,3 +1,3 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes"; -export default [index("routes/Home.tsx"), route("/games/flappy", "games/flappy-bird-js/Game.tsx")] satisfies RouteConfig; +export default [index("routes/Home.tsx"), route("/games/flappy-bird-js", "games/flappy-bird-js/Page.tsx")] satisfies RouteConfig; diff --git a/package-lock.json b/package-lock.json index 8a78452..27ac022 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,19 @@ "@pixi/react": "^8.0.3", "@react-router/node": "^7.7.1", "@react-router/serve": "^7.7.1", + "highlight.js": "^11.11.1", "isbot": "^5.1.27", + "marked-react": "^3.0.2", "matter-js": "^0.20.0", "pixi.js": "^8.12.0", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-lowlight": "^3.1.0", "react-router": "^7.7.1" }, "devDependencies": { "@react-router/dev": "^7.7.1", + "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.4", "@types/matter-js": "^0.20.0", "@types/node": "^20", @@ -1801,6 +1805,19 @@ "node": ">= 10" } }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, "node_modules/@tailwindcss/vite": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.12.tgz", @@ -1835,6 +1852,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, "node_modules/@types/matter-js": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/@types/matter-js/-/matter-js-0.20.0.tgz", @@ -1880,6 +1906,12 @@ "@types/react": "*" } }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-rsc": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/@vitejs/plugin-rsc/-/plugin-rsc-0.4.11.tgz", @@ -2344,6 +2376,19 @@ "node": ">= 8" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -2683,6 +2728,19 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2751,6 +2809,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2932,6 +2998,15 @@ "node": ">= 0.4" } }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/hosted-git-info": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz", @@ -3389,6 +3464,30 @@ "dev": true, "license": "MIT" }, + "node_modules/lowlight": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-2.9.0.tgz", + "integrity": "sha512-OpcaUTCLmHuVuBcyNckKfH5B0oA4JUavb/M/8n9iAvanJYNQkrVm4pvyX0SUaqkBG4dnWHKt7p50B3ngAG2Rfw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "fault": "^2.0.0", + "highlight.js": "~11.8.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lowlight/node_modules/highlight.js": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", + "integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3409,6 +3508,30 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.1.tgz", + "integrity": "sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/marked-react": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/marked-react/-/marked-react-3.0.2.tgz", + "integrity": "sha512-uLuH5P5i1H+igH+Y7toWX+sM2UGDdYMIPZ9JtBNajkyFiQBoAMJJY7gBlm3nHXHiPDZHkhZ9p/cb9XO0F1qnCA==", + "license": "MIT", + "dependencies": { + "marked": "^16.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || >=17.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3888,6 +4011,20 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/prettier": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", @@ -4008,6 +4145,24 @@ "react": "^19.1.1" } }, + "node_modules/react-lowlight": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-lowlight/-/react-lowlight-3.1.0.tgz", + "integrity": "sha512-dUhonEgIqptM58wmdjQIBe4zQMlDEt3e5PYdR8VedZGurrdnqjjDtVDD5uHc1V3FQTTjxo9GMX6kyxEcexg3XQ==", + "license": "MIT", + "dependencies": { + "lowlight": "^2.4.1" + }, + "peerDependencies": { + "highlight.js": "^11.0.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "highlight.js": { + "optional": true + } + } + }, "node_modules/react-reconciler": { "version": "0.31.0", "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz", @@ -4717,6 +4872,13 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/package.json b/package.json index fe9a53f..cbee0d4 100644 --- a/package.json +++ b/package.json @@ -12,15 +12,19 @@ "@pixi/react": "^8.0.3", "@react-router/node": "^7.7.1", "@react-router/serve": "^7.7.1", + "highlight.js": "^11.11.1", "isbot": "^5.1.27", + "marked-react": "^3.0.2", "matter-js": "^0.20.0", "pixi.js": "^8.12.0", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-lowlight": "^3.1.0", "react-router": "^7.7.1" }, "devDependencies": { "@react-router/dev": "^7.7.1", + "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.4", "@types/matter-js": "^0.20.0", "@types/node": "^20", From 35ffc71cdf9ccd2fb76bf4976f2ccf35cf2b1845 Mon Sep 17 00:00:00 2001 From: Pedro Santayana Date: Mon, 27 Oct 2025 19:22:32 -0300 Subject: [PATCH 6/6] fim --- app/games/flappy-bird-js/Flappy.tsx | 30 -- app/games/flappy-bird-js/Page.tsx | 31 -- app/games/flappy-bird-js/assets/blue-bird.png | Bin 0 -> 431 bytes app/games/flappy-bird-js/assets/logo.png | Bin 0 -> 16390 bytes .../flappy-bird-js/componentes/Flappy.tsx | 44 ++ app/games/flappy-bird-js/componentes/Page.tsx | 37 ++ app/games/flappy-bird-js/content.md | 80 --- app/games/flappy-bird-js/game.ts | 422 --------------- app/games/flappy-bird-js/logicas/fisica.ts | 25 + app/games/flappy-bird-js/logicas/ia.ts | 80 +++ app/games/flappy-bird-js/logicas/jogo.ts | 509 ++++++++++++++++++ app/games/flappy-bird-js/readme.md | 143 +++++ app/routes.ts | 2 +- 13 files changed, 839 insertions(+), 564 deletions(-) delete mode 100644 app/games/flappy-bird-js/Flappy.tsx delete mode 100644 app/games/flappy-bird-js/Page.tsx create mode 100644 app/games/flappy-bird-js/assets/blue-bird.png create mode 100644 app/games/flappy-bird-js/assets/logo.png create mode 100644 app/games/flappy-bird-js/componentes/Flappy.tsx create mode 100644 app/games/flappy-bird-js/componentes/Page.tsx delete mode 100644 app/games/flappy-bird-js/content.md delete mode 100644 app/games/flappy-bird-js/game.ts create mode 100644 app/games/flappy-bird-js/logicas/fisica.ts create mode 100644 app/games/flappy-bird-js/logicas/ia.ts create mode 100644 app/games/flappy-bird-js/logicas/jogo.ts create mode 100644 app/games/flappy-bird-js/readme.md diff --git a/app/games/flappy-bird-js/Flappy.tsx b/app/games/flappy-bird-js/Flappy.tsx deleted file mode 100644 index bcca195..0000000 --- a/app/games/flappy-bird-js/Flappy.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useRef } from "react"; -import { iniciar } from "./game"; - -export default function FlappyBird() { - const containerRef = useRef(null); - - useEffect(() => { - const finalizar = iniciar(containerRef!); - - return () => finalizar(); - }, [containerRef]); - - return ( -
-
-

Distância do Cano:

-

Y do Cano:

-

Velocidade do Pássaro:

-

Y do Pássaro:

-

Tempo:

-

Pontos:

-
- - -
-
-
-
- ); -} \ No newline at end of file diff --git a/app/games/flappy-bird-js/Page.tsx b/app/games/flappy-bird-js/Page.tsx deleted file mode 100644 index 566db02..0000000 --- a/app/games/flappy-bird-js/Page.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; -import Markdown from "marked-react"; -import FlappyBird from "./Flappy"; -import Lowlight from "react-lowlight"; -//@ts-ignore -import javascript from 'highlight.js/lib/languages/javascript'; -import 'highlight.js/styles/default.css' -import content from "./content.md?raw"; - -Lowlight.registerLanguage('js', javascript); - -const renderer = { - code(snippet: string, lang: string) { - //@ts-ignore - return ; - }, -}; - -export default function Jogo() { - return ( -
-

Flappy Bird

-
- -
-
- -
-
- ); -} \ No newline at end of file diff --git a/app/games/flappy-bird-js/assets/blue-bird.png b/app/games/flappy-bird-js/assets/blue-bird.png new file mode 100644 index 0000000000000000000000000000000000000000..d82802ab8c75244ddbe55d20995eea4a4eea8d2d GIT binary patch literal 431 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 J!EcBM{|C?TjG+Jk literal 0 HcmV?d00001 diff --git a/app/games/flappy-bird-js/componentes/Flappy.tsx b/app/games/flappy-bird-js/componentes/Flappy.tsx new file mode 100644 index 0000000..ac7d688 --- /dev/null +++ b/app/games/flappy-bird-js/componentes/Flappy.tsx @@ -0,0 +1,44 @@ +import { useEffect, useRef } from "react"; +import { iniciar } from "../logicas/jogo"; +import type { ControladorFlappy } from "../logicas/jogo"; + +export default function FlappyBird() { + const containerRef = useRef(null); + const controllerRef = useRef(null); + + useEffect(() => { + // inicia o jogo quando o componente monta + controllerRef.current = iniciar(containerRef!); + return () => { + try { controllerRef.current && controllerRef.current.stop(); } catch { } + }; + }, []); + + function handleRestart() { + // reinicia o jogo: para e cria novamente + try { controllerRef.current && controllerRef.current.stop(); } catch { } + controllerRef.current = iniciar(containerRef!); + // reseta o contador de gerações no novo controlador, se disponível + controllerRef.current?.resetGeracao?.(); + } + + return ( +
+
+
+
+

Distância do cano:

+

Altura da passagem:

+

Velocidade vertical do pássaro:

+

Altura do pássaro:

+

Tempo:

+

Geração:

+

Pontos:

+
+ +
+
+
+
+ ); +} diff --git a/app/games/flappy-bird-js/componentes/Page.tsx b/app/games/flappy-bird-js/componentes/Page.tsx new file mode 100644 index 0000000..168cadf --- /dev/null +++ b/app/games/flappy-bird-js/componentes/Page.tsx @@ -0,0 +1,37 @@ +"use client"; +import Markdown from "marked-react"; +import FlappyBird from "./Flappy"; +import Lowlight from "react-lowlight"; +//@ts-ignore +import typescript from 'highlight.js/lib/languages/typescript'; +import 'highlight.js/styles/dark.css' +import content from "../readme.md?raw"; + +import logo from "../assets/logo.png" + +Lowlight.registerLanguage('ts', typescript); + +const renderer = { + code(snippet: string, lang: string) { + //@ts-ignore + return ; + }, +}; + +export default function Jogo() { + return ( +
+
+ logo +
+
+
+ +
+
+
+ +
+
+ ); +} diff --git a/app/games/flappy-bird-js/content.md b/app/games/flappy-bird-js/content.md deleted file mode 100644 index 824076e..0000000 --- a/app/games/flappy-bird-js/content.md +++ /dev/null @@ -1,80 +0,0 @@ -# Visão geral - -Esta página descreve um agente treinado para jogar Flappy Bird utilizando um perceptron de uma camada e um algoritmo genético. O agente é representado por indivíduos em uma população; cada indivíduo possui um conjunto de genes que define seu comportamento. A cada ciclo, os indivíduos tomam decisões baseadas em entradas do ambiente e são avaliados com base no tempo de sobrevivência. - -## Estrutura do Código - -O código utiliza a biblioteca `matter-js` para simular a física do jogo. Por exemplo, os canos e os pássaros são representados como corpos físicos: - -```js -const canoInferior = Bodies.rectangle(50, 50, 50, 400, { /* ... */ }); -const canoSuperior = Bodies.rectangle(50, 50, 50, 400, { /* ... */ }); -``` - -Esses corpos são adicionados ao mundo da simulação: - -```js -Composite.add(motor.world, [canoInferior, canoSuperior]); -``` - ---- - -## Decisão do Agente - -Cada indivíduo decide se deve "pular" com base em um perceptron de uma camada. As entradas para o perceptron incluem a distância do cano, a posição do cano, a velocidade do pássaro e a posição do pássaro. - -Exemplo (pseudocódigo): - -```js -const entradas = [distanciaCano, canoY, velocidadePassaro, passaroY]; -const soma = entradas.reduce((acc, entrada, index) => acc + entrada * pesos[index], 0); -return soma >= 0; // decide pular -``` - -Se a soma ponderada das entradas for maior ou igual a zero, o agente decide pular. - ---- - -## Algoritmo Genético - -Após cada ciclo, os indivíduos são avaliados com base em seu tempo de sobrevivência: - -```js -function obterFitness(individuo) { return individuo.tempo - TEMPO_TREINAMENTO; } -``` - -Os indivíduos mais aptos são selecionados para reprodução, e novos indivíduos são criados por cruzamento e mutação: - -```js -const novosGenes = cruzar(ind1, ind2); -novaPopulacao.push(criarIndividuo(novosGenes)); -``` - ---- - -## Treinamento e Evolução - -O treinamento ocorre ao longo de múltiplos ciclos. Quando todos os indivíduos morrem, uma nova população é gerada: - -```js -if (todosMortos) { populacao = novaPopulacao(); } -``` - -Isso permite que o algoritmo genético refine os genes dos indivíduos ao longo do tempo. - ---- - -## Interatividade (controle de tempo) - -O código permite ajustar a escala de tempo da simulação (input range): - -```js -motor.timing.timeScale = 10 * parseInt(entrada.value) / 100; -``` - ---- - -## Notas - -- A versão Markdown serve para documentação/README; para execução interativa utilize `Page.tsx` que importa o componente `Game`. -- Se você quiser que esta versão `Page.md` seja usada como página no projeto (em vez de `Page.tsx`), é necessário atualizar rotas ou conversão para MDX e configurar o build para processar arquivos Markdown/MDX. diff --git a/app/games/flappy-bird-js/game.ts b/app/games/flappy-bird-js/game.ts deleted file mode 100644 index 0edb803..0000000 --- a/app/games/flappy-bird-js/game.ts +++ /dev/null @@ -1,422 +0,0 @@ -import Matter from "matter-js"; -import type { RefObject } from "react"; - -import birdTexture from "./assets/bird.png" -import backgroundTexture from "./assets/background-day.png" -import pipeTextureTop from "./assets/pipe-green-top.png" -import pipeTextureBottom from "./assets/pipe-green-bottom.png" - -const { - Bodies, - Body, - Composite, - Engine, - Events, - Render, - Runner, -} = Matter; - -// --- Tipos --- -interface EntradaRede { - distanciaCano: number; - canoY: number; - velocidadePassaro: number; - passaroY: number; - pesos: number[]; -} - -// Cada gene é um array de 88 bits (4x22 bits) -interface Individuo { - corpo: Matter.Body; - genes: boolean[]; // 88 bits - tempo: number; - estaVivo: boolean; -} - -// --- Helpers para conversão bits <-> número --- -const GENE_BITS = 22; -const NUM_GENES = 4; -const TOTAL_BITS = GENE_BITS * NUM_GENES; - -// Converte 22 bits (boolean[]) para número no range [-1, 1] -function bitsParaNumero(bits: boolean[]): number { - if (bits.length !== GENE_BITS) throw new Error("bitsParaNumero: tamanho incorreto"); - // Interpreta como inteiro não-sinalizado - let val = 0; - for (let i = 0; i < GENE_BITS; i++) { - if (bits[i]) val |= (1 << (GENE_BITS - 1 - i)); - } - // Normaliza para [0, 1] - const norm = val / ((1 << GENE_BITS) - 1); - // Mapeia para [-1, 1] - return norm * 2 - 1; -} - -// Converte número no range [-1, 1] para 22 bits (boolean[]) -function numeroParaBits(num: number): boolean[] { - // Garante range - const clamped = Math.max(-1, Math.min(1, num)); - // Normaliza para [0, 1] - const norm = (clamped + 1) / 2; - // Converte para inteiro - const val = Math.round(norm * ((1 << GENE_BITS) - 1)); - // Para cada bit - const bits: boolean[] = []; - for (let i = GENE_BITS - 1; i >= 0; i--) { - bits.push(Boolean((val >> i) & 1)); - } - return bits; -} - -// --- Constantes de configuração --- -const TEMPO_TREINAMENTO = 30 * 1000; -const TAMANHO_POPULACAO = 1000; -const ESCALA = 1; -const LARGURA_CANVAS = 500; -const ALTURA_CANVAS = 320 + 125; - -// Dimensões dos sprites/corpos -const PIPE_WIDTH = 50; -const PIPE_HEIGHT = 320; -const BIRD_WIDTH = 52; -const BIRD_HEIGHT = 24; -const INTERVALO_CANO = 100; - -/** - * Classe que encapsula toda a lógica do jogo Flappy Bird. - * - Mantém o estado do motor Matter.js - * - Cria e gerencia a população de indivíduos (agentes) - * - Fornece um método de limpeza (stop) para desacoplar do componente React - */ -class FlappyGame { - private containerRef: RefObject; - private engine: Matter.Engine; - private render: Matter.Render; - private runner: Matter.Runner; - private categoriaJogador = 0x0001; - private categoriaObstaculo = 0x0002; - private canoSuperior!: Matter.Body; - private canoInferior!: Matter.Body; - private populacao: Individuo[] = []; - private ciclos = 1; - private pontos = 0; - - constructor(containerRef: RefObject) { - this.containerRef = containerRef; - - if (!containerRef?.current) { - throw new Error("containerRef is null. Provide a mounted div reference."); - } - - // Cria engine e render - this.engine = Engine.create(); - this.render = Render.create({ - element: containerRef.current, - engine: this.engine, - options: { - width: LARGURA_CANVAS, - height: ALTURA_CANVAS, - background: backgroundTexture, // background sprite - wireframes: false - }, - }); - - this.runner = Runner.create(); - - // Inicializa o mundo e os objetos - this.initWorld(); - this.start(); - - console.log("FlappyGame: iniciado"); - } - - // Inicializa canos, gravidade e adiciona ao mundo - private initWorld() { - // Cano superior (pipe) - this.canoSuperior = Bodies.rectangle(50, 50, PIPE_WIDTH, PIPE_HEIGHT, { - inertia: Infinity, - friction: 0, - collisionFilter: { category: this.categoriaObstaculo }, - label: "obstaculo", - render: { - sprite: { - texture: pipeTextureTop, - xScale: 1, - yScale: 1, - }, - }, - }); - - // Cano inferior (pipe, invertido) - this.canoInferior = Bodies.rectangle(50, 50, PIPE_WIDTH, PIPE_HEIGHT, { - inertia: Infinity, - friction: 0, - label: "obstaculo", - collisionFilter: { category: this.categoriaObstaculo }, - render: { - sprite: { - texture: pipeTextureBottom, - xScale: 1, - yScale: 1, - }, - }, - }); - - this.engine.gravity.scale = 0; - - Composite.add(this.engine.world, [this.canoSuperior, this.canoInferior]); - - Render.run(this.render); - Runner.run(this.runner, this.engine); - - // cria população inicial - for (let i = 0; i < TAMANHO_POPULACAO; i++) { - this.criarIndividuo(); - } - - // Eventos do motor - Events.on(this.engine, "beforeUpdate", (evt) => this.loop(evt)); - Events.on(this.engine, "collisionStart", (evt) => this.tratarColisao(evt)); - - // Conecta controle de escala de tempo (se existir no DOM) - const escalaEl = document.getElementById("escalaTempo") as HTMLInputElement | null; - if (escalaEl) { - escalaEl.onchange = (e) => { - e.preventDefault(); - const value = parseInt(escalaEl.value || "0"); - this.engine.timing.timeScale = 10 * value / 100; - console.log("FlappyGame: timeScale alterado ->", this.engine.timing.timeScale); - }; - } - - // posiciona os canos inicialmente - this.reiniciarCano(); - } - - // Inicia o loop (já ligado nos eventos do engine) - private start() { - console.log("FlappyGame: start runner e render"); - } - - // Para e limpa o jogo - public stop() { - console.log("FlappyGame: parando jogo e limpando recursos"); - Events.off(this.engine, "beforeUpdate"); - Events.off(this.engine, "collisionStart"); - Engine.clear(this.engine); - Render.stop(this.render); - Runner.stop(this.runner); - try { this.render.canvas.remove(); } catch { /* canvas pode já ter sido removido */ } - // limpa texturas caso existam - // @ts-ignore - matter-js internal - this.render.textures = {}; - } - - // --- Loop principal do jogo chamado antes de cada atualização --- - private loop(evento: Matter.IEventTimestamped) { - // Aplica força gravitacional simples a cada indivíduo - this.populacao.forEach((individuo) => { - Body.applyForce(individuo.corpo, individuo.corpo.position, { - x: 0, - y: individuo.corpo.mass * 0.001 * ESCALA, - }); - }); - - // Move canos - Body.setVelocity(this.canoInferior, { x: -2 * ESCALA, y: 0 }); - Body.setVelocity(this.canoSuperior, { x: -2 * ESCALA, y: 0 }); - - // Quando passar do limite, reposiciona e conta ponto - if (this.canoInferior.position.x < PIPE_WIDTH) { - this.reiniciarCano(); - this.pontos++; - this.setTextById("pontos", String(this.pontos)); - console.log("FlappyGame: cano passou -> pontos", this.pontos); - } - - // Remove indivíduos fora do Y visível - this.populacao.filter(i => i.estaVivo).forEach(i => { - if (i.corpo.position.y > ALTURA_CANVAS || i.corpo.position.y < 0) { - this.removerIndividuo(i); - } - }); - - // Atualiza tempo de sobrevivência para aptidão - this.populacao - .filter(i => i.estaVivo) - .forEach(i => i.tempo = evento.timestamp); - - // Decisão de pulo para cada indivíduo - this.populacao.forEach((individuo) => { - const devePular = this.individuoDevePular(individuo); - if (devePular) this.pular(individuo.corpo); - }); - - // Se todos morreram, evolui população - const todosMortos = this.populacao.every(i => !i.estaVivo); - if (todosMortos) { - console.log("FlappyGame: todos mortos - gerando nova população (ciclo)"); - this.pontos = 0; - this.setTextById("pontos", String(this.pontos)); - this.ciclos++; - this.reiniciarCano(); - - // Remove qualquer corpo remanescente dos vivos - this.populacao.filter(i => i.estaVivo).forEach(i => this.removerIndividuo(i)); - - this.populacao = this.novaPopulacao(); - } - } - - // --- Colisão: marca indivíduo como morto --- - private tratarColisao(evento: Matter.IEventCollision) { - evento.pairs.forEach((par) => { - const corpo = par.bodyB; - const individuo = this.populacao.find((ind) => ind.corpo === corpo); - if (individuo) { - console.log("FlappyGame: colisão detectada - removendo individuo"); - this.removerIndividuo(individuo); - } - }); - } - - // --- Geração de nova população por seleção/crossover --- - private novaPopulacao(): Individuo[] { - const ordenados = this.populacao.toSorted((a, b) => this.obterFitness(b) - this.obterFitness(a)); - const maisAptos = Math.floor(TAMANHO_POPULACAO * 0.1); - const nova: Individuo[] = []; - - // Mantém os melhores (copia os bits) - for (let i = 0; i < maisAptos; i++) { - nova.push(this.criarIndividuo([...ordenados[i].genes])); - } - - // Preenche o resto com cruzamentos - for (let i = 0; i < TAMANHO_POPULACAO - maisAptos; i++) { - const r1 = Math.floor(this.obterAleatorio(0, Math.floor(TAMANHO_POPULACAO * 0.5))); - const r2 = Math.floor(this.obterAleatorio(0, Math.floor(TAMANHO_POPULACAO * 0.5))); - const ind1 = ordenados[r1]; - const ind2 = ordenados[r2]; - const genes = this.cruzar(ind1, ind2); - nova.push(this.criarIndividuo(genes)); - } - - console.log("FlappyGame: nova população gerada"); - return nova; - } - - private cruzar(ind1: Individuo, ind2: Individuo): boolean[] { - const novosGenes: boolean[] = []; - for (let i = 0; i < TOTAL_BITS; i++) { - const p = Math.random(); - if (p < 0.45) novosGenes.push(ind1.genes[i]); - else if (p < 0.9) novosGenes.push(ind2.genes[i]); - else novosGenes.push(Math.random() < 0.5); // mutação aleatória - } - return novosGenes; - } - - // Reposiciona os canos com altura aleatória - private reiniciarCano() { - const alturaAleatoria = this.obterAleatorio(25 + INTERVALO_CANO/2, ALTURA_CANVAS - (25 + INTERVALO_CANO/2)); - Body.setPosition(this.canoSuperior, { y: alturaAleatoria - PIPE_HEIGHT/2 - INTERVALO_CANO/2, x: LARGURA_CANVAS + (PIPE_WIDTH/2) }); - Body.setPosition(this.canoInferior, { y: alturaAleatoria + PIPE_HEIGHT/2 + INTERVALO_CANO/2, x: LARGURA_CANVAS + (PIPE_WIDTH/2) }); - Body.setVelocity(this.canoSuperior, { x: 0, y: 0 }); - Body.setVelocity(this.canoInferior, { x: 0, y: 0 }); - console.log("FlappyGame: canos reiniciados, altura", alturaAleatoria); - } - - private pular(corpo: Matter.Body) { - Body.setVelocity(corpo, { x: corpo.velocity.x, y: -4 * ESCALA }); - } - - private obterAleatorio(min: number, max: number) { - return Math.random() * (max - min) + min; - } - - // Decisão do agente usando um perceptron simples - private individuoDevePular(individuo: Individuo) { - const distanciaCano = this.canoInferior.position.x; - const canoY = this.canoInferior.position.y + 300; - const velocidadePassaro = individuo.corpo.velocity.y; - const passaroY = individuo.corpo.position.y; - - // escreve dados na UI apenas para o primeiro indivíduo (para inspeção) - if (this.populacao.findIndex(indi => indi === individuo) === 0) { - this.setTextById("distanciaCano", String(distanciaCano)); - this.setTextById("canoY", String(canoY)); - this.setTextById("velocidadePassaro", String(velocidadePassaro)); - this.setTextById("passaroY", String(passaroY)); - this.setTextById("tempo", String(individuo.tempo)); - } - - // Extrai 4 números dos 88 bits - const pesos: number[] = []; - for (let i = 0; i < NUM_GENES; i++) { - const bits = individuo.genes.slice(i * GENE_BITS, (i + 1) * GENE_BITS); - pesos.push(bitsParaNumero(bits)); - } - return this.calcularPulo({ distanciaCano, canoY, velocidadePassaro, passaroY, pesos }); - } - - private calcularPulo({ distanciaCano, canoY, velocidadePassaro, passaroY, pesos }: EntradaRede) { - const entradas = [distanciaCano, canoY, velocidadePassaro, passaroY]; - const soma = entradas.reduce((acc, entrada, index) => acc + entrada * pesos[index], 0); - return soma >= 0; - } - - private criarIndividuo(genes1?: boolean[]): Individuo { - // Usa sprite do pássaro - const corpo = Bodies.rectangle(50, 50, BIRD_WIDTH, BIRD_HEIGHT, { - inertia: Infinity, - collisionFilter: { category: this.categoriaJogador, group: -1 }, - label: "individuo", - render: { - sprite: { - texture: birdTexture, - xScale: 1, - yScale: 1, - }, - }, - }); - - Composite.add(this.engine.world, [corpo]); - - let genes: boolean[]; - if (genes1) { - genes = [...genes1]; - } else { - // Gera 88 bits aleatórios - genes = Array.from({ length: TOTAL_BITS }, () => Math.random() < 0.5); - } - - const individuo: Individuo = { corpo, genes, tempo: 0, estaVivo: true }; - this.populacao.push(individuo); - return individuo; - } - - private obterFitness(individuo: Individuo) { - return individuo.tempo - TEMPO_TREINAMENTO; - } - - private removerIndividuo(individuo: Individuo) { - individuo.estaVivo = false; - Composite.remove(this.engine.world, [individuo.corpo]); - } - - // Helper para atualizar texto em labels se existirem - private setTextById(id: string, text: string) { - const el = document.getElementById(id); - if (el) el.innerText = text; - } -} - -/** - * Função de conveniência usada pelo componente React: cria FlappyGame e retorna - * uma função de limpeza para parar/limpar quando o componente desmonta. - */ -export function iniciar(containerRef: RefObject) { - const jogo = new FlappyGame(containerRef); - return () => jogo.stop(); -} - diff --git a/app/games/flappy-bird-js/logicas/fisica.ts b/app/games/flappy-bird-js/logicas/fisica.ts new file mode 100644 index 0000000..23590ae --- /dev/null +++ b/app/games/flappy-bird-js/logicas/fisica.ts @@ -0,0 +1,25 @@ +import Matter from "matter-js"; + +// Módulo de física traduzido: cria engine, render e runner do Matter.js +// Exporta função que inicia o render/runner e retorna os objetos criados + +const { Engine, Render, Runner } = Matter; + +export function criarMotorERender(element: HTMLElement | null, largura: number, altura: number) { + if (!element) throw new Error("criarMotorERender: element é nulo"); + const engine = Engine.create(); + const render = Render.create({ + element, + engine, + options: { + width: largura, + height: altura, + wireframes: false, + }, + }); + const runner = Runner.create(); + // inicia render e runner + Render.run(render); + Runner.run(runner, engine); + return { engine, render, runner }; +} diff --git a/app/games/flappy-bird-js/logicas/ia.ts b/app/games/flappy-bird-js/logicas/ia.ts new file mode 100644 index 0000000..eb99158 --- /dev/null +++ b/app/games/flappy-bird-js/logicas/ia.ts @@ -0,0 +1,80 @@ +// Lógica da IA: genes como bits e perceptron +// Arquivo traduzido para PT-BR com comentários explicativos + +export type Genes = boolean[]; + +export interface EntradaRede { + distanciaCano: number; + canoY: number; + velocidadePassaro: number; + passaroY: number; + pesos: number[]; +} + +export const GENE_BITS = 22; +export const NUM_GENES = 4; +export const TOTAL_BITS = GENE_BITS * NUM_GENES; + +// Converte 22 bits (boolean[]) para número no intervalo [-1, 1] +export function bitsParaNumero(bits: boolean[]): number { + if (bits.length !== GENE_BITS) throw new Error("bitsParaNumero: tamanho incorreto"); + let val = 0; + for (let i = 0; i < GENE_BITS; i++) { + if (bits[i]) val |= (1 << (GENE_BITS - 1 - i)); + } + const norm = val / ((1 << GENE_BITS) - 1); + return norm * 2 - 1; +} + +// Converte número no intervalo [-1, 1] para 22 bits (boolean[]) +export function numeroParaBits(num: number): boolean[] { + const clamped = Math.max(-1, Math.min(1, num)); + const norm = (clamped + 1) / 2; + const val = Math.round(norm * ((1 << GENE_BITS) - 1)); + const bits: boolean[] = []; + for (let i = GENE_BITS - 1; i >= 0; i--) { + bits.push(Boolean((val >> i) & 1)); + } + return bits; +} + +// Gera genes aleatórios (TOTAL_BITS bits) +export function criarGenesAleatorios(): Genes { + return Array.from({ length: TOTAL_BITS }, () => Math.random() < 0.5); +} + +// Cruzamento/mutação bit a bit entre dois genes +export function cruzarGenes(g1: Genes, g2: Genes): Genes { + const out: Genes = []; + for (let i = 0; i < TOTAL_BITS; i++) { + const p = Math.random(); + if (p < 0.45) out.push(g1[i]); + else if (p < 0.9) out.push(g2[i]); + else out.push(Math.random() < 0.5); // mutação + } + return out; +} + +// Extrai os 4 pesos (números) a partir dos 88 bits +export function extrairPesos(genes: Genes): number[] { + const pesos: number[] = []; + for (let i = 0; i < NUM_GENES; i++) { + const bits = genes.slice(i * GENE_BITS, (i + 1) * GENE_BITS); + pesos.push(bitsParaNumero(bits)); + } + return pesos; +} + +// Calcula a decisão do perceptron a partir das entradas e dos genes +// Retorna true para pular, false caso contrário +export function calcularPuloPorGenes(distanciaCano: number, canoY: number, velocidadePassaro: number, passaroY: number, genes: Genes) { + const pesos = extrairPesos(genes); + const entradas = [distanciaCano, canoY, velocidadePassaro, passaroY]; + const soma = entradas.reduce((acc, entrada, idx) => acc + entrada * pesos[idx], 0); + return soma >= 0; +} + +// Cria uma população de genes (array de Genes) +export function criarPopulacaoGenes(tamanho: number): Genes[] { + return Array.from({ length: tamanho }, () => criarGenesAleatorios()); +} diff --git a/app/games/flappy-bird-js/logicas/jogo.ts b/app/games/flappy-bird-js/logicas/jogo.ts new file mode 100644 index 0000000..f439eda --- /dev/null +++ b/app/games/flappy-bird-js/logicas/jogo.ts @@ -0,0 +1,509 @@ +import Matter from "matter-js"; +import type { RefObject } from "react"; + +import birdTexture from "../assets/bird.png" +import blueBirdTexture from "../assets/blue-bird.png" +import backgroundTexture from "../assets/background-day.png" +import pipeTextureTop from "../assets/pipe-green-top.png" +import pipeTextureBottom from "../assets/pipe-green-bottom.png" +import { criarMotorERender } from "./fisica"; +import type { Genes } from "./ia"; +import { cruzarGenes, calcularPuloPorGenes, extrairPesos, TOTAL_BITS } from "./ia"; + +const { + Bodies, + Body, + Composite, + Engine, + Events, + Render, + Runner, +} = Matter; + +// --- Tipos --- +interface Individuo { + corpo: Matter.Body; + genes: Genes; // 88 bits + tempo: number; + estaVivo: boolean; +} + +// --- Constantes de configuração --- +const TEMPO_TREINAMENTO = 30 * 1000; +const TAMANHO_POPULACAO = 100; +const ESCALA = 1; +const LARGURA_CANVAS = 500; +const ALTURA_CANVAS = 320 + 125; + +// Dimensões dos sprites/corpos +const PIPE_WIDTH = 50; +const PIPE_HEIGHT = 320; +const BIRD_WIDTH = 52; +const BIRD_HEIGHT = 24; +const INTERVALO_CANO = 100; + +/** + * Classe que encapsula toda a lógica do jogo Flappy Bird. + * Traduzida para PT-BR com comentários e nomes em português. + */ +class JogoFlappy { + private containerRef: RefObject; + private engine: Matter.Engine; + private render: Matter.Render; + private runner: Matter.Runner; + private categoriaJogador = 0x0001; + private categoriaObstaculo = 0x0002; + private canoSuperior!: Matter.Body; + private canoInferior!: Matter.Body; + private canoAlturaCentro: number = 0; + private populacao: Individuo[] = []; + private ciclos = 0; + private pontos = 0; + // Indivíduo observado pela UI (mostrado com textura diferente) + private observadoIndividuo: Individuo | null = null; + // listeners para notificações de pesos do indivíduo observado + private observedPesosListeners: Set<(pesos: number[] | null) => void> = new Set(); + // Timestamp de início da geração (usado para calcular tempo de vida relativo) + private geracaoInicioTimestamp = 0; + + constructor(containerRef: RefObject) { + this.containerRef = containerRef; + + if (!containerRef?.current) { + throw new Error("containerRef é nulo. Forneça uma div montada."); + } + + // Cria motor, render e runner via módulo de física (inicia render/runner) + const ctx = criarMotorERender(containerRef.current, LARGURA_CANVAS, ALTURA_CANVAS); + this.engine = ctx.engine; + this.render = ctx.render; + this.runner = ctx.runner; + // aplica fundo (createEngineAndRender setou wireframes:false mas não background) + if (this.render && (this.render.options as any)) (this.render.options as any).background = backgroundTexture; + + // Inicializa o mundo e os objetos + this.initWorld(); + this.start(); + + console.log("JogoFlappy: iniciado"); + } + + // Inicializa canos, gravidade e adiciona ao mundo + private initWorld() { + // Cano superior (pipe) + this.canoSuperior = Bodies.rectangle(50, 50, PIPE_WIDTH, PIPE_HEIGHT, { + inertia: Infinity, + friction: 0, + collisionFilter: { category: this.categoriaObstaculo }, + label: "obstaculo", + render: { + sprite: { + texture: pipeTextureTop, + xScale: 1, + yScale: 1, + }, + }, + }); + + // Cano inferior (pipe, invertido) + this.canoInferior = Bodies.rectangle(50, 50, PIPE_WIDTH, PIPE_HEIGHT, { + inertia: Infinity, + friction: 0, + label: "obstaculo", + collisionFilter: { category: this.categoriaObstaculo }, + render: { + sprite: { + texture: pipeTextureBottom, + xScale: 1, + yScale: 1, + }, + }, + }); + + this.engine.gravity.scale = 0; + + Composite.add(this.engine.world, [this.canoSuperior, this.canoInferior]); + + // cria população inicial + for (let i = 0; i < TAMANHO_POPULACAO; i++) { + this.criarIndividuo(); + } + + // seleciona um indivíduo observado para a UI + this.selecionarProximoObservado(); + + // Eventos do motor + Events.on(this.engine, "beforeUpdate", (evt) => this.loop(evt)); + Events.on(this.engine, "collisionStart", (evt) => this.tratarColisao(evt)); + + // Controle de teclado para ajuste de timeScale (não exposto na UI) + window.addEventListener("keydown", this.handleTimeScaleKey); + + // posiciona os canos inicialmente + this.reiniciarCano(); + + // inicializa UI com pontos 0 + this.setTextById("pontos", "0"); + // inicializa UI com geração atual + this.setTextById("geracao", String(this.ciclos)); + } + + // Inicia o loop (já ligado nos eventos do engine) + private start() { + console.log("JogoFlappy: start runner e render"); + } + + // Para e limpa o jogo + public stop() { + console.log("JogoFlappy: parando jogo e limpando recursos"); + Events.off(this.engine, "beforeUpdate"); + Events.off(this.engine, "collisionStart"); + window.removeEventListener("keydown", this.handleTimeScaleKey); + Engine.clear(this.engine); + Render.stop(this.render); + Runner.stop(this.runner); + try { this.render.canvas.remove(); } catch { /* canvas pode já ter sido removido */ } + // limpa texturas caso existam + // @ts-ignore - matter-js internal + this.render.textures = {}; + } + + // Key handler para ajuste de timeScale + private handleTimeScaleKey = (e: KeyboardEvent) => { + if (!e) return; + const key = e.key; + if (key === "+" || key === "=") { + this.adjustTimeScale(0.25); + } else if (key === "-") { + this.adjustTimeScale(-0.25); + } else if (key === "0") { + this.setTimeScale(1); + } + }; + + // API programática para set/get/adjust timeScale + public setTimeScale(value: number) { + const clamped = Number(value) || 1; + this.engine.timing.timeScale = clamped; + console.log("JogoFlappy: timeScale set ->", this.engine.timing.timeScale); + } + + public getTimeScale() { + return this.engine.timing.timeScale; + } + + // Reseta o contador de gerações para 1 e atualiza a UI + public resetGeracao() { + this.ciclos = 1; + this.setTextById("geracao", String(this.ciclos)); + } + + public adjustTimeScale(delta: number) { + const next = Math.max(0.01, (this.engine.timing.timeScale || 1) + delta); + this.setTimeScale(Number(next.toFixed(2))); + } + + // --- Loop principal do jogo chamado antes de cada atualização --- + private loop(evento: Matter.IEventTimestamped) { + // Aplica força gravitacional simples a cada indivíduo + this.populacao.forEach((individuo) => { + Body.applyForce(individuo.corpo, individuo.corpo.position, { + x: 0, + y: individuo.corpo.mass * 0.001 * ESCALA, + }); + }); + + // Move canos (apenas velocidade X) + Body.setVelocity(this.canoInferior, { x: -2 * ESCALA, y: 0 }); + Body.setVelocity(this.canoSuperior, { x: -2 * ESCALA, y: 0 }); + + // Força os canos a manterem sua posição vertical (lock Y) enquanto permitem X + try { + const targetSuperiorY = this.canoAlturaCentro - PIPE_HEIGHT / 2 - INTERVALO_CANO / 2; + const targetInferiorY = this.canoAlturaCentro + PIPE_HEIGHT / 2 + INTERVALO_CANO / 2; + Body.setPosition(this.canoSuperior, { x: this.canoSuperior.position.x, y: targetSuperiorY }); + Body.setPosition(this.canoInferior, { x: this.canoInferior.position.x, y: targetInferiorY }); + // Garante velocidade Y zero para evitar jitter + Body.setVelocity(this.canoSuperior, { x: this.canoSuperior.velocity.x, y: 0 }); + Body.setVelocity(this.canoInferior, { x: this.canoInferior.velocity.x, y: 0 }); + } catch { } + + // Quando passar do limite, reposiciona e conta ponto + if (this.canoInferior.position.x < PIPE_WIDTH) { + this.reiniciarCano(); + this.pontos++; + this.setTextById("pontos", String(this.pontos)); + console.log("JogoFlappy: cano passou -> pontos", this.pontos); + } + + // Remove indivíduos fora do Y visível + this.populacao.filter(i => i.estaVivo).forEach(i => { + if (i.corpo.position.y > ALTURA_CANVAS || i.corpo.position.y < 0) { + this.removerIndividuo(i); + } + }); + + // Inicializa o timestamp de início da geração na primeira atualização + if (this.geracaoInicioTimestamp === 0) this.geracaoInicioTimestamp = evento.timestamp; + + // Atualiza tempo de sobrevivência (relativo ao início da geração) + this.populacao + .filter(i => i.estaVivo) + .forEach(i => i.tempo = evento.timestamp - this.geracaoInicioTimestamp); + + // Decisão de pulo para cada indivíduo + let observedInputs: number[] | null = null; + this.populacao.forEach((individuo) => { + // Entradas para o perceptron + const distanciaCano = this.canoInferior.position.x; + const canoY = this.canoInferior.position.y - (PIPE_HEIGHT / 2 + INTERVALO_CANO / 2); + const velocidadePassaro = individuo.corpo.velocity.y; + const passaroY = individuo.corpo.position.y; + const devePular = calcularPuloPorGenes(distanciaCano, canoY, velocidadePassaro, passaroY, individuo.genes); + if (devePular) this.pular(individuo.corpo); + // Se é o observado, armazena para UI + if (this.observadoIndividuo && individuo === this.observadoIndividuo) { + observedInputs = [distanciaCano, canoY, velocidadePassaro, passaroY]; + } + }); + + // Atualiza labels da UI para o indivíduo observado a cada tick + if (this.observadoIndividuo && observedInputs) { + this.setTextById("distanciaCano", this.formatNumber(observedInputs[0])); + this.setTextById("canoY", this.formatNumber(observedInputs[1])); + this.setTextById("velocidadePassaro", this.formatNumber(observedInputs[2])); + this.setTextById("passaroY", this.formatNumber(observedInputs[3])); + this.setTextById("tempo", this.formatTime(this.observadoIndividuo.tempo)); + } + + // Garante que o indivíduo observado esteja sempre desenhado por último (na frente) + this.trazerObservadoFrente(); + + // Se todos morreram, evolui população + const todosMortos = this.populacao.every(i => !i.estaVivo); + if (todosMortos) { + console.log("JogoFlappy: todos mortos - gerando nova população (ciclo)"); + this.pontos = 0; + this.setTextById("pontos", String(this.pontos)); + this.setTextById("geracao", String(this.ciclos)) + this.ciclos++; + this.reiniciarCano(); + + // Remove qualquer corpo remanescente dos vivos + this.populacao.filter(i => i.estaVivo).forEach(i => this.removerIndividuo(i)); + + this.populacao = this.novaPopulacao(); + // seleciona novo observado quando nova população é gerada + this.selecionarProximoObservado(); + // reseta o tempo de vida relativo para a nova geração + this.geracaoInicioTimestamp = evento.timestamp; + } + } + + // --- Colisão: marca indivíduo como morto --- + private tratarColisao(evento: Matter.IEventCollision) { + evento.pairs.forEach((par) => { + const corpo = par.bodyB; + const individuo = this.populacao.find((ind) => ind.corpo === corpo); + if (individuo) { + console.log("JogoFlappy: colisão detectada - removendo individuo"); + this.removerIndividuo(individuo); + } + }); + } + + // --- Geração de nova população por seleção/crossover --- + private novaPopulacao(): Individuo[] { + const ordenados = this.populacao.toSorted((a, b) => this.obterFitness(b) - this.obterFitness(a)); + const maisAptos = Math.floor(TAMANHO_POPULACAO * 0.1); + const nova: Individuo[] = []; + + // Mantém os melhores (copia os bits) + for (let i = 0; i < maisAptos; i++) { + nova.push(this.criarIndividuo([...ordenados[i].genes])); + } + + // Preenche o resto com cruzamentos + for (let i = 0; i < TAMANHO_POPULACAO - maisAptos; i++) { + const r1 = Math.floor(this.obterAleatorio(0, Math.floor(TAMANHO_POPULACAO * 0.5))); + const r2 = Math.floor(this.obterAleatorio(0, Math.floor(TAMANHO_POPULACAO * 0.5))); + const ind1 = ordenados[r1]; + const ind2 = ordenados[r2]; + const genes = cruzarGenes(ind1.genes, ind2.genes); + nova.push(this.criarIndividuo(genes)); + } + + console.log("JogoFlappy: nova população gerada"); + return nova; + } + + // Reposiciona os canos com altura aleatória + private reiniciarCano() { + const alturaAleatoria = this.obterAleatorio(25 + INTERVALO_CANO / 2, ALTURA_CANVAS - (25 + INTERVALO_CANO / 2)); + this.canoAlturaCentro = alturaAleatoria; + Body.setPosition(this.canoSuperior, { y: alturaAleatoria - PIPE_HEIGHT / 2 - INTERVALO_CANO / 2, x: LARGURA_CANVAS + (PIPE_WIDTH / 2) }); + Body.setPosition(this.canoInferior, { y: alturaAleatoria + PIPE_HEIGHT / 2 + INTERVALO_CANO / 2, x: LARGURA_CANVAS + (PIPE_WIDTH / 2) }); + Body.setVelocity(this.canoSuperior, { x: 0, y: 0 }); + Body.setVelocity(this.canoInferior, { x: 0, y: 0 }); + console.log("JogoFlappy: canos reiniciados, altura", alturaAleatoria); + } + + private pular(corpo: Matter.Body) { + Body.setVelocity(corpo, { x: corpo.velocity.x, y: -4 * ESCALA }); + } + + private obterAleatorio(min: number, max: number) { + return Math.random() * (max - min) + min; + } + + private criarIndividuo(genes1?: boolean[]): Individuo { + // Usa sprite do pássaro + const corpo = Bodies.rectangle(50, 50, BIRD_WIDTH, BIRD_HEIGHT, { + inertia: Infinity, + collisionFilter: { category: this.categoriaJogador, group: -1 }, + label: "individuo", + render: { + sprite: { + texture: birdTexture, + xScale: 1, + yScale: 1, + }, + }, + }); + + Composite.add(this.engine.world, [corpo]); + + let genes: boolean[]; + if (genes1) { + genes = [...genes1]; + } else { + // Gera 88 bits aleatórios + genes = Array.from({ length: TOTAL_BITS }, () => Math.random() < 0.5); + } + + const individuo: Individuo = { corpo, genes, tempo: 0, estaVivo: true }; + this.populacao.push(individuo); + return individuo; + } + + private obterFitness(individuo: Individuo) { + return individuo.tempo - TEMPO_TREINAMENTO; + } + + private removerIndividuo(individuo: Individuo) { + individuo.estaVivo = false; + Composite.remove(this.engine.world, [individuo.corpo]); + // Se o indivíduo removido era o observado, seleciona outro vivo + if (this.observadoIndividuo === individuo) { + console.log("JogoFlappy: indivíduo observado morreu, selecionando próximo"); + this.selecionarProximoObservado(); + } + } + + // Seleciona um indivíduo vivo para ser observado pela UI. + // Atualiza a textura do antigo observado (volta à textura normal) e + // aplica a textura `blueBirdTexture` ao novo observado. + private selecionarProximoObservado() { + // limpa textura do observado anterior, se existir + if (this.observadoIndividuo) { + try { + (this.observadoIndividuo.corpo.render as any).sprite.texture = birdTexture; + } catch { /* ignore */ } + } + + const proximo = this.populacao.find(i => i.estaVivo) || null; + this.observadoIndividuo = proximo; + if (this.observadoIndividuo) { + try { + (this.observadoIndividuo.corpo.render as any).sprite.texture = blueBirdTexture; + } catch { /* ignore */ } + console.log("JogoFlappy: agora observando indivíduo", this.populacao.indexOf(this.observadoIndividuo)); + } else { + console.log("JogoFlappy: nenhum indivíduo vivo para observar"); + } + // notifica listeners sobre os pesos atuais do observado + this.notifyObservedPesos(); + } + + // Notifica todos os listeners registrados com os pesos atuais (ou null) + private notifyObservedPesos() { + const pesos = this.getObservedPesos(); + this.observedPesosListeners.forEach(cb => { + try { cb(pesos); } catch { /* ignore listener errors */ } + }); + } + + // Permite que UI se inscreva para receber atualizações de pesos do observado + public addObservedPesosListener(cb: (pesos: number[] | null) => void) { + this.observedPesosListeners.add(cb); + // envia o estado inicial imediatamente + try { cb(this.getObservedPesos()); } catch { } + return () => { this.observedPesosListeners.delete(cb); }; + } + + // Remove e readiciona o corpo do observado para que ele seja desenhado por último + // (desenhado acima dos demais) no canvas do Matter.Render. + private trazerObservadoFrente() { + if (!this.observadoIndividuo) return; + if (!this.observadoIndividuo.estaVivo) return; + try { + const world: any = this.engine.world; + const bodies: Matter.Body[] = world.bodies as Matter.Body[] || []; + if (bodies.length === 0) return; + const ultimo = bodies[bodies.length - 1]; + if (ultimo === this.observadoIndividuo.corpo) return; // já está na frente + // remover e readicionar move o corpo para o final do array de bodies + Composite.remove(this.engine.world, this.observadoIndividuo.corpo); + Composite.add(this.engine.world, this.observadoIndividuo.corpo); + } catch (err) { + console.log("JogoFlappy: erro ao trazer observado para frente", err); + } + } + + // Retorna os pesos (4 numbers) do indivíduo observado, ou null se não houver + public getObservedPesos(): number[] | null { + if (!this.observadoIndividuo) return null; + try { + return extrairPesos(this.observadoIndividuo.genes); + } catch { + return null; + } + } + + // Helper para atualizar texto em labels se existirem + private setTextById(id: string, text: string) { + const el = document.getElementById(id); + if (el) el.innerText = text; + } + + // Formata número com 2 casas decimais + private formatNumber(n: number) { + if (!isFinite(n)) return "0.00"; + return Number(n).toFixed(2); + } + + // Formata tempo em milissegundos para mm:ss + private formatTime(ms: number) { + const totalSeconds = Math.max(0, Math.floor(ms / 1000)); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + const mm = String(minutes).padStart(2, "0"); + const ss = String(seconds).padStart(2, "0"); + return `${mm}:${ss}`; + } +} + +/** + * Função de conveniência usada pelo componente React: cria JogoFlappy e retorna + * uma função de limpeza para parar/limpar quando o componente desmonta. + */ +export function iniciar(containerRef: RefObject) { + const jogo = new JogoFlappy(containerRef); + return { + stop: () => jogo.stop(), + resetGeracao: () => jogo.resetGeracao(), + }; +} + +// exporta tipo do controlador para consumidores +export type ControladorFlappy = ReturnType; diff --git a/app/games/flappy-bird-js/readme.md b/app/games/flappy-bird-js/readme.md new file mode 100644 index 0000000..5be698f --- /dev/null +++ b/app/games/flappy-bird-js/readme.md @@ -0,0 +1,143 @@ +# Flappy Bird — algoritmo genético + perceptron + +Este documento descreve, de forma didática, a parte central do agente que joga Flappy Bird neste repositório: o perceptron de uma camada (que decide quando pular) e o algoritmo genético (que evolui os pesos representados por genes). A implementação está em TypeScript e usa Matter.js para a física. + +Repositório: [includeDaniel/PlayAI](https://github.com/includedaniel/PlayAI) + +Arquivos principais (caminhos no repositório): + +- [Lógica do perceptron / genes](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/logics/ia.ts) +- [Lógica do jogo (orquestração, população)](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/logics/jogo.ts) +- [Física (Matter.js helper)](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/logics/fisica.ts) +- [Componente React que monta o canvas e labels](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/components/Flappy.tsx) +- [Página que integra o jogo](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/components/Page.tsx) + +--- + +## Resumo abstrato do fluxo + +1. O mundo é simulado pelo Matter.js (canos e pássaros são corpos físicos). +2. Cada agente (indivíduo) possui um vetor de genes (bits). Esses bits são convertidos em 4 pesos numéricos. +3. Em cada tick, para cada indivíduo, o perceptron calcula a soma ponderada das entradas ambientais e decide se o pássaro deve pular. +4. Indivíduos ganham tempo de sobrevivência enquanto vivem; quando todos morrem, o algoritmo genético cria a próxima geração por seleção e cruzamento. + +--- + +## Detalhes: representação de genes e pesos + +Cada indivíduo tem um vetor de bits chamado `Genes` (no código `Genes = boolean[]`). + +- São 4 grupos de 22 bits cada (total = 88 bits). Cada grupo representa um peso do perceptron. +- A conversão bits → número é feita por uma função que interpreta os 22 bits como um inteiro não assinado, normaliza para [0,1] e mapeia para [-1,1]. + +Trecho (implementação principal, veja o arquivo [logicas/ia.ts](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/logicas/ia.ts)): + +```ts +// converte 22 bits para número no intervalo [-1, 1] +function bitsParaNumero(bits: boolean[]): number { + // (implementação: soma de bit shifts, normalização e map para [-1,1]) +} + +// extrai os 4 pesos a partir dos 88 bits +function extrairPesos(genes: boolean[]): number[] { + const pesos = []; + for (let i = 0; i < 4; i++) { + const slice = genes.slice(i * 22, (i + 1) * 22); + pesos.push(bitsParaNumero(slice)); + } + return pesos; +} +``` + +Observação: usar bits permite representar com granularidade controle direto da codificação genética e realizar cruzamentos bit-a-bit facilmente. + +--- + +## Perceptron (decisão de pulo) + +O perceptron é uma única camada linear sem função de ativação complexa — a saída é apenas a soma ponderada das entradas comparada a zero. + +Entradas usadas no perceptron (ordem usada no código): + +1. Distância horizontal até o cano (x) +2. Y da abertura entre os canos (y) +3. Velocidade vertical do pássaro (vy) +4. Y do pássaro (y) + +Decisão (pseudocódigo): + +```ts +const pesos = extrairPesos(genes); // [w0, w1, w2, w3] +const entradas = [distancia, canoY, velocidade, passaroY]; +const soma = entradas.reduce((acc, v, i) => acc + v * pesos[i], 0); +const devePular = soma >= 0; // true => aplica impulso de pulo +``` + +Veja a função real: [logicas/ia.ts](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/logicas/ia.ts) + +Dica prática: a normalização das entradas (escala e offset) afeta muito o comportamento do perceptron — verifique como as entradas são calculadas em [logicas/jogo.ts](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/logicas/jogo.ts) para entender a escala usada no experimento. + +--- + +## Algoritmo Genético (fluxo e implementação) + +Visão geral do ciclo de vida genético: + +1. Inicializa uma população de N indivíduos com genes aleatórios (`criarGenesAleatorios`). +2. Executa a simulação por um tempo (indivíduos acumulam `tempo` de sobrevivência). +3. Quando todos morrem (ou chega o fim do ciclo), calcula-se fitness (aqui usado o tempo de sobrevivência como critério). +4. Gera nova população mantendo os mais aptos (elitismo) e preenchendo o restante com filhos gerados por cruzamento bit-a-bit. + +Pontos-chave do código: + +- Função que cria população aleatória: `criarGenesAleatorios()` — gera 88 bits aleatórios. +- Cruzamento: `cruzarGenes(g1, g2)` — para cada bit há probabilidade de herdar de pai A, pai B ou sofrer mutação. +- Seleção: os indivíduos são ordenados por fitness; os top K são mantidos (elitismo) e os demais são gerados por cruzamento entre os melhores. + +Trecho ilustrativo de cruzamento (pseudocódigo): + +```ts +function cruzarGenes(g1: Genes, g2: Genes): Genes { + const filho = []; + for (let i = 0; i < 88; i++) { + const p = Math.random(); + if (p < 0.45) filho.push(g1[i]); + else if (p < 0.9) filho.push(g2[i]); + else filho.push(Math.random() < 0.5); // mutação + } + return filho; +} +``` + +Implementação completa e parâmetros: [logicas/ia.ts](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/logicas/ia.ts) + +Como a aptidão é medida (fitness): + +```ts +function obterFitness(individuo) { + // Exemplo do projeto — usa tempo de sobrevivência + return individuo.tempo - TEMPO_TREINAMENTO; +} +``` + +O `TEMPO_TREINAMENTO` aparece no código de orquestração (`jogo.ts`) e é usado para calibrar a função de fitness. + +--- + +## Dicas para experimentação e tuning + +- Tamanho da população (`TAMANHO_POPULACAO`): maior população explora mais, mas custa performance. +- Taxa de mutação: aumente se a população estagnar, diminua se houver muita aleatoriedade. +- Normalização das entradas: centralizar e normalizar entradas (por exemplo, dividir distância por largura do canvas) melhora estabilidade do perceptron. +- Elitismo: manter uma pequena porcentagem dos melhores preserva soluções já boas. + +Parâmetros estão em: [logicas/jogo.ts](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/logicas/jogo.ts) + +--- + +## Onde olhar no código (rápido mapa) + +- [logicas/ia.ts](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/logicas/ia.ts) — conversão bits↔número, criar genes, cruzamento, extrair pesos e decisão do perceptron. +- [logicas/jogo.ts](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/logicas/jogo.ts) — mecânica do jogo (criação de corpos, loop, reinício de canos, população, seleção, fitness). +- [logicas/fisica.ts](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/logicas/fisica.ts) — helper que inicializa Matter.js (engine, render, runner). +- [componentes/Flappy.tsx](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/components/Flappy.tsx) e [componentes/Page.tsx](https://github.com/includedaniel/PlayAI/tree/main/app/games/flappy-bird-js/components/Page.tsx) — integração React/DOM, labels e botão de reiniciar. diff --git a/app/routes.ts b/app/routes.ts index 22f8f56..fae066e 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -1,3 +1,3 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes"; -export default [index("routes/Home.tsx"), route("/games/flappy-bird-js", "games/flappy-bird-js/Page.tsx")] satisfies RouteConfig; +export default [index("routes/Home.tsx"), route("/games/flappy-bird-js", "games/flappy-bird-js/componentes/Page.tsx")] satisfies RouteConfig;