Skip to content

Commit

Permalink
Finish game & start refactoring web-game
Browse files Browse the repository at this point in the history
  • Loading branch information
phisn committed Sep 21, 2024
1 parent 148a9ef commit e9b16b6
Show file tree
Hide file tree
Showing 26 changed files with 677 additions and 222 deletions.
10 changes: 10 additions & 0 deletions packages/game/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export class Game {
private moduleShape: ModuleShape
private moduleWorld: ModuleWorld

private started = false

constructor(config: GameConfig, deps: GameDependencies) {
this.store = new GameStore()

Expand All @@ -42,6 +44,14 @@ export class Game {
}

public onUpdate(input: GameInput) {
if (this.started === false) {
if (input.thrust === false) {
return
}

this.started = true
}

this.moduleLevel.onUpdate(input)
this.moduleRocket.onUpdate(input)
this.moduleWorld.onUpdate(input)
Expand Down
32 changes: 32 additions & 0 deletions packages/game/src/model/replay/replay-capture-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { f16round } from "@petamoriken/float16"
import { ReplayModel } from "../../../proto/replay"
import { GameInput } from "../../game"
import { ReplayFrame, replayFramesToBytes } from "./replay"

export class ReplayCaptureService {
private frames: ReplayFrame[] = []
private accRotation = 0

constructReplay() {
return ReplayModel.create({
frames: replayFramesToBytes(this.frames),
})
}

captureFrame(input: GameInput) {
let diff = f16round(input.rotation - this.accRotation)

if (Math.abs(diff) < 0.0001) {
diff = 0
}

this.accRotation += diff

this.frames.push({
diff,
thrust: input.thrust,
})

return this.accRotation
}
}
4 changes: 4 additions & 0 deletions packages/game/src/model/replay/replay-stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ReplayStats {
ticks: number
deaths: number
}
232 changes: 232 additions & 0 deletions packages/game/src/model/replay/replay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { getFloat16, setFloat16 } from "@petamoriken/float16"

export interface ReplayFrame {
diff: number
thrust: boolean
}

interface Packet {
write: (view: DataView, offset: number) => number
size: number
}

export function replayFramesToBytes(replayFrames: ReplayFrame[]) {
const packets = [...packDiffs(replayFrames), ...packThrusts(replayFrames)]
const packetsSize = packets.reduce((acc, packet) => acc + packet.size, 0)

const diffssize = packDiffs(replayFrames).reduce((acc, packet) => acc + packet.size, 0)
const thrustssize = packThrusts(replayFrames).reduce((acc, packet) => acc + packet.size, 0)

console.log("diffs", diffssize)
console.log("thrusts", thrustssize)

const u8 = new Uint8Array(packetsSize)
const view = new DataView(u8.buffer)

let offset = 0

for (const packet of packets) {
offset += packet.write(view, offset)
}

return u8
}

export function replayFramesFromBytes(bytes: Uint8Array) {
const view = new DataView(
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),
)

const [diffs, offset] = unpackDiffs(view, 0)
const thrusts = unpackThrusts(view, offset)

if (diffs.length !== thrusts.length) {
throw new Error("diffs and thrusts length mismatch")
}

return diffs.map((diff, i) => ({
diff,
thrust: thrusts[i],
}))
}

function packDiffs(frames: ReplayFrame[]): Packet[] {
enum packDiffType {
Zero = 0,
NonZero = 1,
}

interface packDiffZero {
type: packDiffType.Zero
count: number
}

interface packDiffNonZero {
type: packDiffType.NonZero
value: number
}

type packDiff = packDiffZero | packDiffNonZero

const packed: packDiff[] = []

for (const item of frames) {
const previous = packed.at(-1)
const itemType = item.diff === 0 ? packDiffType.Zero : packDiffType.NonZero

if (
itemType === packDiffType.Zero &&
previous &&
previous.type === packDiffType.Zero &&
previous.count < 255
) {
previous.count++
} else {
if (itemType === packDiffType.Zero) {
packed.push({
type: packDiffType.Zero,
count: 1,
})
} else {
packed.push({
type: packDiffType.NonZero,
value: item.diff,
})
}
}
}

const zerocount = packed.filter(ct => ct.type === packDiffType.Zero).length
const nonzerocount = packed.filter(ct => ct.type === packDiffType.NonZero).length
const uniquenonzerocount = new Set(
packed
.filter((ct): ct is packDiffNonZero => ct.type === packDiffType.NonZero)
.map(ct => ct.value),
).size

console.log("zerocount", zerocount)
console.log("nonzerocount", nonzerocount)
console.log("uniquenonzerocount", uniquenonzerocount)

return [
{
write: (view, offset) => {
view.setUint32(offset, packed.length, true)
return 4
},
size: 4,
},
...packed.map(
(ct): Packet => ({
write: (view, offset) => {
switch (ct.type) {
case packDiffType.Zero: {
view.setUint16(offset, 0)
view.setUint8(offset + 2, ct.count)
return 3
}
case packDiffType.NonZero: {
setFloat16(view, offset, ct.value, true)
return 2
}
}
},
size: ct.type === packDiffType.Zero ? 3 : 2,
}),
),
]
}

function unpackDiffs(view: DataView, offset: number) {
const length = view.getUint32(offset, true)
const diffs: number[] = []

offset += 4

for (let i = 0; i < length; i++) {
const diff = getFloat16(view, offset, true)

if (diff === 0) {
const count = view.getUint8(offset + 2)

for (let j = 0; j < count; j++) {
diffs.push(0)
}

offset += 3
} else {
diffs.push(diff)

offset += 2
}
}

return [diffs, offset] as const
}

function packThrusts([first, ...remaining]: ReplayFrame[]): Packet[] {
interface packThrust {
thrust: boolean
count: number
}

const packed: packThrust[] = [
{
thrust: first.thrust,
count: 1,
},
]

for (const item of remaining) {
const previous = packed.at(-1)!

if (previous.thrust === item.thrust && previous.count < 255) {
previous.count++
} else {
packed.push({
thrust: item.thrust,
count: 1,
})
}
}

return [
{
write: (view, offset) => {
view.setUint32(offset, packed.length, true)
return 4
},
size: 4,
},
...packed.map(
(ct): Packet => ({
write: (view, offset) => {
view.setUint8(offset, ct.thrust ? 1 : 0)
view.setUint8(offset + 1, ct.count)
return 2
},
size: 2,
}),
),
]
}

function unpackThrusts(view: DataView, offset: number) {
const length = view.getUint32(offset, true)
const thrusts: boolean[] = []

offset += 4

for (let i = 0; i < length; i++) {
const thrust = view.getUint8(offset) === 1
const count = view.getUint8(offset + 1)

for (let j = 0; j < count; j++) {
thrusts.push(thrust)
}

offset += 2
}

return thrusts
}
44 changes: 44 additions & 0 deletions packages/game/src/model/replay/runtime-from-replay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import RAPIER from "@dimforge/rapier2d"
import { ReplayModel } from "../../../proto/replay"
import { WorldConfig } from "../../../proto/world"
import { Game, GameInput } from "../../game"
import { GameStore } from "../store"
import { replayFramesFromBytes } from "./replay"

export function runtimeFromReplay<T>(
rapier: typeof RAPIER,
replay: ReplayModel,
world: WorldConfig,
gamemode: string,
initializer?: (store: GameStore) => T,
handler?: (input: GameInput, store: GameStore, context?: T) => void,
) {
const replayFrames = replayFramesFromBytes(replay.frames)
const game = new Game(
{
gamemode,
world,
},
{
rapier,
},
)

let accumulator = 0

const context = initializer?.(game.store)

for (const frame of replayFrames) {
accumulator += frame.diff

const input = {
rotation: accumulator,
thrust: frame.thrust,
}

game.onUpdate(input)
handler?.(input, game.store, context)
}

return game.store
}
24 changes: 24 additions & 0 deletions packages/game/src/model/replay/validate-replay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import RAPIER from "@dimforge/rapier2d"
import { ReplayModel } from "../../../proto/replay"
import { WorldConfig } from "../../../proto/world"
import { ReplayStats } from "./replay-stats"
import { runtimeFromReplay } from "./runtime-from-replay"

export function validateReplay(
rapier: typeof RAPIER,
replay: ReplayModel,
world: WorldConfig,
gamemode: string,
): ReplayStats | false {
const runtime = runtimeFromReplay(rapier, replay, world, gamemode)
const worldComponent = runtime.resources.get("summary")

if (worldComponent?.finished) {
return {
ticks: worldComponent.ticks,
deaths: worldComponent.deaths,
}
}

return false
}
Loading

0 comments on commit e9b16b6

Please sign in to comment.