-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Finish game & start refactoring web-game
- Loading branch information
Showing
26 changed files
with
677 additions
and
222 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export interface ReplayStats { | ||
ticks: number | ||
deaths: number | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.