#ToolKitty
1 messages Β· Page 5 of 1
To sort of break up the runtime into logical chucks.
Nah... I'll leave that alone for now :p
i think that's a good choice
4000.... I was too slow... no controllable character yet π
mm? 4000?
Another cake π
That didn't take long
Good thing we're not in off topic doing this... ppl will go nuts.
I mean they'll get annoyed. :p
lol imagine
Forgot one prelude/solid-js/store
Done... globbed her that time.
Should them all... less painful :p
Ohh.... jsx might be a nice one to give access to.
Later job :p
Need to use a different name to onCleanup for user script clean ups. Because that is already taken by solid.
And make it optional too. Not required.
onUnload maybe
I went with an optional onLoad... now I'll leave it along.. getting carried away. :p
Sorry i mean onUnload as a replacement not onLoad.
Something that would be nice too is like a samples loader, so the user can try out a sample or start off with an sample to turn it into something else.
We could provider the user with a sample projects they can optionally use as well as a blank one.
made a small start on the collision system.
I am thinking about getting the collision system to create entities in the world to represent the result of the collisions (more of a pure ECS), just to cut down on the amount of infrastructure to add to the code.
The other systems then see the results in the collision result entities in the world and react to them.
Even an KeyboardInputSystem can store key press states as entities in the world for other systems to react to.
And... I'll probaby through in that chiptune player as a system too. For easy sound effects and music. But the user needs to supply their own music and sound effects files for those.
I should be careful with what files are in the github repo when we release.
Down the road. We'll need to add a music composer & sound effect tools. :p
This is also great for debugging. As the rendering system can be set to debug mode to show active collisions with green boxes.
For sprite / map cell collision, it's a simple matter of taking the corner coordinates of the sprites and doing an integer division by the cell size to find the cells the sprite collides with.
For sprite sprite collision, we might be able to do something clever. Like writing the cells collisions to a 2d array buffer and if a entry in an element already exists then those sprites may collide. (Heuristic)
It can make sprite / sprite collision close to O(n) instead O(nΒ²)
We can use https://github.com/bigmistqke/pianissimo as a start π
That makes more sense π€£
Interesting!
Absolutely, we can throw her in. If it's OK.
I think it's another common thing they do in ECS. (A proven strategy)
I think I have a way to escape the callback hell in the createComputeds in PixiRenderSystem.
I'll forward it here:
The k function of that particular monad is the continuation (what to do next).
The pipe method is the monadic bind operator.
Basically calling k in the current pipe executes the next pipe from with-inside itself.
If k from the current pipe is not executed, then the next pipe is not executed. (Earily bail)
If k is executed multiple times in the current pipe, then the chain of the next pipes is executed multiple times. (Multi-worlds / Green threads kinda) [Aka list monad]
It basically provides the power of callCC from lisp.
i could abstract the piano-roll as a component
solid-piano
That would be neat.
I'll come clean with that monad.
It's not my invention.
It's the Cont monad from Haskell.
never played around w Haskell
I did a lil lisp when I made a toy interpreter for it. Beautiful language from a compiler pov. It still baffles me that it's one of the earliest languages. So elegant and powerful.
I should play around w other languages more. It's good inspiration
Mb Cont a good name? Gets you extra dev points for knowing what a monad is π
Yeah... I was just making fun in off topic.
I asked for help there to escape callback hell for nested createComputeds
I was scratching my head for a long time when I first saw Cont. The code for it is super sort, but it was such a head spin still.
Here is where Cont is trippy in Haskell:
do
action1()
action2()
callCC (\k -> do
action3()
k()
k()
k()
)
action4()
action5()
In the code above action4 and action5 are executed 3 times each. (The k is everything after the callCC)
Resulting execution:
action1 action2 action3 action4 action5 action4 action5 action4 action5
And callCC is not native or built into Haskell. It's just a pure function that could be implemented in userland.
callCC can also return different values to the code afterwards. Allowing code to branch different paths afterwards. (Green threads)
Green threads = simulating multithreaded code with a single thread.
i remember pichu talking about those at some point
peculiar!
They are good for lightweight tasks, because they can spawn instantly (low overhead)
Implementing green threads in C is an absolute nightmare. And requires assembly language hacks around the compiled code.
And callCC comes with lisp. Very powerful tool.
Well Scheme anyway
I read article some time ago that callCC is so powerful it can be used to implement any monad in existence.
Also.... I read somewhere generators have a similar power, and javascript has native built in generators.
If a generator can also implement any possible monad (I'm unsure), then we can make an even cleaner solution to escape callback hell for deeply nested createComputeds.
I'll try asking AI. It will be difficult to find an article on it.
Google Gemini has come up with a way of implementing the Maybe monad using JavaScript generators:
const Maybe = {
of: (value) => ({
_value: value,
isNothing: false,
flatMap: function(fn) {
return this.isNothing ? Maybe.nothing() : fn(this._value);
},
}),
nothing: () => ({
_value: null,
isNothing: true,
flatMap: function() {
return this;
},
}),
};
function* calculate(xMaybe, yMaybe) {
const x = yield xMaybe;
const y = yield yMaybe;
return Maybe.of(x + y);
}
function runMaybe(generator) {
const iterator = generator();
let current = iterator.next();
while (!current.done) {
if (current.value.isNothing) {
return Maybe.nothing();
}
current = iterator.next(current.value._value);
}
return current.value;
}
const just5 = Maybe.of(5);
const just10 = Maybe.of(10);
const nothing = Maybe.nothing();
console.log(runMaybe(calculate(just5, just10))); // Output: { _value: 15, isNothing: false, flatMap: [Function: flatMap] }
console.log(runMaybe(calculate(just5, nothing))); // Output: { _value: null, isNothing: true, flatMap: [Function: flatMap] }
It's quite interesting.
U can see in the calculate function, the AI eliminated the need for pipe / bind / then fluent interface function calls. Effectively making a do-notation like thing that can be seen in Haskell.
What would be interesting is if this same thing can be done for the Cont monad with generators. If it is possible, then we will suddenly have super powers.
Google Gemini came up with this example:
function runCont(generator) {
const iterator = generator();
function step(val) {
const result = iterator.next(val);
if (result.done) {
return Cont.of(result.value);
}
return result.value.flatMap(step);
}
return step();
}
function* computationWithCallCC() {
const result = yield Cont.callCC((k) => {
console.log("Inside callCC");
setTimeout(() => k(42), 500);
return Cont.of("Initial value");
});
return `Result from callCC: ${result}`;
}
runCont(computationWithCallCC).run(console.log);
// Output:
// Inside callCC
// (after 0.5 seconds)
// Result from callCC: 42
Quite amazing... so we do have the power of callCC in javascript.
We should do a clean implementation of this in ToolKitty and use it to manage our callback hell for PixiRenderSystem and the CollisionSystem (getting the same callback hell in it too.)
Here is the full chat with Gemini:
https://g.co/gemini/share/15be460f0410
Oh dear... AI might be hallucinating. Struggling with getting the implementation to type check in TypeScript. :p
I mean.. not all valid javascript code is typecheckable. You can do some wild stuff w js.
True.. I'll copy some of its code to a playground to see if it works.
Last 10 Gemini attempts fail :p
Maybe using generators to make a nice do-like syntax for any monad is impossible in javascript.
Hold on a minute ths guy has done it:
https://gist.github.com/yelouafi/13ad4335f92e448f625930bb390da532
He even demoed coroutines (green threads)
Just ignore all the type errors and see the console output:
https://playground.solidjs.com/anonymous/015f6196-e8c1-4440-91b1-4d354161b6e5
Maybe I should of googled 1st, instead of going directly to AI.
Someone even wrote a full article:
https://dev.to/yelouafi/algebraic-effects-in-javascript-part-2---capturing-continuations-with-generators-13da
He also goes implementing the async (monad) with it, that way take for granted today.
Even try / catch implemented by callCC.
There is a lot of language constructs that the callCC primitive can implement.
And he has implemented breakpoints via callCC lol
Debugger primitives via callCC
Nice, imma read it!
Still makes my head spin π
I keep having to reread the code over and over to wrap my head around it.
Hahah ok good I m not the only one π
One thing with iterators too. They seems to form a message pipe. Similar to postMessage, but in the same local thread.
It might be an easier way to think about them.
yield switches execution from the generator back to the caller while also passing a message.
next() switches execution from the caller back to the generator while also passing message.
A generator itself is kinda like a simple version of a coroutine.
I wonder if it's possible to swap it around, so instead of the users code getting polluted with generator functions and yield, the user will be passed a generator and just keep calling next in their code. And the execution stepper use yield instead of next.
No because yield can only be executed directly within the generator function itself, and not a callback within the generator.
When I get PC access next. I'll have a go at making that single indent level nested createComputed using generators.
Can't wait for that... here we go Termux :p
God... why do I keep reaching for TypeScript for things that are impossible to type check π
Just gonna end up with any everywhere
I'll give generates a miss for now.
I can not seems to expose a type safe interface to the end user. (The user also being myself in this case.)
It's a shame really...
Imagine if javascript had built in support for callCC with proper typescript typing.
They'd never have to add another language feature, because callCC can do it all. (It can even do goto)
But I will still go ahead with our fluent interface version of Cont that uses pipe / then. It's type safe.
Probably not too bad:
import { createComputed } from "solid-js";
import { createSignal } from "solid-js/types/server/reactive.js";
export type Cont<A> =
((k: (a: A) => void) => void) & {
then: <B>(fn: (a: A) => Cont<A>) => Cont<B>,
};
export function makeCont<A>(f: (k: (a: A) => void) => void): Cont<A> {
let cont = {
...f,
then: <B>(fn: (a: A) => Cont<B>): Cont<B> =>
makeCont((k) => cont((a) => fn(a)(k))),
} as Cont<A>;
return cont;
}
let [ value1, setValue1, ] = createSignal(1);
let [ value2, setValue2, ] = createSignal(1);
let [ value3, setValue3, ] = createSignal(1);
function example() {
makeCont<number>
((k) => {
createComputed(() =>
k(value1() + 5)
);
})
.then<number>((a) => makeCont<number>((k) => {
createComputed(() =>
k(a + value2() * 2)
);
}))
.then<number>((a) => makeCont<number>((k) => {
createComputed(() =>
k(a + value3() - 1)
);
}))
(() => {});
}
(the 3 createComputed are nested inside each other via the continuation k, but their indent level in the code remains the same)
Maybe I can throw in a thenComp as well as the then to combined the a and the k together as two parameters to a lambda to cut down on the noise.
I think this looks better now:
import { createComputed, createSignal } from "solid-js";
export type Cont<A> =
((k: (a: A) => void) => void) & {
then: <B>(fn: (a: A) => Cont<A>) => Cont<B>,
thenCont: <B>(f: (a: A, k: (a: A) => void) => void) => Cont<B>,
run: () => void,
};
export function makeCont<A>(f: (k: (a: A) => void) => void): Cont<A> {
let cont = {
...f,
then: <B>(fn: (a: A) => Cont<B>): Cont<B> =>
makeCont((k) => cont((a) => fn(a)(k))),
thenCont: <B>(fn: (a: A, k: (a: B) => void) => void): Cont<B> =>
makeCont((k) => cont((a) => fn(a, k))),
run: () => cont(() => {}),
} as Cont<A>;
return cont;
}
let [ value1, setValue1, ] = createSignal(1);
let [ value2, setValue2, ] = createSignal(1);
let [ value3, setValue3, ] = createSignal(1);
function example() {
makeCont<number>
((k) => {
createComputed(() =>
k(value1() + 5)
);
})
.thenCont<number>((a, k) => {
createComputed(() =>
k(a + value2() * 2)
);
})
.thenCont<number>((a, k) => {
createComputed(() =>
k(a + value3() - 1)
);
})
.run();
}
A lot more compact in the example. And type safe.
Actually... I will change it back to a class since it is using a run method at the end. No advantage to overwriting function properties.
Here she is in action. Using the Cont monad to visually remove nesting from createComputed in the code while they still are nested computationally.
https://github.com/clinuxrulz/solid-kitty/blob/4ba97e9e4a0294c7e8e681a801554cadbca5426d/src/systems/PixiRenderSystem.tsx#L183
The type parameter to thenCont I was hoping typescript can infer it for me so I don't have to provide the types for the passing state. Maybe it is still possible with some type magic to get it to infer for us.
The only other change is I threw in a ScaleComponent (they are no longer flees):
And I think the fluid interface version of Cont is more performant than the generator version of Cont, so I am not too disappointed with not going the generator way due to insufficient type safety.
Yes, I have heard before too that generators have some perf issues. The .then syntax looks nice!
I'll have to run the termux visibility test later 2nyt... see if its easier to read :p
I suppose I can go 2 space tabs and shorten up a few variable names.
4 space is most common though
Not falling off the screen as badly, due to the indentation level remaining fixed.
Callback hell solved.
If javascript did support proper do-notation, then that example would look like this:
function example() {
do*(contMonad) {
let a <- callCC((k) => createComputed(() => k(value1() + 5)));
let b <- callCC((k) => createComputed(() => k(a + value2() * 2)));
createComputed(() => k(b + value3() - 1);
}
}
It is more compact. But not by much ig.
Is 4 spaces more common? Thought it felt pretty big the indentation when I looked at the source. Prettier does 2 spaces by default
2 the default? I thought IDEs defaulted to 4. π
Oh well. I'd be happy with 2 for side projects.
And will fit more tabs in termux.
Combining callCC + createComputed like that looks like createMemo
Ohh.... using .then seems shorter than .thenCont. Because in .then typescript is able to infer type parameter B, leading to less typing.
Typescript type checker can not infer B on it's own here:
thenCont<B>(fn: (a: A, k: (b: B) => void) => void): Cont<B>
But it can here:
then<B>(fn: (a: A) => Cont<B>): Cont<B>
We can reduce a lot of typing, if we do not need to tell the type system what B is:
.thenCont<{
pixiApp: Application<Renderer>;
levelsFolder: AutomergeVfsFolder;
lookupSpriteSheetFromTextureAtlasRef: (textureAtlasRef: string) =>
| Spritesheet<{
frames: Dict<SpritesheetFrameData>;
meta: {
image: string;
scale: number;
};
}>
| undefined;
levelRefEntities: Accessor<string[]>;
}>(
Initially I though thenCont would be shorter, but it's not π
ye that's pretty chunky lol
If it was Rust or Haskell, the type system would be able to infer B for thenCont. But not Typescript.
I feel silly now. I should of realised that sooner. π
Actually no sorry.... TypeScript still can not infer B.
Because kontinue is being called in a lambda.
Not solved yet.
would it be because that <B> is a generic of the method?
a no it returns Cont<B>
still trying to wrap my head around the code π
In the case of Array.map, B can be inferred, because B is the return type of the lambda.
Where ever you kontinue, it is copy pasting the code in the next then block in the place of kontinue.
It's like our own syntactic sugar.
where is kontinue coming from?
Oh sorry... k in that example. kontinue in PixiRenderSystem.
gotcha!
kontinue even works from inside a mapArray, it's like our own version of reactive green threads.
Though... solid doesn't need green threads, everything conceptually updates in parallel.
It's just a copy paste syntactic sugar is probably the best way to view it.
mm ye ig it's kind of impossible to get B inferred
I'll keep trying. It would avoid a lot of typing. Especially in Termux.
What if I created a wrapper over createComputed to help infer B
To try and get B in a return position of a lambda.
E.g. instead of calling kontinue, it can return B
ye i think something like this could work!
Typescript hates me π€£ :
https://playground.solidjs.com/anonymous/dbc0f90f-4907-414e-b415-32579e7d7a71
I got B in a return position of a lamba but still no go.
Ohh.... because it's still in a function body. Still more needs to be done.
I might need a .thenContCreateComputed
Well... createMemo is basically createComputed but returns something we can infer B on. Maybe there is a trick there.
Nope... that won't work... bcuz mapArray :p
Unless a thenContCreateComputed and a thenContCreateComputedMapArray methods are added.
what about something like thenCont(createMemo)(...)?
Having a go at that. The types are tricky.
Feels like trying to solve algebra
We know typescripts type system is Turing complete. So something gotta give. π
Just gotta get a B on the right hand side of a lambda. Just like we try and get a single x of the left hand side when solving algebra.
I got B in a return position of a lambda now. But still not there:
https://playground.solidjs.com/anonymous/3ab73351-2b74-4eee-90dd-d53058dc89b5
I use the identity function to help infer B
what about something like this ?
Quickly discover what the solid compiler will generate from your JSX template
(full disclosure: still don't really get Cont π€£ )
Your idea is working... i think
That very clever!
Its a copy paste monad π
It copy pastes code blocks in for you so your code isn't indented too far.
Well done @zenith void !! π
It seems correct
i wonder if it couldn't be expressed as a series of nested memo's instead of computeds π€
and that you return instead of call next
(not sure if i m being clear here)
Depends on the use case.
Mine I did not want the whole map to reload if only one cell changes.
createComputeds inside createComputeds to keep the reactivity fine grain. Because we are rendering.
but wouldn't that be the case right now too?
or... be the same
The same... if nested memos
like if the first in the chain is called it will re-trigger all of them, if it's the leaf it will only be the leaf that is re-called
Bcuz if nested we are not using the return value.
Thats absolutely correct.
We're basically doing JSX without JSX.
All the way down to sprite positions. So if mario moves, then it only updates the coordinates of the pixi sprite and nothing else.
Because of the deep nesting of createComputed. It created the need to find an abstraction to reduce the visual nesting.
Interesting... what is this <const B> magic? π
i meant something like this
Quickly discover what the solid compiler will generate from your JSX template
a it wasn't needed for this example!
but it can be really handy feature: https://www.typescriptlang.org/play/?#code/GYVwdgxgLglg9mABAD0QHgggzlRAVAPgAoBDALnwEpEBvAXwChEUiAiACxIBsu5XKGAekEA9APxA
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
function x <const T>(a: T) {}
x("hallo")
T here isn't string but "hallo"
Ohh... not quite there. k / kontinue needs to be able to be called 0 or more times.
E.g. mapArray inside createMemo
especially handy for tuples and the like x([0, 1 , 2]) instead of x([0, 1, 2] as const)
could u make up a small example with the mapArray?
... not small, but I am on a mobile π
gotcha
So kontinue is being called in mapArray
I think we're super close though. We have a solution for a single createComputed.
ig this one also won't work with the mapArray right?
No, because innerK needs to be inside the callback instead of outside.
Ohh... shoot maybe it does work.
I'll copy paste your thenContInfer function here for a closer look.
Which is proving hard, bcuz monaco doesn't let mobiles select text π
thenContInfer<const B>(
cb: (fn: () => void) => void,
fn: (a: A) => B,
): Cont<B> {
return this.then((a) => Cont.of((innerK) => cb(() => innerK(fn(a)))));
}
Yeah... not quite, but close.
A needs to be accessible outside of mapArray.
we need to get u a nice repl w codemirror and that typescript lsp.
Eventually.
when's ur bday? π
Quickly discover what the solid compiler will generate from your JSX template
Tried to find ur b-day 1st:
https://m.facebook.com/bigmistqke/
yay, the origin story
bigmistqke was a design studio my buddy and i started when we were studying π
together w misha gurovich very talented 3d artist
Not sure what to think... it does work, but it is also no longer a continuation monad.
Very cool π
true, but mb that's not a bad thing?
why i was thinking of memos too is that chaining computeds isn't the most performant if u compare it w nesting memos, because you break the reactive graph into different pieces
nvm that was incorrect
because you are not doing effect -> signal -> effect
All good π
it's effect -> effect -> signal
No, not a bad thing. Just different.
so then mb it IS more performant to do the chaining computeds π€
I thought so too. But depends on use case.
ye kind of interesting!
When solid renders jsx, it is basically chaining computeds
although no, thinking a bit more, it does break up the graph into pieces
it is
const [value, setValue] = createSignal()
const [doubleValue, setDoubleValue] = createSignal()
createComputed(() => setDoubleValue(value() * 2))
vs
const [value, setValue] = createSignal()
const doubleValue = createMemo(() => value() * 2)
In pixi we have no signals to set too. We're directly mutating pixi.
a i see
Mario moves... the pixi sprite coordinates change.
No recreation of pixi objects
I really like ur first solution:
thenContInfer<const B>(
cb: (fn: () => void) => void,
fn: (a: A) => B,
): Cont<B> {
return this.then((a) => Cont.of((innerK) => cb(() => innerK(fn(a)))));
}
I wonder if it can be tweaked some more.
does it need the cb actually? or can we always presume it will be createComputed?
a la
thenContInfer<const B>(
fn: (a: A) => B,
): Cont<B> {
return this.then((a) => Cont.of((innerK) => createComputed(() => innerK(fn(a)))));
}
Yeah... and then fn needs to receive innerK so it can be used inside mapArray
but then we are back to square 1 right?
Yeah. That is where it falls apart. A recursive type dependency.
i think if we wanna make the typescript gods happy we'll need to return the type
Well... not all is lost. We can do:
.thenContCreateComputed
and
.thenContCreateComputedMapArray.
Problem solved. π
It's wordy. Yes. But not as wordy as filling in the type parameter B.
.thenCCC / .thenCCCMA.
type ContinuationType<F> = F extends (a: any, k: (b: infer B) => void) => void ? B : never;
Let's make ourselves the TypeScript gods π
type B = ContinuationType<typeof fn>;
I'll have sleep on it though... that's a last ditch effort π
They got DOOM to run on the type systems... and we just wanna extract the type of B π
Do we dare implement a better typescript inside typescript type system and run it inside itself.... bad idea I know.
That approach seems won't work. The type information for k is not there until it's called.
ye, i think the problem is trying to get the type from its usage inside the function body
i think that just will not work
it would also be a bit weird if that would work
mb back to generators? thenCont(function* (a) { while (true) { yield a * 2 }})?
What if ContinuationType was used at the call site instead of in the thenCont method?
(nvm)
All good.
could u elaborate?
I just need a PC :p
It's probably not possible still, just throwing stuff at the wall to see what sticks.
Thats what I had in mind:
.thenCont(function(a, k) {
type B = ContinuationType<typeof this>;
createTypedComputed<B>(() => a + value2() * 2, k);
})
Doesn't work though.
But here is something that does work:
|| \\ @ts-ignore ||
Another approach might be whar they call a list monad transformer. We basically end with:
.then((a: A) => Accessor<B[]>): Accessor<B[]>
where ListMonadTransformer<A> is the type Accessor<A[]>
Just exppanding its type out in .then.
It kinda resembles one of the unioned types in JSX.Element.
... solid-universal and just render with JSX π... yes we could do that.
It's a transformer because it takes a memo monad and applies it to a list monad to make a new monad.
Accessor<B[]> is effectively like running k zero or more times.
Actually JSX is more suitable because it makes a tree structure.
JSX.Element can made of many JSX.Element.
To be fair. JSX can become deeply nested too. (Visually in the code editor.). Like a different kind of callback hell.
It's really a type system problem we are facing. I am happy enough just to manually fill in <B> for now.
true
ye, it really is
the limitations of ts
Maybe I am inferring the B wrong.
Maybe this conceptually:
.then<Z>(fn: Z): infer off of Z
So we can just determine a generic type straight off a single input variable.
And B is somewhere hidden in Z that we infer off.
That didn't work either :p
Back to this approached I think (specialized constructors). I was trying different kinds of things for the entire day with no luck.
Thats hard too π
I'll just fill in type B manually for a while.
I must be out of my mind. I keep saying I am giving up, but then I keep trying again π
I got this specialized constructor that seems to do the trick:
thenContCC<B>(fn: (a: A) => Accessor<B[]>): Cont<B> {
return this.thenCont((a: A, k: (b: B) => void) =>
createMemo(mapArray(
fn(a),
(b: B) => k(b),
))
);
}
that will be usable in most cases
even suits a regular Accessor<A | undefined>, because A | undefined can be represented by A[].
Specialized constructors demo:
https://playground.solidjs.com/anonymous/a2e5325e-3a6f-49ad-befd-4a76a2996824
You can see it is returning an Accessor<B[]> each of the Bs is feed through a k via mapArray.
(conceptually returning an array of things to feed to k)
I have tried out the specialized Cont constructors on the beginning of the CollisionSystem (unfinished):
You can see that all those <B> types giving us grief before are now all fully inferred.
I think I can improve it a bit more. There is an unnecessary mapArray in thenContCC that can more or less have it's lambda fused with the users mapArray, reducing the number of mapArrays.
The funny thing is. It will be creating memo that are never read, just to please the type checker.
Power Tools π§:
static ofCC<A>(a: Accessor<A[]>): Cont<A> {
return Cont.of((k: (a: A) => void) =>
createMemo(mapArray(a, (a: A) => k(a))),
);
}
map<B>(fn: (a: A) => B): Cont<B> {
return Cont.of((k: (b: B) => void) => this.fn((a) => k(fn(a))));
}
filterNonNullable(): Cont<NonNullable<A>> {
return Cont.of((k: (a: NonNullable<A>) => void) =>
this.fn((a) => {
if (a !== undefined && a !== null) {
k(a);
}
}),
);
}
then<B>(fn: (a: A) => Cont<B>): Cont<B> {
return Cont.of((k) => this.run((a) => fn(a).run(k)));
}
Those 4 together can do just about anything.
filter shows Conts stream/generator like side.
lool
that's with the rabbit holes, once you are in them you can't go out
i mean... if you are gonna create memos anyway, it kind of defeats the benefit of not going for something simple like #1344274340484616244 message
That is true.
However filterNonNullable is able to handle skipping actions (early bail). Without creating another memo.
And map can transform an action without creating another memo.
Cont
.ofCC(characters)
.map((c) => c.is_goomba ? c : undefined)
.filterNonNullable()
.run((c) => move(c))
looks elegant!
It's like an action stream ig
Also we are never reading the memo, we're using the memo like a createComputed.
The return value is just there to please type inference for the type checker. So that we do not need to fill in <B>.
If TypeScript's type checker was better, then it would still be createComputeds.
I'll remove thenCC and thenCCCC soon I think (but keeping then). So we have no createMemo or mapArray inside the Cont class. Those other ones seem like they are not required.
Cont can also behave like a promise. Where k is the resolve function. To handle reject, you just return a Result type through k. Then filter out the err case from the actions. (Redirect error case to be logged somewhere)
We could lift async stuff into it viaofPromise / liftPromise. And it will interact with the other stuff as if there was no async.
Maybe lift is a better word.
Cont.liftAccessorArray, Cont.liftAccessor, Cont.liftPromise, Cont.liftArray, ....
(For constructing a Cont from other things)
Cont is only about how to kontinue.
For the hell of it. Let's do a:
static Cont.liftComputed<A>(...)
For good measure.
Where u must supply <A>.
(As an extra)
Maybe could do some type hackery instead of adding an extra signal?
A lil treat
I'll have more of a play later.
Typescript's type inference is actually pretty good. It's a bit much to expect it to infer a type from a usage point.
Soon there will be no createMemo or mapArray inside Cont class. It will all be lifted from existing signals in the users code.
ye exactly, it's kind of what you don't want in 99.999999999999999% of the situations
Would be nice if javascript received syntactic sugar for all monads, instead of just Promises, which are a single type of monad anyway.
And generators are a type of monad too.
It feels like they keep redoing the same work for each specific cases, instead of doing it once as a general case.
Before syntactic exists in js for promises. TypeScript type inference would not for them.
Promise<A>(k: (resolve: (a: A) => void, reject: (e: any) => void))
Look familiar? That's a Cont.
Cont almost can be implemented by a Promise. But the promise is not capable of executing straight away, if we need it to. It would be kind of like a microTask execution-wise.
Typescript type inference works well with promises using when await. await is equivalent to our then.
One thing that would be interesting is a Cont.lower...().
E.g.
let b =
Cont
.liftAccessor(a)
.lowerPromise();
(You just converted a memo into a promise.)
let c =
Cont
.liftPromise(b)
.lowerAccessor();
(You just converted your promise back into a memo.)
I didn't get that far tonight:
Promise to Cont:
static liftPromise<A>(a: Promise<A>): Cont<Result<A, any>> {
return Cont.of((k: (a: Result<A, any>) => void) =>
a.then((b) => k(ok(b))).catch((e) => k(err(e))),
);
}
Instead of getting the user to make their own computed, I get them use/let Cont make their computed. It was the only way I could make the type inference work:
static liftCC<A>(fn: EffectFunction<undefined | NoInfer<A>, A>): Cont<A> {
return Cont.of((k: (a: A) => void) =>
createComputed(() => {
k(fn(undefined));
}),
);
}
Likewise for createComputed(mapArray(:
static liftCCMAA<A>(a: Accessor<A[]>): Cont<A> {
return Cont.of((k: (a: A) => void) => createComputed(mapArray(a, k)));
}
Long story short. There will be the same number of signals before using Cont as after using Cont. But those signals will be constructed inside the Cont class instead of user code to please type inference.
Accessor to Accessor, using Cont to handle earily bail. The pattern can also be used to ease conversion between memos and signals while we wait for solid 2.0.
let pageSnapPoints =
Cont
.liftAccessor(() => modeParams.paperSpaceWorld)
.then((world) =>
Cont
.liftAccessor(() => modeParams.currentPageEntityId)
.filterNonNullable()
.then((pageId) => Cont.liftAccessor(world.hookupEntityComponent(pageId, pageComponentType)))
.filterNonNullable()
)
.map((page) => {
let pageState = page.state;
return [
Vec3.zero,
Vec3.create(0.5 * pageState.width, 0.0, 0.0),
Vec3.create(pageState.width - 2.0 * pageState.margin, 0.0, 0.0),
Vec3.create(pageState.width - 2.0 * pageState.margin, 0.5 * pageState.height, 0.0),
Vec3.create(0.0, pageState.height - 2.0 * pageState.margin, 0.0),
Vec3.create(0.5 * pageState.width, pageState.height - 2.0 * pageState.margin, 0.0),
Vec3.create(pageState.width - 2.0 * pageState.margin, pageState.height - 2.0 * pageState.margin, 0.0),
Vec3.create(0.0, 0.5 * pageState.height, 0.0),
];
})
.lowerAccessor([]);
We can auto convert between any lift and lower combination.
Cont can behave as an IR (intermediate representation) for several different constructs, allowing conversation between them.
Here's the code for lowerAccessor:
lowerAccessor(): Accessor<A | undefined>;
lowerAccessor(initValue: A): Accessor<A>;
lowerAccessor(initValue?: A): Accessor<A | undefined> {
let value: A | undefined = initValue;
let result = createMemo(() => {
this.run((nextValue) => { value = nextValue; });
return value;
});
return result;
}
If there happens to be any reactive things in the chain, lowerAccessor will capture it in its resulting memo.
Since a Cont can bail somewhere the resulting memo can contain a value that is undefined. Just like a promise that is still pending, when converted to a memo, the memo value will be undefined while the promise is pending.
Let's pretend we have 6 constructs (lifts and lowers).
6 x 5 / 2 = 15... 15 conversion functions for free.
Construct examples: try/catch, promises, generators, coroutines (aka green threads), signals, streams, iterators, arrays, nullable/undefinable values (the Maybe monad), normal values (the Identity monad), ...
We can also trampoline to avoid stack overflow for a deep call stack.
A lot of fp languages have something called tail call elimination. Where if a function call is in the tail of the function it is executed in, then it is converted into a GOTO. Thus not consuming additional stack space as the call goes deeper.
In many cases it can converted a recursive function into a while loop basically.
We won't do the many conversion thing though. It's not our goal π... our goal is just to minimise callback hell in deep signal nested for use in kitty's systems.
pretty neat ngl!
All the nice things we could of had immediately in javascript, if only they supplied syntactic sugar for monads in general instead of just Promises. π
I guess this opens the door for Arrows:
https://github.com/tc39/proposal-pipeline-operator
Then guy has a do-notation from a generator:
https://curiosity-driven.org/monads-in-javascript#do
However we loose type safety with the generator. (Messages it sends are of a single type only.)
Plus generators can only do single shot continuations. (Can't do all monads)
Arrows are quite interesting too:
https://hackage.haskell.org/package/base-4.21.0.0/docs/Control-Arrow.html
There is not even a proposal for do-notation in js that I can find.
Oh... fluent interfaces and squint eyes will do for now. :p
Oh well.... enough FP talk. I once used PureScript (like Haskell, but for the web), but I abandoned it due to performance reasons. Abstraction are great, but they can all have overhead.
Could be fun to write a proposal π getting a feature into js is like a 1_000_000 dev points at least
PixiRenderSystem has been updated to suit the changes to Cont:
All types for <B> are inferred all the way down.
Would be easier to do transpiler plugin for it mb.
Cont is very close to a Promise too, but unlike Promise, the same resolve / k can be called multiple times.
Can almost use await as sugar for Cont, but not quite.
I'll throw this in later for good measure. Might be fun to play with green threads.
callCC :: ((a -> ContT r m b) -> ContT r m a) -> ContT r m a
callCC f = ContT $ \ c -> runContT (f (\ x -> ContT $ \ _ -> c x)) c
(Haskell code to translate to TypeScript)
ContT is a monad transformer to add Cont on top of any monad. But we'll only be interested in Cont.
This one is a head spin xD :
static callCC<A,B>(fn: (k: (a: A) => Cont<B>) => Cont<A>): Cont<A> {
return Cont.of((k) => fn((a: A) => Cont.of((_) => k(a))).run(k));
}
What a contraption π
I used to understand it. But seems I forgot it. And just spend hours trying to understand it again. :p
So basically k here is the next instruction address after callCC.
What we can do is store that k in an circular buffer, and kick off a different k... and that is what allows us to do green threads... I think.
Gotta find that article again so I can steal code to play with. :p
I'll reread this.
The example from the article:
function* main() {
yield fork(proc("1", 4));
yield fork(proc("2", 2));
yield dequeue();
console.log("end main");
}
function* proc(id, n) {
for (let i = 0; i <= n; i++) {
yield sleep(1000);
console.log(id, i);
yield pause;
}
}
const processQueue = [];
function fork(gen) {
return next => {
processQueue.push(
(function*() {
yield gen;
yield dequeue();
})()
);
next();
};
}
const pause = callcc(function*(k) {
processQueue.push(k());
yield dequeue();
});
function* dequeue() {
if (processQueue.length) {
const next = processQueue.shift();
yield next;
}
}
Ouput:
1 0
2 0
1 1
2 1
1 2
2 2
1 3
1 4
end main
(His yield is our then)
So pause holds the magic here. It uses callCC to swap out the next instruction address with a circle buffer of threads.
Looks quite simple with an example.
Back to collision detection system when I get home. I'm getting side tracked. π
I can expose Cont through kitty's prelude and play with green threads in user scripts.
Some of that fp stuff is addictive. And is not really leading me towards the goal.
I know what is going on with the size tailwind. Because kitty is exposing itself as a library for itself, tailwind can not strip out anything incase it gets used for a user script.
π ... i press c instead of ctrl+c and wiped out all my code in user script. (No undo) π
I think I'll wait for PC access to do it again.
Here we go, green thread demo:
https://youtu.be/kTYqkIc2F0I
Full code:
import { Cont } from "prelude";
let div = document.createElement("div");
div.style.setProperty("position", "absolute");
div.style.setProperty("left", "20px");
div.style.setProperty("top", "20px");
div.style.setProperty("width", "300px");
div.style.setProperty("z-index", "100");
div.style.setProperty("background-color", "rgba(0,0,0,0.7)");
setTimeout(() => {
document.body.appendChild(div);
main().run();
}, 3000);
export function onUnload() {
document.body.removeChild(div);
}
function main(): Cont<void> {
let [_, z] = do_();
_(fork(proc("A", 4)));
_(fork(proc("B", 2)));
_(dequeue());
_(print("done main."));
return z();
}
function proc(id: string, n: number): Cont<void> {
let [_, z] = do_();
for (let i = 1; i <= n; ++i) {
_(sleep(1000));
_(print(`${id}: ${i}`));
_(pause());
}
return z();
}
function do_(): [(k: Cont<void>) => void, () => Cont<void>] {
let result: Cont<void> = Cont.of((k) => k());
let _ = (inst: Cont<void>) => {
result = result.then((_) => inst);
};
return [_, () => result];
}
function print(msg: string): Cont<void> {
return Cont.of((k) => {
div.append(msg);
div.appendChild(document.createElement("br"));
k();
});
}
function sleep(ms: number): Cont<void> {
return Cont.of((k) => {
setTimeout(k, ms);
});
}
const processQueue: Cont<void>[] = [];
const pause: () => Cont<void> = () => Cont.callCC<void,void>((k) => {
processQueue.push(k());
return dequeue();
});
const dequeue: () => Cont<void> = () => Cont.of((k) => {
if (processQueue.length != 0) {
let next = processQueue.shift();
next.run(k);
return;
}
k();
});
function fork(proc: Cont<void>): Cont<void> {
return Cont.of((k) => {
processQueue.push(
proc.then((_) => dequeue())
);
k();
});
}
I have no use case for green threads, but still pretty cool.
Needs a better do notation to make this _.(...) dissappear. Maybe the _ variable can be passed as a parameter instead. May look cleaner.
We can also return a handle to the result of the monadic operations, to completely hide the do-notation and make it look like plain javascript.
Maybe contexts as a do-notation + return handles on values for return values. And watch the do-notation vanish.
Do-notation take 2:
function main(): Cont<void> {
return do_(() => {
fork(proc("A", 4));
fork(proc("B", 2));
dequeue();
print("done main.");
});
}
function proc(id: string, n: number): Cont<void> {
return do_(() => {
for (let i = 1; i <= n; ++i) {
sleep(1000);
print(`${id}: ${i}`);
pause();
}
});
}
(Using context to accumulate monadic binds (.then)). I think it looks pretty sweet.
With some fine tuning, could be handy for monster logic. In case a monster's logic is more easily expressed as a procedure.
Now this is interesting... monadic handles to values:
(ask returning monadic handle name)
function main(): Cont<void> {
return do_(() => {
fork(proc("A", 4));
fork(proc("B", 2));
dequeue();
print("done main.");
let name = ask("Whats your name?");
exec(name.then(
(name) => do_(() =>
print(`Hello: ${name}`))
));
});
}
I'll recopy the full code here from user script as a backup
too many characters ... github gist then π
Here is the magic: (monadic handles for return values (ask)):
https://gist.github.com/clinuxrulz/76ac4647cc84e4aad5c215183abd3218
It's a shame window.prompt is not async. It would be a better demonstration if it was.
An async prompt could be made with solidjs + jsx though.
I think we have the perfect do-notation.
Small utility for reading monad value handles:
function read<A>(a: Cont<A>, fn: (a: A) => Cont<void>) {
exec(a.then(fn));
}
The example now becomes:
function main(): Cont<void> {
return do_(() => {
fork(proc("A", 4));
fork(proc("B", 2));
dequeue();
let name = ask("Whats your name?");
read(name, (name) => do_(() => {
print(`Hello ${name}`);
}));
print("done main.")
});
}
I don't think we can get any cleaner than that.
And I am super side tracked again lol π
Just getting excited with that do-notation for Cont. I should make it a part of the prelude too.
Do-notation for Cont added to prelude now:
https://github.com/clinuxrulz/solid-kitty/blob/main/src/cont-do.ts
Another magic trick π© πͺ:
This code:
function main(): Cont<void> {
return do_(() => {
let name = ask(_("What is your name?"));
print(append(_("Hello "), name));
});
}
console.log(compile(main()));
Logs this to the console:
function() {
let x0 = "What is your name?";
let x1 = window.prompt(x0)
let x2 = "Hello ";
let x3 = x2 + x1;
console.log(x3};
}
Full gist:
https://gist.github.com/clinuxrulz/bb090a574ec6aab0a0a79a2c15c44de3
The Reified Monad trick above just returns variable names for values. But we pretend they are values for the sake of the DSL.
It does loose type safety. But we can reintroduce type safety with a type wrapper trick:
type Val<R> = Cont<string>;
Where R is the type of value being passed around.
Actually not that special. Could of been done without Cont altogether. π
I'll officially leave that FP Cont stuff alone for a while. And focus back on the collision system.
At the moment I am trialing making the collision events into entities that are children of the sprite that took part in the collision.
Not sure if its the best idea, but will see how it goes.
I have some basic attach/detach from parent methods in EcsWorld to assist with the parent/children structure:
attachToParent(entityId: string, parentId: string) {
{
let lastParentId = this.getComponent(entityId, parentComponentType)?.state?.parentId;
if (lastParentId != undefined) {
this.detactFromParent(entityId);
}
}
let childrenComponent = this.getComponent(parentId, childrenComponentType);
if (childrenComponent == undefined) {
childrenComponent = childrenComponentType.create({
childIds: [ entityId, ],
});
this.setComponent(parentId, childrenComponent);
} else {
childrenComponent.setState("childIds", produce((childIds) => childIds.push(entityId)));
}
this.setComponent(
entityId,
parentComponentType.create({
parentId,
})
);
}
detactFromParent(entityId: string) {
let parentComponent = this.getComponent(entityId, parentComponentType);
if (parentComponent != undefined) {
this.unsetComponent(entityId, parentComponentType);
let parentId = parentComponent.state.parentId;
let childrenComponent = this.getComponent(parentId, childrenComponentType);
if (childrenComponent != undefined) {
let newChildIds =
childrenComponent
.state
.childIds
.filter((id) => id !== entityId);
if (newChildIds.length == 0) {
this.unsetComponent(parentId, childrenComponentType);
} else {
childrenComponent.setState(
"childIds",
newChildIds
);
}
}
}
}
yes.... it has 2 sources of truth. But without 2 sources of truth, it will have a higher traversal cost when navigating the tree.
I really got my fingers cross for the performance of "Generational GC", we got a lot of short live objects being allocated for each frame π
ig we can just keep optimizing it when we have something working.
Looks like the collision system causes the game to lock up the moment mario hits a level tile... so partly working with bugs.
Ran out of time to debug it tonight.
ye seems to be common to link both parent and child
makes sense
Collision detection system starting to work. It's being shown by the rendering system for debugging purposes.
The position of the child collision object entities use positions relative to the parent entity. Hopefully it helps with logic involved for the parent entity to respond to the collision.
Here's a sample of one possible way a user might implement the monster ai for a goomba in user's script.
function goombaAI(entity) {
let moveDirX = 1;
while (true) {
while (notHitBlock(entity)) {
moveX(entity, moveDirX);
nextFrame();
}
moveDirX = -moveDirX;
}
}
Obviously this would lock up in an infinite loop. Unless it was a green thread (coroutine). (Thus my attraction to Cont. [Using callCC inside nextFrame to suspend and resume the green thread.])
Yes.. it could just be split up into a GoombaComponent which holds the monster state, and a GoombaSystem or MonstersSystem which exposes a per frame update method. (Which is what is normally done.)
But for users coming from Scratch, the green thread direction might be more intuitive.
In the end the end user can decide how they wanna do it ig.
We got generators and promises π
... we can use one of those for Scratch like monster AI instead of Cont.
For this example. You might be thinking, it is too simple, what about gravity, mario squishing goomba, or goomba hitting mario.
Well they could be considered separate concerns, and implemented in separate green threads that run in parallel to this one. (Thus achieving separation of concerns.)
One of the design principles of SOLID.
Or... the user can do it the system way. Up to the user.
Maybe a system to register/unregister update code per component type.
Generators will do for now I guess:
function* goombaWalk(entity) {
let moveDirX = 1;
while (true) {
while (notHitBlock(entity)) {
moveX(entity, moveDirX);
yield;
}
moveDirX = -moveDirX;
}
}
And just compose generators together. A generator itself is a weaker version of a coroutines anyway. (Can almost do what coroutines can.)
A goomba is just a simple example. There are monster with much more complex logic.
Like the angry sun ig:
https://youtube.com/shorts/vSizs3uYL9o?si=7up9BrTyXofrxOdd
Not really that complicated. Just a few small circles and a swoop.
Actually generators can not be used unfortunately. Because their state can not be copied or cloned for GGRS (good game roll back system) for multiplayer support.
The monsters have attack sequences which can make u want to program them just sequentially. But for GGRS to work, the state of the sequences will need to be stored in components which are updated once per frame.
... Cont state is clonable though. (E.g. store the k retrieve from callCC in a component. The k reference can be saved/restored for GGRS)
Though a downside to Cont is that it's execution state can not be serialised to disk for load/save snapshot support. Because we can not serialise/deserialise the capture of lambas.
Actually this can be worked around still by storing all the ks after a first pass, then resuming a k by index number and getting the user to use a single json object for it's state.
When I get PC access again, I'll try to get something playable going. So we can play with different theories.
Had a bit of a think about attack sequences for monster. I think this pattern will be enough:
function mobAI(seq: number, state: Json): { seq: number, } {
switch (seq) {
case 0:
// do stuff
return { seq: 1, };
case 1:
// do stuff
. . .
case 2:
. . .
}
}
(Each case number forms the escape and re-entry of the sequence. Similar to yield for a generator, or await for a promise.)
Easy to serialise and deserialise for GGRS. (And probably the way it was done on the old Nintendo)
Testing the users script can respond to the results from the collision system:
https://youtube.com/shorts/S2wgEFZLtog?si=izKwEilvvN11BIz4
Seems to be fine.
Threw my daughters face in there to keep her entertained. π .. her face is done just as a multiblock tile.
I think how to respond to a collision and gravity (maybe?), belongs in the user's code. Because we don't know what kinda of game they are making.
Will need to thing about that a bit more. Can still provide utilities to handle common cases.
I completely forgot we can edit the level while playing the level. I can demonstrate that later when we have monsters going.
lesgoooo
Some addictions are hard to break:
function goombaAI() {
let state: GoombaAIState = {
walkDirX: 1,
x: 0,
};
do_(() => {
let loopStart = Cont.of<void>((k) => k());
exec(Cont.callCC<void,void>((k) => {
loopStart = k();
return loopStart;
}));
exec(Cont.of((k) => {
state.x += state.walkDirX;
k();
}));
nextFrame();
exec(Cont.of((k) => loopStart.run(k)));
}).run();
}
callCC can make labels to jump to.
(Cont.of((k) => loopStart.run(k)) is basically goto loopStart;)
It is possible to make a string keyed record to those labels and store them in the state of the monsters AI Component.
On the next frame, the label name can be read from the ecs component, and jumped too via the lookup table.
Also the label names for re-entry can be auto generated by order of occurrence. So the user does not need to make their own names to the re-entry points.
The re-entry labels would be auto generated inside nextFrame, the user won't even need to know they are there.
It's also usable with GGRS (good game rollback system), and even can be loaded/save to disk.
Still looks ugly ig. Just exploring options.
Was thinking an AISystem or MonsterLogicSystem where those things can be registered for an entity while it is alive, and deregister when the entity dies.
Not really a win. The GoombaAIState would still need to move to a ecs component too. Otherwise any rollback would be in an inconsistent state.
I think I finally understand the implementation of callCC:
static callCC<A,B>(fn: (k: (a: A) => Cont<B>) => Cont<A>): Cont<A> {
return Cont.of((k) =>
fn((a: A) =>
Cont.of((_) => k(a))
).run(k)
);
}
So, Cont<B> can really be Cont<never>. The B is really just there to please the type checker. As when the users code chains that callback from the given callCC lambda param, the code will bail out of callCC and does not execute the next thing in that chain inside callCC. And if callCC does not execute that lambda param, then program just carries on as normal (nothing special in that case.)
It confused me first seeing k used twice in the implementation of callCC. But it's fine now.
The real purpose of callCC is to obtain the current continuation for you do things with it. (Like store it away for later execution.)
The full name of the function is:
callWithCurrentContinuation
But it is a bit wordy, and often gets abbreviated to just callCC
Getting rid of B makes it better already. Now automatic type inference works through callCC too:
static callCC<A>(fn: (k: (a: A) => Cont<never>) => Cont<A>): Cont<A> {
return Cont.of((k) => fn((a: A) => Cont.of((_) => k(a))).run(k));
}
Also lowers confusion. Because your not thinking about "what is B?".
The shape of the data structure we really need for re-entry and labels is:
(() => void)[] (a list of resumes)
Then a label is merely an index in that list.
That's bare minimum for a procedure like code-looking monster sequence.
Cont already covers that data type. But the thens form a link list instead of an array.
Unlike the array, a link list can be of infinite length. (The thens are lazy)
(() => void)[] shape looks like it would suit simulating a CPU. π... indexed into via program counter (IP)
The do_ is not quite the same as Haskell. Its more of a do-notation for monoids, not monads, but still useful.
It can be made more general to work with other things (not just Cont) like so:
do_: <A>({ empty: A, append: (a: A, b: A) => A, }) => (cb: (exec: (instr: A) => void) => A
In the case of Cont the type A is Cont<void>, and append is Cont.then(...)
Monaco must be using an older version of TypeScript. It fails to type check my TypeSchemas, which work fine in vs code:
I could save myself a huge headake by just using json objects for component types, and a json verification library on top to confirm the shape of the data. (Would of made that automerge wrapper a piece of cake.)
Ohh... nice!
function mobAI(entity: string) {
let loopStart = nop;
callCC((k) => loopStart = k);
sideEffect(() => {
let mob = world.getComponent(
entity,
mobComponentType
);
if (mob == undefined) {
return;
}
mob.setState(
"x",
(x) => x + mob.setState.dirX,
)
});
nextFrame();
goto(() => loopStart);
}
That looks squeaky clean. (The do_ notation construct hidden in the caller to the function, so the end user does not need to know about magic going on.)
The advantages over updateMobAI() per frame can not be seen until there are multiple stages in the sequence. (angry sun)
Here is that callCC helper used in mobAI above:
function callCC(fn: (k: Cont<void>) => void): void {
exec(Cont.callCC<void,never>((k) =>
do_(() => fn(k()))
));
}
goto(s) in javascript π€£ :
https://www.youtube.com/shorts/DtyJUHSXMow
My daughter got up me for covering her face in bricks. I told her its to protect her privacy.
Can probably make a while abstraction over those gotos.
For a monster delaying between changing sequence, there could be a while loop with a frame delay count while calling nextFrame() inside it.
Sun sits still ---delay---> sun swirls in small circles for x-number of frames ---> sun swoops ----> back to sun sits still.
Each sequence animated in its own while loop.
Ohh.... mario can fall through the level if the level does not load quick enough :p... gotta spawn him after the level is finished loading.
Next weekend goal may be to do a full replica level from the original game with all the mobs (monsters).
Make it an example in a examples combo box somewhere, that can be loaded easy.
And keep iterating the design of the prelude/engine to make the process easier and easier.
Feels a bit strange to use a separate editor window to edit a level while playing it. It might be better to also have the ability to add level blocks in the actual gameplay window that we play the game in.
Like an overlay of controls that can be switch on and off.
We're already loading the levels from an automerge doc inside the iframe. There is no reason why we can not also write to the automerge level files from inside iframe too.
yes, i like it!
ye it's a bit headscratching for me tbh
some wild javascript!
It will still be up to the end user. That approach there is more of an experiment. (I'd never do it in code at work.)
Just things that feel naturally procedural, I'd like to see if it's possible to code in a procedural way.
It's kinda like a DSL in a way for a sequence like control flow visually, but under the hood when executed it is still update per frame.
I applaud the exploration!
I'll need a while loop for that DSL. That looks like a plain while loop.
Maybe something like this:
while_(cond: () => boolean)(body: () => void)
Which might look OK.
That cond being a lambda will feel a little odd. But it's the best I can do.
I think I have a good analogy.
It's all single pass just like solid Components
mobAI is just executed once to build the DSL code, and that DSL code is executed in an update loop per frame. Just a single pass to build up the code for the DSL.
And do_ in a way is kind of like createMemo. So it's no less mysterious than solid. (But has a different purpose than createMemo of course)
I better do the angry sun demo to prove the worth of the DSL π
And down the road. Multiplayer to prove the DSLs worth over a generator. (Rollback code)
I believe SVG animations are kind of like this DSL as well.
Our DSL while loop:
export function while_(cond: Cont<boolean>): (body: () => void) => void {
return (body) => {
let loopStart = makeLabel();
read(
cond,
(cond) =>
cond ?
do_(() => {
body();
goto(loopStart);
}) :
nop
);
};
}
a delay between two different attack sequences:
// previous sequence
// E.g. angry sun sits still
//
// wait before progressing to next sequence
let delayTicks: number;
sideEffect(() => delayTicks = 100);
while_(() => delayTicks != 0)(() => {
sideEffect(() => delayTicks--);
nextFrame();
});
//
// next sequence
// E.g. angry sun swirling in the sky
(The delay between the sun sitting still in the sky before it starts twirling)
that's probably as close to a JavaScript looking while-loop I can get.
I'll try to hide all the Conts under the hood so the end-user does not need to know/think about them.
(Would rather not explain Cont to end users.)
Example of using the coroutine DSL:
https://youtu.be/25rIln5Pflg
ok that is a helpful demo
makes me understand how it can tie in with scheduling of stuff
Yeah... it's internally converts a sequential algorithm into an iterative one under the hood. So u can think about monster ai/logic sequentially.
It animates more smoothly when I am not screen recording. :p ... the recording doesn't do it justice.
Ahh... the recording missed the use of the related system in the recording. I'll push to main and pass along a link.
that's an export file that can be imported. I'll commit some examples in the code along the way.
Here is the relevant system code:
https://github.com/clinuxrulz/solid-kitty/blob/main/src/systems/MonsterLogicSystem.ts
Basically when the nextFrame DSL function is called, the continuation gets assigned to that local resume variable.
resume gets kicked off each time the nextFrame function provided by the system is called.
It's kinda like that debug/breakpoint example we saw from that guys blog about implementing callCC using generators.
Here is the nextFrame from the DSL:
It's grabs the "current continuation" by using callCC and passes it out to the system which is stored in that resume variable to be kicked off again on the next frame.
Can be fun to replicate all these bad guys:
https://youtu.be/ePYh8CiM9zI?si=v0QV7PdbIV4A2mdX
In todayβs video weβre taking a closer look at the enemies of Super Mario 3, as I lay out my Top 10 favorites, explaining why each character is unique and memorable.
Check out my Patreon, help support my work:
https://www.patreon.com/killgruz
0:00 - intro
0:48 - Buster Beetle
1:56 - Spike
3:08 - Thwomp
4:06 - Fire Bros
5:12 - Ptooie
6:19 -...
MonsterLogicSystem was probably not a good name. Because it could be used for the player too.
Different running paces as after you start running for example
And the slide when u suddenly change directions.
Coroutines can also be forked by another coroutine and a handle to the process can be past back for earily termination.
E.g. Angry sun stops twirling and just falls when you hit it with a super star. Then dissapears after a time out.
The coroutines can also support a per frame update function as well. Depending on the logic being implemented, sometimes a per frame state machine makes more sense.
Yay!!! π
Not out of the woods yet.
valtown is a CJS module and it is using import ts from "typescript" in its code instead of import * as ts from "typescript"
This is causing the production build to fail when valtowns library is used.
Previewing the vite is working fine though.
Maybe I should try a later version of valtown. That might bring why chee is on version 3, instead of the release version 2.
Why ppl are still making CJS modules these days is beyond me π
Hmm... chee had a patch file too in her production build process.
valtown version 3 didn't help. I'll keep investigating.
Trying to do a vite plugin to patch valtown.
Hmm.... what is this hiding in chee's littlebook:
tsconfig: ... allowSyntheticDefaultImports
No go. I already had that flag set to true. Will keep investigating.
Last resort resort ig is copy paste all of valtown and recompile it to esm.
Here is the current battle for context:
I manually patch valtown to get further in the production build process.
Next.... glob imports don't work in production build it web workers.
She's an up hill battle π
pnpm patch commit fails in termux too. Bcuz it try's to load a GUI editor for the commit message for the patch.
From compiled code inside glob import:
That error could be outside the glob too. I'll have to narrow down the location.
When I get on a PC I'll make some proper patch files for valtown codemirror ts. So I can have a smooth production build without editing the resulting build files afterwards.
Seems there is a breaking change in the TypeScript lib where they switched from regular enums to string enums. And I believe valtown codemirror ts is built against the TypeScript lib version prior to string enums.
Will keep investigating.
Watching the ryan stream: apparently a new direction for signal 2.0, and it's going to be based on continuations!
It's possible. However I don't understand what the advantage would be for signals alone. I'll have to watch the stream.
Solid 2.0 !== signal 2.0 I'm guessing.
Here is one experimental implementation floating around.
https://github.com/rkirov/cont-signal
And I suppose there is a lot of control flow power (e.g. coroutines)
This guyis using burrido for his do-notation... he also feels the pain π
The issue I have with burrido is the generators yields can not be type checked. (They'll all have the same type.)
And there is another reason not use burrido for anything serious:
https://github.com/pelotom/burrido?tab=readme-ov-file#caveats
Weird.
During pnpm run start everything works fine.
During pnpm run build, the ts from import * as ts from "typescript" is an empty object when it gets passed to createVirtualTypescriptEnvironment.
Vite must be stripping unused symbols, but vite does not realise they are in use by a 3rd party library.
Wonder if I can do this to avoid stripping:
// @ts-ignore const ts = await import(/* vite-ignore */"http s://esm.sh/[email protected]");
Solid 2.0 === signal 2.0! It's a new direction milo came up with. Apparently thinking about moving away from lazy signals and doing this approach inspired by something from ocaml.
Ryan said he's going to do a stream w milo soon
Ye that should work!
It sounds very experimental.
It will need a lot of testing.
In languages without tail call elimination you run the risk of stack overflow for example.
Ye, fresh from the trenches
But they apparently already have an implementation
I think they are talking about it in next
es6 for webkit (apple) has tail call elimination but not for Chrome.
That worked π
Now to tidy the mess I left behind by my failed prior attempts :p
(I have a double import of typescript now)
More tweaking needed:
The documentation for the method is covering the auto complete items.
Can finally select text on mobile π
God... the hoops we gotta jump through.
And she works on gh pages... yay:
Code mirror is using web workers in kitty. So it's less sluggish than Monaco at the moment. I did try web workers on Monaco, but I ran into issues. I'll revisit that later on.
Amazing work random!
Cheers
Would you happen to know how to get the "tab"-key to autocomplete instead of "enter"-key in code mirror?
google found a solution:
https://discuss.codemirror.net/t/using-tab-key-for-autocomplete-suggestions/7234/2
hmm.... next is.... make a full level I guess. And add features a long the way to make it easier to do.
Might try replicating Super Mario 3, World 1 Level 1 as a starter. And work from there.
here is the level I will replicate:
got a few monsters to write the logic for:
Goomba; Venius Fire Trap; Koopa Troopa; Paragoomba; and a couple of others
won't be finished in 1 weekend π
There is a bug in my coroutines at the moment for when there is multiple monsters. I will need to sort that out first.
I think I spotted the bug already... but I might get in trouble for switching on the PC. It will have to wait. π
Apparently I have some sort of addiction and I get stuck on the PC once I start. π
Thus the push for code mirror. So I can pretend I am in Facebook.
If I write a helper function that converts a Cont into a per frame update function, it will simplify things greatly.
Turns a Cont thats using do-notation into a per frame update function, that u can execute over and over again:
Its what they call a trampoline.
Some call it a pogo stick function.
It's how we will get many monsters on the screen at once. Each of which pretends they own the CPU.
I believe Windows 3.11 uses a similar technique for their threads.
But on a machine/assembly code level. Not a language level.
Back in the days when computers only had 1 CPU.
I should probably build the trampoline into the shape of the Cont type itself. Just incase of any async in the chain that will drop the nextFrame context.
The true shape of a Cont is:
((A => R) => R)
A is the return type of your computation, and R can be anything u want in order to add more features to ur computation.
Currently R is void for the bare basic stuff.
R could be changed to Cont<A> | undefined for built in trampolining.
(E.g. if Cont.run() returns Cont u run the new Cont and keep going in a while loop)
Although we might wanna do more things than just trampoline. Maybe I should expose R as a type param we can change.
I.E. we are not trampolining to avoid stack overflow. We are trampolining to represent how much work to do per frame. The intent is not clear enough in the shape of the data type.
It's a context thing we are chasing (I.E. the Reader monad for R).
The context being the nextFrame callback for now. But may be many more later on.
The reader monad has this shape.
Context => A
Leading to a Cont of:
(A => (NextFrame => void)) => (NextFrame => void)
(For our current use case)
(NextFrame here is our function to accept the suspended computation to resume on the next frame)
Which may look insane. But that's following the pattern.
Well... R can then be all the additional api we are supplying to Cont for the context of its execution environment.
I wanted to keep it simple looking... but I might to switch on R to make other things more simple implementation-wise.
That's a shame π«
Hmm.... what if Cont<R=void,A>... that will default R to void in places I don't need R.
Or maybe Cont<A,R=void>, then I can pretend I am a single type param in places I don't need R.
Or... I can just be careful with async and keep R as void.
Decisions Decisions.... π
I can view it a different way too... what about the Writer monad for R.
The writer monad takes on this shape.
A => R
Leading to Cont of:
(A => (void => NextFrame)) => (void => NextFrame)
Nope... I'll stick with R as void π... don't wanna complicated things early in the piece.
The main thing to be careful about is the current context strategy being used does not work across async boundaries.
There may need to be some extra async primitives added to restore the nextFrame state over an async boundary.
Doing this may work over async boundaries because I am reading the nextFrame_ to a local variable outside of a Cont. Fingers crossed:
(The do notation captures the nextFrame_ callback for the entire call chain via local variables, even across async boundaries)
Basically each concurrently running Cont has their own separate nextFrame callback.
The bug is fixed (via toPerFrameUpdateFn). We can now have multiple monsters:
Another demo:
https://youtube.com/shorts/5GLTIXeqzdM?feature=share
I suppose all addition API for Cont can be handle via a context, just like we did for nextFrame. So R can remain void forever.
Signals via continuations is probably not that crazy. Our memos re-execute after their dependences change, and re-execution is k. It's just a matter of the scheduling that k ig to avoid glitches.
"Protecting ourselves from copyright" strategy:
A 100% free tileset.
First a level ripper to speed up the process:
That will help generate the array of tile numbers for the map. Then we use AI to generate unique looking tiles for each of the tiles.
Just need a fast reliable (but secure not requied) hash function to do fast tile matching. I might ask Gemini for one.
That toPerFrameUpdateFn should probably return () => boolean instead of () => void. So u have a flag to know if it terminate or if it is still going.
https://xxhash.com/
(Super fast non-cryptographic hash algorithm)
Ohhh... it's used by call of duty and minecraft, that hash algorithm.
Basically we can turn each of the level tiles into a 4 byte integer for a quick search rather than comparing pixel for pixel.
We can also use it to help speed up finding a picture in a picture that is not aligned to a tile boundary.
sounds cool!
Here's a basic tile ripper function:
https://github.com/clinuxrulz/solid-kitty/blob/97dcd9f14a9248bb1ad22ccf56a5e0db33563737/src/level-builder/level/ImageTileMatcher.tsx#L112
It doesn't handle hash collisions, but should do the trick.
The performance should be O(n) for tile matching, where n is the number of pixels, regardless of the number of distinct tiles.
It won't be 100% perfect, but hopefully 95% correct, and the last 5% can be adjusted by hand.
Tried with 3 tiles to begin with. Looks like the image tile matcher works:
was thinking as a shortcut. The meta-data for the tiles can just be a string/textbox. Because we do not know the shape of the users data, because we do not know what game they are making.
then the use can (if they want) parse that meta data as a json object and interpret what it means in their game.
Starting to get fun π
Svg is very slow (for pan/zoom; no dom change) with a lot of elements. I should of stayed away from svg I think. (Old original Nintendo out performing modern SVG on modern PC π)
I'll use canvas on it instead... down the road.
CameraComponent is in:
https://youtube.com/shorts/_GuHZ9GFpuU?si=pm4F2rAEboFmFLPV
ig ... logic for more monster types, and implement how the entities respond to certain types of collisions... then we have a playable level.
ahh... and that meta data. I gotta get that into the tile properties. It will just be a textbox that takes user data (probably json string) to descriib the properties of certain tiles. (E.g. hit this box you get a coin; hit this box you get a mushroom; etc.)
Playing YouTube for the kids in a split screen while recording in the other half on a 15 year old laptop slows the frame rate a bit. It's smoother without the other stuff going on. π
But there are places I can optimise to make it smoother.
Mm kind of surprising. Are you frustum culling them?
Also, is it rendering pixels as svg?
Not on the svg renderer. I was being lazy.
Also... each tile is an svg image of the whole tileset being cropped to that one tile.
Which is probably not ideal either.
And the tilesset svg it renders pixels as filled in outlines?
Nope... using an svg image element for each whole tile.
A gotcha
With a render param to remove antialiasing.
Mm, seems like that should be able to work fine
It could be all the 0s
I remember it pan/zoom slow with all empty blocks.
The 0 is the index number of the tile.
Text is like a curvy vector image.
I'll remove the zeros and see what happens.
The svg render is only in the level editor... I got tiles render 4 different ways all over the place π
1 via svg, 1 in divs, 1 in 2d canvas and 1 in webgl (pixi.js)
The 2d canvas version from previewing the resulting tiles from the image tile mapper is quite fast and would make a suitable replacement if needed.
Problem solved. It was the 0s. Removing them made it fast and snappy.
If I need the ID numbers back, I can render the numbers to images and put those images in the svg.
They were only there for debugging purposes.
Basic meta data for tiles is next sub goal.
Just gonna go with a text box that takes a json value.
Gotta represent which tiles are solid, which are platforms, etc.
I didn't reach the goal of a playable level this weekend. Maybe next weekend. π
Got close though I think
step by step πββοΈ
Maybe I can slip in meta data before I get kicked off π
I need it to set up spawn points for monsters too.
Bare bones basic meta data editing for tiles is in.
It technically should be possible to make a full game while lying in bed on my phone now :p
I should do a proper youtube tutorial showing ppl how to use it.
Then I load up Chrome instead of Firefox and it's even more silky smooth π
(No jittering in map scrolling in show game)
Just use chrome... just use chrome...
Yes please π
True... makes me remember when I did the pianissimo thingy. Firefox support is a good motivator for optimizing the code.
Note to self todo:
- Adjust sprite on-screen rendering position to be relative to camera.
- Switch to immutable Vec2 (instead of mutable) for easy of maintenance.
- Start making use of Complex type from Transform for handling rotations.
- A Tileset ripper. Give it a screenshot of a game, and it will make you a tileset.
When the record/tuple proposal finally lands in javascript, there will no longer be a need for mutable vectors at all from a performance stand point.
Dam... I get home from work and there are some keys missing from the keyboard.
This is not my day π
I'm missing the 8 * key and the 9 ( key.
So no multiplication or function calls... tough π
Yes, has been replaced by Composites: https://github.com/tc39/proposal-composites
Removed some of the syntax
πa mystery
Praise Termux... she works without keys.
I see. Not sure it achieves the same goals though.
Records and tuples have no GC pressure, as they allow you to make your own primitive values, like numbers. They can consume stack space rather than heap space.
Composites on the other hand still consume heap space.
But if generational GC is performant as they say, then maybe it's OK not to have records/tuples.
Maybe I can buy individual keys instead of a whole new keyboard.
I've got a plan. I will pull out the two most unused keys on the keyboard and jamb them in the 8 / 9 spots.
So I'll remove "Scroll Lock" key and...
What's another useless key
And "Insert" key
I can swear I've never touched them.
.... about the pull the keys out, and we just found keys 8 and 9 π
... they were in the vacuum cleaner.
That was close π
loool mystery solved!
Did a quick unique tile finder via Termux. We can take an image of a map from any 2d retro game and turn it into a level in under 60 seconds:
O shit that's very cool!!
We can also build a statistical model and perform a waveform function collapse to generate unique levels from given sample levels. But it's more suitable to top down RPGs.
Of course it won't do the meta data on the tiles. That is still up to the user to do. Unless we hook up some AI.
Need a full screen button too:
So... 91% of kids have an attention span of less then 60 seconds π
Maybe I can squeeze in something small from my todo list before bed time.... let's see.
I'll do Immutable Vec2 (for easier maintenance) I think.
If performance drops, I can have a 2nd mutable one that can get frozen to the Immutable one at the end of a long calc.
Ohhh... usage count 380 π... wonder what I will break.
I suppose just do it and keep testing for a while before merging into main.
If it were NASA, then ur not allowed to touch the heap. It's all stack memory for their apps which I found interesting.
It's to insure an upper bounds on memory usage via static analysis.
Also no recursion is allowed.
I gotta remember to revisit the array proxy for the automerge object that allows it to be used for a solid store.
There was a bug in there, that I left in there, because proxies were annoying me. :p
I think the key will be doing it without TypeSchema or types. It just seems to get in the way for this case. (I.E. createStore doesn't need a type schema to know the shape of your data, so the proxy over an automerge object should not need one either.)
@zenith void thank you for this code:
function getGuideKind(index: number) {
const isLastGuide = dirEnt().indentation - index === 1
return isLastGuide && isLastChild(dirEnt().path)
? 'elbow'
: isLastChild(getAncestorAtLevel(index))
? 'spacer'
: isLastGuide
? 'tee'
: 'pipe'
}
I had to implement a tree ui component yesterday at work. And it really helped me. (I was quite puzzled with the guides.)
I do have another tree-like component, but it was a bit of a hack around <detail> with css sprinkled on top rather than using <div>s
Oo nested details, that's kind of neat!
It's dirty hack π... I wanted to do it properly.
<detail> u have to call methods on it to expand and collapse rather than just updating solid store state and have it's render auto update.
I kinda feel like we should make a general purpose tree ui component down the road that isn't tied to file systems. Unless one already exists.
Just to complete the ui toolkit.
Also (unrelated).... ReactiveSet for storing selection state can give O(1) updates. I experience that the other day at work too.
U just pass the ReactiveSet for selection state down to ur nodes, and have ur nodes mutate and observe that reactive state.
It could provide a clue for creating an efficient createSelector for multi select.
Currently it's O(n) for createSelector for multiselect I believe. The type signature would need to change a bit to support O(1). But atleast we know it's possible without changing the reactive implementation under the hood. I used to think Β½ edges would be required to support O(1) in that case.
After a few more tweaks, I'll have a crack at implementing some of your designs.
They seem like they can help tie things together, and prevent the user from getting lost in the user interface.
Ah... shoot that won't work. I'll need TypeSchema because not all my data is json.
Problem solved:
https://github.com/clinuxrulz/solid-kitty/blob/main/src/automerge-doc-mutable-proxy.ts
And tests to prove it works:
https://github.com/clinuxrulz/solid-kitty/blob/e1f00725266ff5308db763ed17aa8e97fb198235/src/index.test.ts#L210
I also found a way for a TypeSchema to derive the type for the State of which shape it defines. But haven't done anything code-wise about it yet.
(To basically save on a double up of type signatures. One value level, one type level.)
Ohh... I forgot to unwrap on the set side to prevent double wrapping.
I need to do my own isWrapped / unwrap just like solid has.
but then that one would not have drag'n'drop and stuff? only for visualizing the tree, not editing it?
also somebody posted https://github.com/lukasbach/headless-tree the other day in #off-topic
Yeah. Editing support can easily be done by the node renderer supplied by the user.
I also need to make sure two read calls to a.b returns the same reference (for case of invarient map for vec2TypeSchema), not just the same value. I'll need to throw in another map.
So almost there.
It looks good π
that dragging looks nice!
Yeah. It's pretty cool. Able to move and reorder.
Got isWrapped / unwrap working for the setter side of that automerge proxy. Just 1 piece of the puzzle left to solve.
Let's suppose b is a non json type that passes back and forth through json serialisation/deserialisation to talk to an automerge doc. I need to make sure reference equality holds true.
I.E. a.b === a.b should return true.
That probably involve another weak map to cache the deserialisation result maybe.
WeakMaps everywhere :p
OK... solved this one too now. Will switch over the automerge projection and see what happens ig.
Interesting that this test would fail:
doc is a makeDocumentProjection of docHandle.
Maybe the change handlers for automerge do not fire immediately. Or something wrong with my test code.
Hmm... not ready to switch to version 3 of that automerge projection through type schema. I lost reactivity somehow.
Source: https://github.com/clinuxrulz/solid-kitty/blob/main/src/automerge-doc-mutable-proxy.ts
It's seemed pretty rock solid. But I must of missed out on a minor detail.
I'll need to write some async vitest tests to narrow it down.
I'll study chee's vite tests. Mine are failing in the test console, but passing in the browser.
... I couldn't work it out. I will run the new tests in the browser I think π
It's probably solidjs thinking it's running server side again.
No luck on version 3. I should of just tried to fix version 2. Rather than a rewrite (v3).
Proxies are definitely my kryptonite.
I think the issue is I need to keep a full referenced path to root, just incase a parent node get swapped out while reactively monitoring a child node.
Another challenge is. I believe an automerge doc can be temporarilly in an inconsistent state while it is still loading. (Incomplete shape)
Ultimate goal:
function createAutomergeProxy<T>(
docHandle: DocHandle<any>,
schema: TypeSchema<T>,
): T
The return value is a reactive mutable. That can be wrapped by createStore.
New projection is finally working π π . I can use both immutable setStore and setStore using produce to update an array in a automerge document wrapped via a proxy from underneath.
Meta data for tile that sprite collided with added to collision component. So the sprite can know what kind of tiles it collided with. (Ground, coin, platform, etc.)
In theory I should now have enough implemented to create a playable level. Will find out if that is the case soon.
It's likely I'll need to add a system or two yet to make it more ergonomic.
- Virtual DPad system thrown in for testing purposes (no keyboard on mobile)
More work to do on it. Here is where we are at:
https://youtube.com/shorts/DGIhrR2EzGk?si=NHW5rNtkzpEEptrG
Cheers π»
Would be neat if there was an algorithm to collapse auto discovered tiles into a compact layout such that they are connected in the right adjacency locations in the compact sheet as they were the original level.
Basically to auto generated a compact tileset from an image. At the moment for auto tiles, it is just using the entire level as a tileset image.
I have a feeling an algorithm based on entropy should do the trick.
H(X) = - Ξ£ [p(x) * log2(p(x))]
That is to say, we make a statistical model giving the probabilities of each tile appearing in adjacency to other tiles produced by the source image. And then process the cells for the final tileset in order of highest entropy (H) to lowest. Using the statistical model to pick a tile in each cell along the way.
And here is the start of the algorithm:
https://github.com/clinuxrulz/solid-kitty/blob/main/src/level-builder/tileset-collapser.ts
You can see the use of the entropy formula to decide the order to process the resulting tile coordinates in.
That one is not really correct yet. Needs some tweaking.
Yeah... the tileset collapsing algorithm can use some work. It sort of has related tiles together, but not in a way they join properly.
It's way off. Needs a lot of work. π
It's meant to make a sensible layout of a collapsed tileset with clouds, pipes, etc. joined up how they would appear in a level.
This must be a few bugs.
Maybe I was meant to go from lowest entropy to highest entropy, not highest entropy to lowest entropy.
Tiny bit better:
It's just to auto lay tiles out so they are easy to find for when adding meta data. Like tiles near like tiles.
It's a little hit and miss, but close enough for now.
Looks pretty!
Down the road. I'll just provide something so the user can drag the last couple of frame/tiles into nice spots. (A move that edits the image underneath)
It's like a 90% complete jigsaw puzzle.
Having a play with the collision response function... seems to be working, but it is a bit complicated, there might be a better way to do it:
The attached file can be imported into:
https://clinuxrulz.github.io/kitty/#/app
the collision response logic is in src/mario.ts at the moment.
Maybe if I provide the tile cell index coordinates in the collision components as well as the ability to read the map might make it more ergonomic.
Or just tuck it away in another optional system, and the end user does not need to worry about it.
I'll pull that into a optional system. One less thing for the user to worry about..
Next is.... Animations?
I'll gonna rename the repo to use the new name.
gonna rename?
solid-kitty to tool-kitty for repo name.
It's still called ToolKitty.
I just never got around to renaming the repo
Animation UI I'll keep simple for now.
You press "new animation", give it a name, pick frames, set speed.
I'm in amongst the animation ui code. But will take a while. Free time is short.
I wonder if I should assign a duration property per frame, or just a frame rate for the overall animation.
Here we goooo!
https://youtube.com/shorts/fRxuzUFgP_8?feature=share
One thing I just realised is we may want to use the same frame multiple times in a sequence. E.g. [1, 2, 3, 2] in a loop.
Maybe tap to toggle frame use was not the best idea. I may need a clear button instead.
that little preview on the top right is fun π
One day, I'll do hour long video tutorial for making a platformer in it from scratch (starting with drawing in the pixel editor, all the way up to the scripts).
I just need a few more features before I can reach that stage.
I'm sure if I do that, and throw resource links in the YouTube description, it will capture some attention. And we'll start having a community.
.... then I can get ppl to write me code for free π (pull request, etc.)
Interesting. .getContext("2d") is not light weight, it has a bit of a delay on my old laptop.
I switched to creating a canvas and ctx just once at the beginning instead of creating a new one each time the selected animation frames changed to speed it up.
All I got to do yesterday was add a name field for the animations....was hoping to get a lot further π
... there is always next weekend.
We can now use the same frame multiple times in a single animation sequence:
Bit weird of how the svg outline of the letter self intersect of Chrome, but do not on Firefox.
Ig they use two different svg libraries.
Next I'll get our scripts able to reference/use those animations by their name.
Also... I'll get that optional basic platformer collision response system in their to simplify user code.
It will handle solid blocks and platforms for now. Slopes can come later.
export class CollisionResolutionSystem {
constructor(params: {
world: EcsWorld,
isSolidBlock: (meta: any) => boolean,
isPlatform: (meta: any) => boolean,
})
. . .
Will be a good start
meta is the meta data associated with the tile the sprite intersects
The tricky part with this strategy is a need to respond late, but before the next frame. So that user code can see when mario hits a ?-box with his head.
If the users can manually pump the systems (manually call update), the can control the order of their side effects.
Another thought, if all side effects are deferred, then it doesn't matter what order the systems and user code process the current state.
E.g. collision resolution of mario comming into contact solid block will not prevent user code from mario opening a ?-block, if changing Mario's position is done after all systems had a chance to observe the state and queue their side effects.
... so that is where createEffect should be used instead of createComputed
Where you have simultaneous reads of which some mutate what they are reading.
Although I'll probably reach out for a per frame update function. You can hit a infinite loop of mario bring push in two different directions if he gets squished between two blocks.
Reactive code not suitable everywhere.
Very true. I remember trying to make a game engine built on top of reactivity and it did often feel like forcing it. Like w graphics programming you kind of need to run everything every frame, so it's inherently coarse grained.
All this is saying is:
Ideally per frame every system should be looking at the same state, and their view should not be effected by mutations of other systems within that frame. (Mutations by systems within that frame should only be observable by other systems on the next frame.)
Most game engines do not honour that for performance reasons. (Additional memory allocations)
But the nice thing about it is that your systems can be updated in any order, and get the same result per frame. (Given that their mutations don't mutate the same piece of data)
Says a fair bit for a 4 token type signature :p
In our case where our games are simple, don't require much processing power and each system is optional. It doesn't hurt to experiment, even if the choices are not the most performant.
that makes sense
and this also
you double the state
is a bit like swapping the buffers w graphics programming
ping pong rendering?
Yeah... I haven't had the chance to do much today or yesterday. Got a soft ban from the PC :p
Sort of.... there is ways around it in assembly. You can effectively store descriptions of change in stack space and don't reset the stack on function return. But with programming language you are forced to use heap space.
But asm like thinks can be emulated by pre-allocating an ArrayBuffer and using it like a stack.
Or what if we had reusable objects that describe change and are re-used with a memory pool.
Example object:
class SetPropertyChange {
entityId: string;
componentType: string;
fieldName: string;
newValue: any;
}
That's one example delta-s
The change objects can be reused via a pool to avoid memory allocations.
It will still use a fair bit of total memory. It is more the performance of the garbage collector that concerns me more.
If we re-use memory rather than gc and re-allocated, then we dodge the garbage collector.
Memory can be reused by keeping the objects in a reserved array.
But maybe I should put some more faith in the generational GC and not worry about it so much. :p
We can see THREE.Vector3 was written so ppl can reuse memory. But I think threejs pre-dates generational GC.
Proper object value types would of helped too, but the proposal got rejected.
Tbh i think game development is probably one of the few places on the web where it makes sense to care about it. Anything that relies on steady fps.
That GC jank
That is true. Even if gc is fast overall on average. The gc pauses would frequently drop the frame rate.
Easier to dodge the gc in Rust than JavaScript. But we will try out best.
That's her for now:
let cr = $.createCollisionResolutionSystem({
world: $.world,
isSolidBlock: (meta) => meta == "g",
isPlatform: (meta) => meta == "p",
maxSpeed: $.Vec2.create(10,20),
});
I just manually call cr.update() once per frame to resolve collision (prevents falling through ground, walking through walls, etc.). Nothing special.
It's not the theoretical correct one (system update order matters). But it will do for now.
One approach for a GC free system update. Just keep an ArrayBuffer of a large enough size and an index to the next free memory in the array.
Once the update call is finished, simply reset the index back to zero and reuse the whole ArrayBuffer on the next update. (After an update all the new state has finished mutating, so safe to reset index to 0)
Sort of like that ZeroGC algorithm used for command line tools written in Java.
It is a bit awkward in javascript though to use a continuation ArrayBuffer for memory in place of javascript objects.
It's like replicating a stack you would find in RAII languages.
I'm asm.js would of done something like that too.
yes, just got to be careful not to make additional intermediary state in the process π€£
Tricky... even making a proxy to make using the ArrayBuffer more developer friendly... boom, u've allocated memory.
Just about gotta just use numbers for your objects, which are indices into that array.
Example:
addVec(obj1: number, obj2: number): number the numbers are like pointers.
Well... wasm returns objects as numbers. Same thing.
We always have the option of implementing some systems in Rust and compiling it to wasm.
If I recall correctly. The original Nintendo games had no dynamic memory allocation at all in their standard runtime lib. You wrote to raw memory and managed it all yourself in your code.
You would preplan what memory ranges would be used for what.
Very different world back then.
Anyway.
Mustn't get too side tracked. I haven't noticed any frame rate problems with our current setup.
Next on the list: Animations in user's scripts.
And we can always refactor systems once problems arrive.
... and a flip, to Β½ the number of animations. (Character walks left; Character walks right)
All that will happen for animations in user scripts, is:
The rendering systems sees the animation component and it will keep updating the sprite component to reveal the current frame of the animation.
Just a data operation, nothing more.
Here she is, the animation system for user scripts. (Hot of the press, completely untested):
https://github.com/clinuxrulz/tool-kitty/blob/main/src/systems/AnimationSystem.ts
Cont seems handy for decomposing on the output side (rendering), but not good at composing the input side. Seems like a handy rendering primitive when u have no JSX.
Memos good for composing input;
Conts good for decomposing into outputs;
Two halves of the same coin i think.
Actually the new createProjection in solid 2.0 also decomposes reactive stuff.
I'll test the new animation system later tonight in Termux in bed.
Gotta play with this when solid 2.0 comes out. Maybe it is good for decomposing reactive values for fine grain outputs.
I.E. a way to pull single values into parts, rather than pushing parts together into single value.
it is kind of interesting that solid's new signal implementation pulls inspiration from continuation stuff like Incremental
Interesting
@scarlet belfry was playing around with it too
signal implementation built on top of incremental computations
looked rly cool but i m pretty clueless about the details
I've read papers about incremental computations in the past. But I think they were different.
https://www.umut-acar.org/research#h.x3l3dlvx3g5f they do go way back
I believe my Β½-edge approach I proposed ages ago does O(1) createSelector.
I might have to wip up a demo library as a PoC.
and so the prophecy continues Those Who Stay Long Enough In Solid Discord Will Write A Signal Implementation
Don't have the energy for it. But it feels like a must. There could be some really nice gems, that might not be found.
It's kind of nice that a signal implementation is not a lot of code
how do you propagate change over a tree
i love self-adjusting computations. and adapton. and nominal adapton. they are such fun papers
i'm currently thinking about the kind of reactivity system you need/can have when you own both the computation AND the textual representation of the computation -- like if you're building a coding playground, or a spreadsheet app,
it's cool because:
- you can statically analyze the code before running it to know what it reads/creates
- computations have names and can be stable across big changes
- there's a distinction between "the code that made this computation changed" and "the outputs this computation creates changed"
so it lets you do stuff that needs language-level features (like Memoized Dynamic Dependency Graphs from acar's self-adjusting computations paper), or seamlessly provide features like the name+args needed for Nominal/Typed Adapton
also as a side note: designing a programming language using codemirror is REALLY fun. you can use the Lezer tree as your AST. in order to add a programming language feature -- you have to add syntax highlighting
it's immediately clear when you've designed a rule ambiguous or wrong, because when you refresh it's the wrong colour -- haha
Β½-edge!
Reactivescript is happening?
Yeah.... about 1:10 in this video, I have an animation demonstrating Β½-edge propergation:
Simulates the reactive update algorithmn
i'm all eyes!!!
It's not a great animation, needs a few tweaks.
Basic idea of a Β½ edge, is you have a one-way edge is some places from sink to source instead of a double edge (normal edge) between source and sink.
It allows back propergation, but not foward propergation in some places to achieve a O(1) createSelector.
so these bottom two fellows can dirty the first node, but will not be dirtied by it?
these two blue fellows on the bottom right
Exactly π―
i have a feature request for the reactivity simulator: a visible id on each of the nodes
haha
<-->-->: normal edge
<-->--: half edge
Maybe a clearer explanation:
You can only forward travel the direction of the mid-triangle arrows (but you can backtrack trough them if in backtracking stage), and you can only forward-enter if an end-triangle arrow is pointing to that node.
That untested code worked first go:
https://youtube.com/shorts/WfBU2lKWRWc?si=GeaB3TqN8DzP1Ttv
We now have animations in user scripts.
Lesgoooo
So probably a flip flag next, so we can face the other way.
Then try out the monsters. We have a monster logic system based on a coroutine DSL waiting to go.
So probably not far off something playable.
Ohh... the collision box for raccoon mario needs to be smaller than the sprite size, because of his tail. Otherwise you can not travel down 1 block wide holes. Some minor work will need to be done there too.
So probably an optional collision shape component for entities needs to be added too. If absent use sprite size, otherwise use collision shape.
I gotta document all this somewhere, to do an animation, you just add this component to your entity:
$.animatedComponentType.create({
textureAtlasFilename: "ss.json",
animationName: "walk",
frameIndex: 0,
})
($ is what I imported my prelude as. Since jquery is extincted, I figured I could use it.)
Then you just change the animation name to change between animations. The frameIndex auto increments, so u always initial set it to zero (unless you want to start at a different frame), and don't worry about it.
And you have to enable the animation system as well:
let dispose = $.useSystem("AnimationSystem");
So pretty minimal code from the users side.
Actually what I'll do next is the examples loader. So we can have examples we develop along side tool kitty, to help improve tool kitty, and also help document how to use it.
Over time a user can pick a example that is close to what they want to make, and just tweak it to make it their own. (E.g. change sprites, edit map, etc.)
Here we go:
Ohmp.... burn:
Sometimes... vite is being too helpful.
Here is the glob import:
const examples = {
"Mario Clone": {
skipPathCharCount: "../../examples/mario_clone/".length,
data: import.meta.glob(
"../../examples/mario_clone/**/*",
{
query: "?url",
import: "default",
},
),
},
};
If I do ?raw, then i can not do binary data, only text.
Another option rename .ts files to .ts.vite_dont_touch_me
I tried ?url&raw, but it just behaved like ?raw (overwrote url behaviour)
I need two globs, one for source files, one for binary data. No other way.
Importing examples working now. Code is not as clean as I would like (two globs instead of one), but it will do.
When an automerge file gets deleted at the moment, it more like it gets archived instead (keeps consuming indexed db space). It's recommended to completely wipe out / delete the file system on the start page button before importing an example in app v2.
The current automerge file system will need a tidy up one day.
... while I think about it, I might need a monster spawn system too, that decides when/were monsters appear via the meta data in the level cells.
Free sprites for reference:
https://kenney.nl/assets/pixel-platformer
Haven't done a lot today.
- Added a
FlipXComponentfor reusing right facing animations for left facing animations or vice versa. - Added
GmeSystem(game music engine) for chiptunes and sound effects.
kenney the legend
Added GmeSystem (game music engine) for chiptunes and sound effects.
fun! u already know how u want to approach it?
Not really sure. Just throwing things in.
I will still need another separate sound/music system for user assets. LibGME is just a cheat to give us something to play with, LibGME will basically allow us to play music and sound effects from any NES game ever made. But I do not have any editors or tool for making custom .nsf files (the NES chiptunes files).
Although....
https://famistudio.org/
Leverage :p
A little odd making music for a system that you can not longer buy, and then emulating the sound chip of that system so you can still play/use the music. But it's a workable pipeline.
I never really understood why all game music went to mp3. Chiptune music is so much more compact. Every level and sound effect in super mario brothers 3 for snes is 36Kb, and there is loads of music and sound effects in that game.
40 songs and 20 sound effects in 36Kb... try to do that mp3.
And that's uncompressed. Through gzip on top, it gets smaller.
20kb in zip. Only went down 30% :p
And the music quality is still good on today's hardware. Because the sound gets rendered. It's like an SVG but for sound.
A suppose that was the point of midis (svgs for sound). But they sort of died off.
Sound system demo:
https://youtube.com/shorts/z05seFMKSsw?feature=share
Note to self:
- add origin and hit box to the frames in texture atlas.
Slowly piecing together the monster spawn system.
The shape:
export function createSpawnSystem(params: {
world: EcsWorld,
doSpawn: (metaData: any) => string | undefined,
}): {
dispose: () => void,
}
It just reads the meta data of the map tiles to identify spawn points.
I guess for the same reasons why they bake lights: higher fidelity without having to compute.
Midi still very much alive and kicking in music software.
I don't know if I would describe it as svg for sound though. With an svg you can describe a full image. You could even make an svg that is an exact replica of an image (a rectangle for each pixel). It can range in complexity from little to a lot of noise.
Midi only captures the notes in time, not the timbre of the sound. You have to hook midi up to a sound engine for it to produce anything audible. It does not have the same quality as svg where u can scale up the complexity upto the "reql" (keeping that typo in) thing
Yeah. I guess that's true.
You can render at different rates though... 32KHz, 44KHz, 48KHz, that's kinda like a scale.
Sure. Is a bit like the framerate in a movie.
That's also the thing with sound, it's temporal
I guess you could define the sound wave as a series of vectors
Or make an svg of a spectogram and render that out
I would be pretty interested in hearing what that would sound like π€
In chiptunes the notes as pretty much formulas dependent on time.
You got your basic shapes you compose in different ways.
Square, triangle, saw and wave.
They compose to form a formula that can be plotted at any resolution.
Thus you can have 48KHz Nintendo music, even though the system probably played it as a lower rate.
Lower resolutions would add some artefacts and low pass filter the sound a bit
The low pass filter is kind of interesting, but it makes sense when you think of it: higher resolution means you can capture faster change aka higher frequencies.
It's like a known trick from 40 (drake producer) to put a low bitrate effect on his beats to give it that water-y effect sound.
Should be easy to recreate in WebAudio
And noise was the other shape.
Interesting.
Makes me think of this Brian Eno quote:
Whatever you now find weird, ugly, uncomfortable and nasty about a new medium will surely become its signature. CD distortion, the jitteriness of digital video, the crap sound of 8-bit - all of these will be cherished and emulated as soon as they can be avoided. Itβs the sound of failure: so much modern art is the sound of things going out of control, of a medium pushing to its limits and breaking apart. The distorted guitar sound is the sound of something too loud for the medium supposed to carry it. The blues singer with the cracked voice is the sound of an emotional cry too powerful for the throat that releases it. The excitement of grainy film, of bleached-out black and white, is the excitement of witnessing events too momentous for the medium assigned to record them.
Yet mp3 uses the same method to compress that temporal data that jpeg to compress its spacial data and it works well.
Maybe spacial and temporal are not that different.
absolutely
raw audio is simply an array of numbers
you can describe it a as a curve
use fourier transform to map it back and forward from time domain to frequency domain
spectograms are really cool too imo
*spacial -> spartial (typo)
i m all about the typo π
Gotta keep it reql π
i wonder if mp3 would use spectograms to do the compression on
would make sense
All I know is it uses the num of cosine waves at increasing frequencies
And the data is all the magnitudes of those waves composed together.
ye that is basically the idea of fourier: all sounds can be described as a combination of frequencies
*all curves
Data compaction happens due to the fact that lower frequencies are more common.
So you can use fewer bits for storing the high frequency stuff. I think
That's how it is in jpeg, and mp3 is similar.
I guess it has the added benefit of removing noise: as noise is the highest amount of detail. I think that's what the purpose was of low pass filters originally.
Getting through the spawn system slowly. It's a bit painful.
- newly seen spawn cell -> spawn unless monster is already spawned and your following it.
- monster goes off screen -> despawn but don't respawn until it's spawn cell leaves the screen and re-enters the screen
Not as simple as, see a spawn cell -> spawn.