#ToolKitty

1 messages Β· Page 5 of 1

dark bay
#

I probably should do "
prelude/systems and prelude/components too maybe... not sure.

#

To sort of break up the runtime into logical chucks.

#

Nah... I'll leave that alone for now :p

zenith void
#

i think that's a good choice

dark bay
#

4000.... I was too slow... no controllable character yet πŸ˜†

zenith void
#

mm? 4000?

dark bay
#

Another cake πŸŽ‚

zenith void
#

ooo shit hahaha

#

congrats πŸŽ‰

dark bay
#

That didn't take long

zenith void
#

i know, goes so fast is crazy

#

like how even, it's like a week ago?

dark bay
#

Good thing we're not in off topic doing this... ppl will go nuts.

I mean they'll get annoyed. :p

zenith void
#

lol imagine

dark bay
#

Forgot one prelude/solid-js/store

dark bay
#

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

dark bay
dark bay
#

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.

dark bay
#

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.

dark bay
#

Down the road. We'll need to add a music composer & sound effect tools. :p

dark bay
dark bay
#

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Β²)

zenith void
zenith void
dark bay
dark bay
#

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.

zenith void
zenith void
#

solid-piano

dark bay
#

I'll come clean with that monad.
It's not my invention.
It's the Cont monad from Haskell.

zenith void
#

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.

zenith void
#

I should play around w other languages more. It's good inspiration

zenith void
dark bay
#

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.

dark bay
#

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.

zenith void
dark bay
#

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

dark bay
#

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.)

dark bay
#

Oh dear... AI might be hallucinating. Struggling with getting the implementation to type check in TypeScript. :p

zenith void
dark bay
dark bay
#

Last 10 Gemini attempts fail :p
Maybe using generators to make a nice do-like syntax for any monad is impossible in javascript.

dark bay
#

He even demoed coroutines (green threads)

#

Maybe I should of googled 1st, instead of going directly to AI.

#

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

dark bay
#

Still makes my head spin πŸ˜†

#

I keep having to reread the code over and over to wrap my head around it.

zenith void
#

Hahah ok good I m not the only one πŸ˜…

dark bay
#

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.

dark bay
#

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.

dark bay
#

When I get PC access next. I'll have a go at making that single indent level nested createComputed using generators.

dark bay
#

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

dark bay
#

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.)

dark bay
#

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)

dark bay
#

But I will still go ahead with our fluent interface version of Cont that uses pipe / then. It's type safe.

dark bay
#

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.

dark bay
#

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.

dark bay
#

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.

zenith void
dark bay
#

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.

dark bay
#

It is more compact. But not by much ig.

zenith void
dark bay
zenith void
#

Haha the eternal discussion 🀣

#

Tabs or spaces, and if spaces, how many

dark bay
#

Oh well. I'd be happy with 2 for side projects.
And will fit more tabs in termux.

dark bay
#

U win πŸ˜†

#

It's all 2 space tab size now.

dark bay
dark bay
#

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 😜

dark bay
#

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.

zenith void
#

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 πŸ˜…

dark bay
#

In the case of Array.map, B can be inferred, because B is the return type of the lambda.

dark bay
zenith void
dark bay
#

Oh sorry... k in that example. kontinue in PixiRenderSystem.

zenith void
#

gotcha!

dark bay
#

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.

zenith void
#

mm ye ig it's kind of impossible to get B inferred

dark bay
#

I'll keep trying. It would avoid a lot of typing. Especially in Termux.

dark bay
#

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

zenith void
dark bay
#

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.

zenith void
#

what about something like thenCont(createMemo)(...)?

dark bay
#

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.

dark bay
#

I use the identity function to help infer B

zenith void
#

what about something like this ?

#

(full disclosure: still don't really get Cont 🀣 )

dark bay
#

Well done @zenith void !! πŸŽ‰

#

It seems correct

zenith void
#

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)

dark bay
#

createComputeds inside createComputeds to keep the reactivity fine grain. Because we are rendering.

zenith void
#

or... be the same

dark bay
#

The same... if nested memos

zenith void
#

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

dark bay
#

Bcuz if nested we are not using the return value.

dark bay
#

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? πŸ˜†

zenith void
zenith void
#
function x <const T>(a: T) {}
x("hallo")

T here isn't string but "hallo"

dark bay
#

Ohh... not quite there. k / kontinue needs to be able to be called 0 or more times.

#

E.g. mapArray inside createMemo

zenith void
zenith void
zenith void
#

gotcha

dark bay
#

So kontinue is being called in mapArray

#

I think we're super close though. We have a solution for a single createComputed.

zenith void
dark bay
#

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.

zenith void
zenith void
#

when's ur bday? 😁

dark bay
#

It's probably on github :p

#

That's sensitive information :p

zenith void
dark bay
zenith void
#

yay, the origin story

#

bigmistqke was a design studio my buddy and i started when we were studying 😁

dark bay
zenith void
#

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

dark bay
#

All good πŸ‘

zenith void
#

it's effect -> effect -> signal

dark bay
zenith void
#

so then mb it IS more performant to do the chaining computeds πŸ€”

dark bay
zenith void
#

ye kind of interesting!

dark bay
#

When solid renders jsx, it is basically chaining computeds

zenith void
#

it is

const [value, setValue] = createSignal()
const [doubleValue, setDoubleValue] = createSignal()
createComputed(() => setDoubleValue(value() * 2))

vs

const [value, setValue] = createSignal()
const doubleValue = createMemo(() => value() * 2)
dark bay
#

In pixi we have no signals to set too. We're directly mutating pixi.

zenith void
#

a i see

dark bay
#

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.

zenith void
#

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)))));
}
dark bay
#

Yeah... and then fn needs to receive innerK so it can be used inside mapArray

zenith void
#

but then we are back to square 1 right?

dark bay
zenith void
#

i think if we wanna make the typescript gods happy we'll need to return the type

dark bay
#

It's wordy. Yes. But not as wordy as filling in the type parameter B.

#

.thenCCC / .thenCCCMA.

dark bay
#

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.

zenith void
#

looool

#

it's turing complete after all

dark bay
zenith void
#

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 }})?

dark bay
#

What if ContinuationType was used at the call site instead of in the thenCont method?

zenith void
#

(nvm)

dark bay
#

All good.

dark bay
#

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.

zenith void
#

the limitations of ts

dark bay
#

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.

dark bay
#

That didn't work either :p

dark bay
dark bay
#

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[].

#

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)

dark bay
dark bay
#

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.

dark bay
#

The funny thing is. It will be creating memo that are never read, just to please the type checker.

dark bay
#

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.

zenith void
#

that's with the rabbit holes, once you are in them you can't go out

zenith void
dark bay
#

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))
dark bay
#

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.

dark bay
#

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)

zenith void
zenith void
dark bay
#

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.

zenith void
dark bay
#

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.

dark bay
#

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)));
  }
dark bay
#

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.

dark bay
#

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([]);
dark bay
#

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), ...

dark bay
#

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.

zenith void
#

pretty neat ngl!

dark bay
#

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. 😜

#

Plus generators can only do single shot continuations. (Can't do all monads)

dark bay
#

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.

zenith void
dark bay
dark bay
#

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.

dark bay
#

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.

dark bay
#

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));
  }
zenith void
#

What a contraption πŸ˜‰

dark bay
#

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

dark bay
#

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.

dark bay
#

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.

dark bay
#

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.

dark bay
#

😭 ... 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.

dark bay
#

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();
    });
}
dark bay
#

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.

dark bay
#

Maybe contexts as a do-notation + return handles on values for return values. And watch the do-notation vanish.

dark bay
#

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();
    }
  });
}
dark bay
#

(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.

dark bay
#

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 πŸ˜›

#

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.

dark bay
#

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.

dark bay
dark bay
#

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

dark bay
#

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.

dark bay
#

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.

dark bay
#

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.

dark bay
#

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.

zenith void
#

makes sense

dark bay
#

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.

dark bay
#

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.

dark bay
#

One of the design principles of SOLID.

#

Or... the user can do it the system way. Up to the user.

dark bay
#

Maybe a system to register/unregister update code per component type.

dark bay
#

A goomba is just a simple example. There are monster with much more complex logic.

#

Not really that complicated. Just a few small circles and a swoop.

dark bay
#

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)

dark bay
#

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.

dark bay
#

When I get PC access again, I'll try to get something playable going. So we can play with different theories.

dark bay
#

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)

dark bay
#

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.

dark bay
dark bay
#

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.

dark bay
dark bay
#

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.

dark bay
dark bay
#

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?".

dark bay
#

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)

dark bay
#

(() => void)[] shape looks like it would suit simulating a CPU. 😜... indexed into via program counter (IP)

dark bay
#

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.)

dark bay
#

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()))
    ));
}
dark bay
#

My daughter got up me for covering her face in bricks. I told her its to protect her privacy.

dark bay
#

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.

dark bay
#

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.

dark bay
#

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.

dark bay
#

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.

zenith void
#

some wild javascript!

dark bay
# zenith void ye it's a bit headscratching for me tbh

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.

dark bay
#

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.

dark bay
#

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)

dark bay
#

I believe SVG animations are kind of like this DSL as well.

dark bay
#

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.)

dark bay
zenith void
#

makes me understand how it can tie in with scheduling of stuff

dark bay
#

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.

#

It's kinda like that debug/breakpoint example we saw from that guys blog about implementing callCC using generators.

dark bay
dark bay
#

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 -...

β–Ά Play video
dark bay
#

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.

dark bay
#

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.

dark bay
#

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.

dark bay
dark bay
#

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.

dark bay
#

Trying to do a vite plugin to patch valtown.

dark bay
#

Hmm.... what is this hiding in chee's littlebook:
tsconfig: ... allowSyntheticDefaultImports

#

No go. I already had that flag set to true. Will keep investigating.

dark bay
#

Last resort resort ig is copy paste all of valtown and recompile it to esm.

#

Here is the current battle for context:

dark bay
#

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 😜

dark bay
#

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:

dark bay
#

That error could be outside the glob too. I'll have to narrow down the location.

dark bay
#

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.

dark bay
#

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.

zenith void
#

Watching the ryan stream: apparently a new direction for signal 2.0, and it's going to be based on continuations!

dark bay
#

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.

#

And I suppose there is a lot of control flow power (e.g. coroutines)

dark bay
#

The issue I have with burrido is the generators yields can not be type checked. (They'll all have the same type.)

dark bay
#

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.

dark bay
#

Wonder if I can do this to avoid stripping:

// @ts-ignore                                 const ts = await import(/* vite-ignore */"http          s://esm.sh/[email protected]");
zenith void
#

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

zenith void
dark bay
#

It will need a lot of testing.

#

In languages without tail call elimination you run the risk of stack overflow for example.

zenith void
#

But they apparently already have an implementation

#

I think they are talking about it in next

dark bay
#

es6 for webkit (apple) has tail call elimination but not for Chrome.

dark bay
#

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:

dark bay
#

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.

zenith void
#

Amazing work random!

dark bay
#

Cheers

dark bay
dark bay
#

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 πŸ˜›

dark bay
#

There is a bug in my coroutines at the moment for when there is multiple monsters. I will need to sort that out first.

dark bay
#

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.

dark bay
#

If I write a helper function that converts a Cont into a per frame update function, it will simplify things greatly.

dark bay
#

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.

dark bay
#

The bug is fixed (via toPerFrameUpdateFn). We can now have multiple monsters:

dark bay
#

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.

dark bay
#

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.

dark bay
#

A 100% free tileset.

dark bay
#

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.

dark bay
#

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.

zenith void
#

sounds cool!

dark bay
#

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.

dark bay
#

Tried with 3 tiles to begin with. Looks like the image tile matcher works:

dark bay
#

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.

dark bay
#

Starting to get fun 😁

dark bay
#

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.

dark bay
#

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.

zenith void
#

Also, is it rendering pixels as svg?

dark bay
#

Also... each tile is an svg image of the whole tileset being cropped to that one tile.

#

Which is probably not ideal either.

zenith void
#

And the tilesset svg it renders pixels as filled in outlines?

dark bay
zenith void
#

A gotcha

dark bay
#

With a render param to remove antialiasing.

zenith void
#

Mm, seems like that should be able to work fine

dark bay
#

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.

dark bay
dark bay
#

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

zenith void
#

step by step πŸƒβ€β™‚οΈ

dark bay
#

Maybe I can slip in meta data before I get kicked off 😜

#

I need it to set up spawn points for monsters too.

dark bay
#

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...

zenith void
zenith void
dark bay
#

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.
dark bay
#

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.

dark bay
dark bay
#

Dam... I get home from work and there are some keys missing from the keyboard.

#

This is not my day 😜

dark bay
#

I'm missing the 8 * key and the 9 ( key.
So no multiplication or function calls... tough πŸ˜†

zenith void
#

Removed some of the syntax

dark bay
#

Praise Termux... she works without keys.

dark bay
#

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.

dark bay
#

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.

dark bay
#

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 πŸ˜†

dark bay
dark bay
# zenith void 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:

dark bay
#

So... 91% of kids have an attention span of less then 60 seconds πŸ˜†

dark bay
#

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.

dark bay
#

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.

dark bay
#

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.)

dark bay
#

@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

zenith void
#

Oo nested details, that's kind of neat!

dark bay
#

<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.

dark bay
#

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.

dark bay
#

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.

dark bay
dark bay
# dark bay I think the key will be doing it without TypeSchema or types. It just seems to g...

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.)

dark bay
zenith void
dark bay
dark bay
zenith void
dark bay
#

Yeah. It's pretty cool. Able to move and reorder.

dark bay
#

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

dark bay
dark bay
#

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.

dark bay
#

I'll need to write some async vitest tests to narrow it down.

dark bay
#

I'll study chee's vite tests. Mine are failing in the test console, but passing in the browser.

dark bay
#

... 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.

dark bay
#

No luck on version 3. I should of just tried to fix version 2. Rather than a rewrite (v3).
Proxies are definitely my kryptonite.

dark bay
#

God I hate proxies πŸ˜†

#

Will keep trying 2nyt in Termux in bed.

dark bay
#

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.

dark bay
#

Another challenge is. I believe an automerge doc can be temporarilly in an inconsistent state while it is still loading. (Incomplete shape)

dark bay
#

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.

dark bay
#

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.

dark bay
#

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.

dark bay
#
  • Virtual DPad system thrown in for testing purposes (no keyboard on mobile)
zenith void
#

Let's goooooo

#

Looking really good random!

dark bay
#

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.

dark bay
dark bay
#

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.

dark bay
dark bay
dark bay
#

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.

dark bay
#

Maybe I was meant to go from lowest entropy to highest entropy, not highest entropy to lowest entropy.

dark bay
#

Tiny bit better:

dark bay
#

Not too shabby:

#

I think I can be happy with that for now.

dark bay
#

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.

dark bay
#

Ohh... it's not a "Super Mushroom"... πŸ„

zenith void
dark bay
# dark bay Not too shabby:

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.

dark bay
#

the collision response logic is in src/mario.ts at the moment.

dark bay
#

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.

dark bay
#

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.

zenith void
dark bay
#

I just never got around to renaming the repo

zenith void
#

yes nice!

#

i always remember when i try to search for it 🀣

dark bay
#

Animation UI I'll keep simple for now.

#

You press "new animation", give it a name, pick frames, set speed.

dark bay
#

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.

dark bay
#

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.

zenith void
#

that little preview on the top right is fun πŸ™‚

dark bay
#

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.)

dark bay
#

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.

dark bay
#

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.

dark bay
#

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.

dark bay
#

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.

dark bay
#
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

dark bay
#

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.

dark bay
#

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.

dark bay
#

... 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.

dark bay
#

System::update: s -> Ξ”s

#

State::update: s -> Array<Ξ”s> -> s

zenith void
# dark bay 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.

dark bay
# dark bay `System::update: s -> Ξ”s`

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

dark bay
#

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.

zenith void
#

you double the state

#

is a bit like swapping the buffers w graphics programming

#

ping pong rendering?

dark bay
#

Yeah... I haven't had the chance to do much today or yesterday. Got a soft ban from the PC :p

dark bay
# zenith void you double the state

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.

dark bay
#

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.

zenith void
#

That GC jank

dark bay
#

Easier to dodge the gc in Rust than JavaScript. But we will try out best.

dark bay
#

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.

dark bay
#

It's not the theoretical correct one (system update order matters). But it will do for now.

dark bay
#

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.

zenith void
dark bay
#

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.

dark bay
#

... and a flip, to Β½ the number of animations. (Character walks left; Character walks right)

dark bay
#

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.

dark bay
#

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.

dark bay
zenith void
zenith void
#

@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

dark bay
#

I've read papers about incremental computations in the past. But I think they were different.

zenith void
#
dark bay
#

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.

zenith void
#

and so the prophecy continues Those Who Stay Long Enough In Solid Discord Will Write A Signal Implementation

dark bay
#

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.

zenith void
#

It's kind of nice that a signal implementation is not a lot of code

#

how do you propagate change over a tree

scarlet belfry
#

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

dark bay
dark bay
#

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.

scarlet belfry
#

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

dark bay
scarlet belfry
#

i have a feature request for the reactivity simulator: a visible id on each of the nodes

#

haha

dark bay
#

<-->-->: normal edge
<-->--: half edge

dark bay
zenith void
#

Lesgoooo

dark bay
#

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.

dark bay
#

So probably an optional collision shape component for entities needs to be added too. If absent use sprite size, otherwise use collision shape.

dark bay
#

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.

dark bay
#

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.)

dark bay
#

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",
      },
    ),
  },
};
dark bay
#

If I do ?raw, then i can not do binary data, only text.

#

Another option rename .ts files to .ts.vite_dont_touch_me

dark bay
#

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.

dark bay
#

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.

dark bay
#

... 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.

dark bay
dark bay
#

Haven't done a lot today.

  • Added a FlipXComponent for reusing right facing animations for left facing animations or vice versa.
  • Added GmeSystem (game music engine) for chiptunes and sound effects.
zenith void
dark bay
dark bay
#

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.

dark bay
#

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.

dark bay
#

Note to self:

  • add origin and hit box to the frames in texture atlas.
dark bay
#

Slowly piecing together the monster spawn system.

dark bay
#

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.

zenith void
zenith void
# dark bay A suppose that was the point of midis (svgs for sound). But they sort of died of...

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

dark bay
#

You can render at different rates though... 32KHz, 44KHz, 48KHz, that's kinda like a scale.

zenith void
#

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 πŸ€”

dark bay
#

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.

zenith void
#

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.

zenith void
dark bay
#

And noise was the other shape.

zenith void
# dark bay 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.

dark bay
#

Maybe spacial and temporal are not that different.

zenith void
#

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

dark bay
#

*spacial -> spartial (typo)

zenith void
#

*spartial -> spatial (?)

#

πŸ™‚

dark bay
#

Lol yeah πŸ˜†

#

My spelling is awful

zenith void
#

i m all about the typo πŸ™‚

dark bay
#

Gotta keep it reql πŸ˜‰

zenith void
#

would make sense

dark bay
#

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.

zenith void
#

ye that is basically the idea of fourier: all sounds can be described as a combination of frequencies

#

*all curves

dark bay
#

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.

zenith void
#

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.

dark bay
#

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.