Something different, but not for me personally.
Parsing has been used to describe the human activity of puzzling out meaning from a string of symbols for longer than electronic computers have been around.
Sure, the brain does not follow the same principles of formal grammars used in computer science, but I think the term "parsing" still applies.
The trouble I ran into was after eating the ghost, the spawn point of the new ghost was too close to my current position and simply sped over me.
With this and all the considerations you've noted above, I'd probably throw an ML solution at it - imagine it takes less coding time.
Coding that from nothing (and understanding it) could take days. Weeks to months if you dont know ml stuff.
In this particular case I think basic Q Tabular Learning could play optimally, no neural net required.
I could be wrong though, throw a full conv at it without temporal difference and see what happens! (probably a kinda okayish score)
This random exploration is less efficient than back propagation, but at least you don't have to muck around with temporal differences. (And it would be hard to figure out how to tweak the weights for backpropagation in this multi-step game; without falling back to reinforcement learning.)
The problem seems simple enough that evolution via mutation and artificial selection has a decent chance to get good in a reasonable amount of computing time.
But both approaches would be an interesting exercise in their respective domain!
Come to think of it, it might even be an interesting toy problem for a SAT solver! Overkill in the same way as an ML approach, of course.
function bot() {
/* direction of the enemy: to our right (1) or to our left (-1) */
dir = (enemy.x > player.x) ? 1 : -1;
nearWall = enemy.x < 10 || enemy.x > 90
/* if pac-man... */
if (
(
/* ...has no powerup or powerup expires in less than 10 "ticks" */
powerTicks < 10 &&
/* ...is headed toward enemy */ player.vx == dir &&
/* ...is too close to enemy */ abs(player.x - enemy.x)
/* ...the distance before running away can be lower if we're near an edge */
< (dir == 1 ? player.x / 7 : ((100 - player.x) / 7))+7 &&
/* and if enemy's state is not "eyes flying back" */
enemy.eyeVx == 0)
) {
// "ArrowUp" or any arrow key reverses the direction of pac-man
document.dispatchEvent(new KeyboardEvent('keydown', {code: 'ArrowUp'}));
document.dispatchEvent(new KeyboardEvent('keyup', {code: 'ArrowUp'}));
}
}
setInterval(bot, 10);[1] https://abagames.github.io/crisp-game-lib-11-games/pakupaku/...
In Chrome, right-click on the page -> View Source. Go to the "Sources" tab. When you open the tree on the left you find a file, that was dynamically loaded (so you wouldn't have found it by just looking at the HTML source of the main page), https://abagames.github.io/crisp-game-lib-11-games/pakupaku/... that seems to contain the game's code. And indeed it's less than 200 lines and implements the core game logic.
There is a main function, update(), containing a conditional "if (!ticks)" that seems to do initialization. "ticks" is probably zero at the start of the game. The code initializes multiple variables, including "player" and "enemy":
if (!ticks) {
player = { x: 40, vx: 1 };
enemy = { x: 100, eyeVx: 0 };
multiplier = 0;
addDots();
powerTicks = animTicks = 0;
}
Then with a bunch of console.log() statements added to the update() function (just edit the code and type Ctrl-S to save it, Chrome allows this sort of in-place code editing), I got a sense that player.x and enemy.x was the x position, with the scale going from 0 (left edge) to 100 (right edge). Then with more console.log() statements I found enemy.eyeVx was normally 0 when the enemy is a ghost or 1 or -1 if the ghost has been eaten and its eyes are flying respectively right or left.Some of the code is less readable, for example:
if (enemy.eyeVx === 0 && (c.a || c.b || c.c)) {
if (powerTicks > 0) {
play("powerUp");
addScore(10 * multiplier, enemy.x, 30);
enemy.eyeVx = player.x > 50 ? -1 : 1;
powerTicks = 0;
multiplier++;
} else {
play("explosion");
end();
}
}
I figured by literally typing "play(...)" in the dev console that these functions play a sound. Therefore based on the sound names "powerUp" and "explosion" I realized this code block is called when pac-man collides with a ghost. And either it has a power-up ("powerTicks > 0") or not, in which case it dies.At this point I had the meaning of "player", "enemy" and "powerTicks" and that's all I needed to write the bot.
eval(atob('ZnVuY3Rpb24gdXBkYXRlKCkgewogIGlmICghdGlja3MpIHsKICAgIHBsYXllciA9IHsgeDogNDAsIHZ4OiAxIH07CiAgICBlbmVteSA9IHsgeDogMTAwLCBleWVWeDogMCB9OwogICAgbXVsdGlwbGllciA9IDA7CiAgICBhZGREb3RzKCk7CiAgICBwb3dlclRpY2tzID0gYW5pbVRpY2tzID0gMDsKICB9CiAgYW5pbVRpY2tzICs9IGRpZmZpY3VsdHk7CiAgY29sb3IoImJsYWNrIik7CiAgdGV4dChgeCR7bXVsdGlwbGllcn1gLCAzLCA5KTsKICBpZiAoaW5wdXQuaXNKdXN0UHJlc3NlZCkgewogICAgcGxheWVyLnZ4ICo9IC0xOwogIH0KICBwbGF5ZXIueCArPSBwbGF5ZXIudnggKiAwLjUgKiBkaWZmaWN1bHR5OwogIGlmIChwbGF5ZXIueCA8IC0zKSB7CiAgICBwbGF5ZXIueCA9IDEwMzsKICB9IGVsc2UgaWYgKHBsYXllci54ID4gMTAzKSB7CiAgICBwbGF5ZXIueCA9IC0zOwogIH0KICBjb2xvcigiYmx1ZSIpOwogIHJlY3QoMCwgMjMsIDEwMCwgMSk7CiAgcmVjdCgwLCAyNSwgMTAwLCAxKTsKICByZWN0KDAsIDM0LCAxMDAsIDEpOwogIHJlY3QoMCwgMzYsIDEwMCwgMSk7CiAgY29sb3IoImdyZWVuIik7CiAgY29uc3QgYWkgPSBmbG9vcihhbmltVGlja3MgLyA3KSAlIDQ7CiAgY2hhcihhZGRXaXRoQ2hhckNvZGUoImEiLCBhaSA9PT0gMyA/IDEgOiBhaSksIHBsYXllci54LCAzMCwgewogICAgLy8gQHRzLWlnbm9yZQogICAgbWlycm9yOiB7IHg6IHBsYXllci52eCB9LAogIH0pOwogIHJlbW92ZShkb3RzLCAoZCkgPT4gewogICAgY29sb3IoCiAgICAgIGQuaXNQb3dlciAmJiBmbG9vcihhbmltVGlja3MgLyA3KSAlIDIgPT09IDAgPyAidHJhbnNwYXJlbnQiIDogInllbGxvdyIKICAgICk7CiAgICBjb25zdCBjID0gY2hhcihkLmlzUG93ZXIgPyAiZyIgOiAiZiIsIGQueCwgMzApLmlzQ29sbGlkaW5nLmNoYXI7CiAgICBpZiAoYy5hIHx8IGMuYiB8fCBjLmMpIHsKICAgICAgaWYgKGQuaXNQb3dlcikgewogICAgICAgIHBsYXkoImp1bXAiKTsKICAgICAgICBpZiAoZW5lbXkuZXllVnggPT09IDApIHsKICAgICAgICAgIHBvd2VyVGlja3MgPSAxMjA7CiAgICAgICAgfQogICAgICB9IGVsc2UgewogICAgICAgIHBsYXkoImhpdCIpOwogICAgICB9CiAgICAgIGFkZFNjb3JlKG11bHRpcGxpZXIpOwogICAgICByZXR1cm4gdHJ1ZTsKICAgIH0KICB9KTsKICBjb25zdCBldnggPQogICAgZW5lbXkuZXllVnggIT09IDAKICAgICAgPyBlbmVteS5leWVWeAogICAgICA6IChwbGF5ZXIueCA+IGVuZW15LnggPyAxIDogLTEpICogKHBvd2VyVGlja3MgPiAwID8gLTEgOiAxKTsKICBlbmVteS54ID0gY2xhbXAoCiAgICBlbmVteS54ICsKICAgICAgZXZ4ICoKICAgICAgICAocG93ZXJUaWNrcyA+IDAgPyAwLjI1IDogZW5lbXkuZXllVnggIT09IDAgPyAwLjc1IDogMC41NSkgKgogICAgICAgIGRpZmZpY3VsdHksCiAgICAwLAogICAgMTAwCiAgKTsKICBpZiAoKGVuZW15LmV5ZVZ4IDwgMCAmJiBlbmVteS54IDwgMSkgfHwgKGVuZW15LmV5ZVZ4ID4gMCAmJiBlbmVteS54ID4gOTkpKSB7CiAgICBlbmVteS5leWVWeCA9IDA7CiAgfQogIGNvbG9yKAogICAgcG93ZXJUaWNrcyA+IDAKICAgICAgPyBwb3dlclRpY2tzIDwgMzAgJiYgcG93ZXJUaWNrcyAlIDEwIDwgNQogICAgICAgID8gImJsYWNrIgogICAgICAgIDogImJsdWUiCiAgICAgIDogZW5lbXkuZXllVnggIT09IDAKICAgICAgPyAiYmxhY2siCiAgICAgIDogInJlZCIKICApOwogIGNvbnN0IGMgPSBjaGFyKAogICAgZW5lbXkuZXllVnggIT09IDAgPyAiaCIgOiBhZGRXaXRoQ2hhckNvZGUoImQiLCBmbG9vcihhbmltVGlja3MgLyA3KSAlIDIpLAogICAgZW5lbXkueCwKICAgIDMwLAogICAgewogICAgICAvLyBAdHMtaWdub3JlCiAgICAgIG1pcnJvcjogeyB4OiBldnggfSwKICAgIH0KICApLmlzQ29sbGlkaW5nLmNoYXI7CiAgaWYgKGVuZW15LmV5ZVZ4ID09PSAwICYmIChjLmEgfHwgYy5iIHx8IGMuYykpIHsKICAgIGlmIChwb3dlclRpY2tzID4gMCkgewogICAgICBwbGF5KCJwb3dlclVwIik7CiAgICAgIGFkZFNjb3JlKDEwICogbXVsdGlwbGllciwgZW5lbXkueCwgMzApOwogICAgICBlbmVteS5leWVWeCA9IHBsYXllci54ID4gNTAgPyAtMSA6IDE7CiAgICAgIHBvd2VyVGlja3MgPSAwOwogICAgICBtdWx0aXBsaWVyKys7CiAgICB9IGVsc2UgewogICAgICBwbGF5KCJleHBsb3Npb24iKTsKICAgIH0KICB9CiAgcG93ZXJUaWNrcyAtPSBkaWZmaWN1bHR5OwogIGlmIChkb3RzLmxlbmd0aCA9PT0gMCkgewogICAgcGxheSgiY29pbiIpOwogICAgYWRkRG90cygpOwogIH0KfQ=='))
when tired just call end()probably better to manipulate the score directly but its not fun that way :)
function update() {
if (!ticks) {
player = { x: 40, vx: 1 };
enemy = { x: 100, eyeVx: 0 };
multiplier = 0;
addDots();
powerTicks = animTicks = 0;
}
animTicks += difficulty;
color("black");
text(`x${multiplier}`, 3, 9);
if (input.isJustPressed) {
player.vx *= -1;
}
player.x += player.vx * 0.5 * difficulty;
if (player.x < -3) {
player.x = 103;
} else if (player.x > 103) {
player.x = -3;
}
color("blue");
rect(0, 23, 100, 1);
rect(0, 25, 100, 1);
rect(0, 34, 100, 1);
rect(0, 36, 100, 1);
color("green");
const ai = floor(animTicks / 7) % 4;
char(addWithCharCode("a", ai === 3 ? 1 : ai), player.x, 30, {
// @ts-ignore
mirror: { x: player.vx },
});
remove(dots, (d) => {
color(
d.isPower && floor(animTicks / 7) % 2 === 0 ? "transparent" : "yellow"
);
const c = char(d.isPower ? "g" : "f", d.x, 30).isColliding.char;
if (c.a || c.b || c.c) {
if (d.isPower) {
play("jump");
if (enemy.eyeVx === 0) {
powerTicks = 120;
}
} else {
play("hit");
}
addScore(multiplier);
return true;
}
});
const evx =
enemy.eyeVx !== 0
? enemy.eyeVx
: (player.x > enemy.x ? 1 : -1) * (powerTicks > 0 ? -1 : 1);
enemy.x = clamp(
enemy.x +
evx *
(powerTicks > 0 ? 0.25 : enemy.eyeVx !== 0 ? 0.75 : 0.55) *
difficulty,
0,
100
);
if ((enemy.eyeVx < 0 && enemy.x < 1) || (enemy.eyeVx > 0 && enemy.x > 99)) {
enemy.eyeVx = 0;
}
color(
powerTicks > 0
? powerTicks < 30 && powerTicks % 10 < 5
? "black"
: "blue"
: enemy.eyeVx !== 0
? "black"
: "red"
);
const c = char(
enemy.eyeVx !== 0 ? "h" : addWithCharCode("d", floor(animTicks / 7) % 2),
enemy.x,
30,
{
// @ts-ignore
mirror: { x: evx },
}
).isColliding.char;
if (enemy.eyeVx === 0 && (c.a || c.b || c.c)) {
if (powerTicks > 0) {
play("powerUp");
addScore(10 * multiplier, enemy.x, 30);
enemy.eyeVx = player.x > 50 ? -1 : 1;
powerTicks = 0;
multiplier++;
} else {
play("explosion");
}
}
powerTicks -= difficulty;
if (dots.length === 0) {
play("coin");
addDots();
}
}Given there aren't many sites that say "just open the console and paste this command to win the big prize", I suspect that any console-only API's aren't very powerful if they exist at all.
https://developer.chrome.com/blog/new-in-devtools-120#self-x...
As well as the usual engagement driving "challenges" like "Omg did you know there's no country starting with Z! Bet you can't think of one!" meanwhile comments are filled with "duuuuh Zanzibaaaar!" and post engagement is >>>>>>>>>>>>
(Admittedly I typically browse with JS disabled...)
And it's quite funny that JS was designed after Self/Smalltalk well before anyone cared about interactive tooling, towards which we evolved independently.
Alan Kay truly is a genius.
1) enemy is running away from player && power is about to expire && player is close to enemy.
2) player is getting too close to enemy's respawn point && player is too far from wall opposite to him && player has little or no power.
3) enemy is close to player (<20 units away) && the player and the enemy are facing each other.
A live preview of the algorithm in action is deployed on github[3].
[1]: https://github.com/creme332/pakupaku-ai
[2]: https://github.com/creme332/pakupaku-ai/blob/main/assets/hig...
The corner cases where pac-man dies is the game difficulty progressively increases (the ghost becomes faster) until you eat it, so sometimes some pellets are left in the middle and pac-man doesn't have enough time to eat them until the ghost reaches it. The ghost will progressively get faster and faster and death is guaranteed. You could improve the code by tempting the ghost to get close to one edge, then cross over to the other edge and quickly eat the middle pellets.
Also, as soon as new pellets are added, one should prioritize eating the middle pellets.
Also, one could add code to detect the powerup pellets, and chose to NOT move away from the ghost if it calculates it can eat the powerup pellet before the ghost reaches pac-man.