Skip to content

Commit 8108f5c

Browse files
Optimize the GameInput & Add randomness in game (#20)
* chore: pad highscore to support ENS * fix: update gameInput format to make it looks smaller and add genesis * feat: add randomness based on gameId (fixes #16) * rm: isEnded from state
1 parent 5df0e7e commit 8108f5c

11 files changed

+142
-53
lines changed

client/game/comets.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { IGameState } from "../comets";
22
import { fetchLeaderboard, fetchMruInfo } from "../rpc/api";
3-
import { removeFromStore, StorageKey } from "../rpc/storage";
3+
import { getFromStore, removeFromStore, StorageKey } from "../rpc/storage";
44
import { getWalletClient } from "../rpc/wallet";
55
import { AttractMode } from "./attractMode";
66
import { GameMode } from "./gameMode";
@@ -30,9 +30,13 @@ export class Comets {
3030
this.currentMode = this.attractMode;
3131
this.tickRecorder = new TickRecorder();
3232

33-
const setGameMode = () => {
33+
const setGameMode = (gameId: string) => {
3434
this.tickRecorder.reset();
35-
this.gameMode = new GameMode(new World(), this.tickRecorder);
35+
this.gameMode = new GameMode(new World(), {
36+
tickRecorder: this.tickRecorder,
37+
gameId,
38+
});
39+
3640
this.currentMode = this.gameMode;
3741

3842
this.gameMode.on("done", (source, world) => {
@@ -65,7 +69,7 @@ export class Comets {
6569

6670
this.attractMode.on("done", () => {
6771
console.log("Start Game");
68-
setGameMode();
72+
setGameMode(getFromStore(StorageKey.GAME_ID));
6973
});
7074
}
7175

client/game/constants.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Rect } from "../comets";
22

3-
export const WIDTH: number = 960;
4-
export const HEIGHT: number = 720;
5-
export const OBJECT_SCALE: number = 0.75;
6-
export const OFF_RECT: number = 120 * OBJECT_SCALE;
3+
export const WIDTH = 960;
4+
export const HEIGHT = 720;
5+
export const OBJECT_SCALE = 0.75;
6+
export const OFF_RECT = 120 * OBJECT_SCALE;
77
export const SHIP_RECT: Rect = {
88
x: WIDTH / 2 - OFF_RECT,
99
y: HEIGHT / 2 - OFF_RECT,

client/game/draw.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import { IQuadtree, Point, Rect } from "../comets";
2+
import Global from "./global";
13
import { Screen } from "./screen";
24
import { Ship } from "./ship";
35
import { random } from "./util";
4-
import Global from "./global";
5-
import { Point, Rect, IQuadtree } from "../comets";
66

77
const VectorLine = "rgba(255,255,255,.8)";
88
const TextColor = "rgba(255,255,255,.8)";

client/game/gameMode.ts

+22-10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Object2D } from "./object2d";
77
import { Screen } from "./screen";
88
import { Sound } from "./sounds";
99
import { Thumper } from "./thump";
10+
import { updateSeed } from "./util";
1011
import { World } from "./world";
1112

1213
export const ACTIONS = [
@@ -20,21 +21,32 @@ export const ACTIONS = [
2021
];
2122

2223
const WAIT_TIME = 5;
23-
24+
type Options = {
25+
gameId?: string;
26+
tickRecorder?: {
27+
recordInputs: (ticks) => void;
28+
};
29+
};
2430
export class GameMode extends EventSource implements IGameState {
2531
bounds: Object2D[] = [];
2632
thumper: Thumper;
27-
tickRecorder = {
28-
recordInputs: (ticks) => {},
29-
};
3033

3134
private lastCollisions: Collisions;
35+
options: Options;
3236

33-
constructor(private world: World, tickRecorder) {
37+
constructor(private world: World, options?: Options) {
3438
super();
35-
if (tickRecorder) {
36-
this.tickRecorder = tickRecorder;
39+
this.options = {
40+
tickRecorder: {
41+
recordInputs: () => {},
42+
},
43+
gameId: options?.gameId,
44+
};
45+
46+
if (options?.tickRecorder) {
47+
this.options.tickRecorder = options.tickRecorder;
3748
}
49+
updateSeed(this.options.gameId);
3850
}
3951

4052
init() {
@@ -44,8 +56,8 @@ export class GameMode extends EventSource implements IGameState {
4456
this.thumper = new Thumper();
4557
}
4658

47-
deserializeAndUpdate(dt: number, input: { v: string }) {
48-
const vi = input.v.split("").reduce((acc, action, index) => {
59+
deserializeAndUpdate(dt: number, input: string) {
60+
const vi = input.split("").reduce((acc, action, index) => {
4961
acc[ACTIONS[index]] = action === "1";
5062
return acc;
5163
}, {});
@@ -54,7 +66,7 @@ export class GameMode extends EventSource implements IGameState {
5466
}
5567

5668
update(dt: number, inputs?: VirtualInput) {
57-
this.tickRecorder.recordInputs(inputs);
69+
this.options.tickRecorder.recordInputs(inputs);
5870
this.world.levelTimer += dt;
5971

6072
if (this.thumper && this.world.ship) {

client/game/highScoreMode.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,12 @@ export class HighScoreMode implements IGameState {
6262
const text = `${this.pad(i + 1, " ", 2)}.${this.pad(
6363
formatAddress(Highscores.scores[i].address),
6464
" ",
65-
6
66-
)} ${this.pad(Highscores.scores[i].score, " ", 8)}`;
65+
13
66+
)} ${this.pad("", " ", 2)} ${this.pad(
67+
Highscores.scores[i].score,
68+
" ",
69+
8
70+
)}`;
6771

6872
screen.draw.text2(text, screen.font.large, (width) => {
6973
return {

client/game/tickRecorder.ts

+7-8
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@ import { ACTIONS } from "./gameMode";
77
export class TickRecorder {
88
public ticks: VirtualInput[] = [];
99

10-
serializedTicks(): { v: string }[] {
11-
return this.ticks.map((input) => {
12-
return {
13-
v: ACTIONS.map((action) => {
14-
return input[action] ? "1" : "0";
15-
}).join(""),
16-
};
17-
});
10+
serializedTicks(): string {
11+
return this.ticks
12+
.map((input) => {
13+
return ACTIONS.map((action) => (input[action] ? "1" : "0"));
14+
})
15+
.map((tick) => parseInt(tick.join(""), 2))
16+
.join(",");
1817
}
1918

2019
public collectInputs(): VirtualInput {

client/game/util.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
// We use a fixed seed for deterministic behavior
2-
const SEED = 0.8;
1+
// default seed
2+
let SEED = 0.8;
3+
4+
export function updateSeed(gameId: string) {
5+
const num = parseInt(gameId.slice(0, 8), 16);
6+
SEED = num / Math.pow(10, String(num).length);
7+
}
38

49
export function random(start: number, end: number): number {
5-
return Math.floor(SEED * (end - start + 1)) + start;
10+
return Math.floor(SEED * (end - start + 1)) + start;
611
}
712

813
export function randomFloat(start: number, end: number): number {
9-
return SEED * (end - start) + start;
10-
}
14+
return SEED * (end - start) + start;
15+
}

rollup/genesis-state.json

+72-3
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,79 @@
22
"state": {
33
"games": [
44
{
5-
"id": "1",
5+
"id": "0x6d220b2af3ddf405e228e83def838dfec21b8e03834723c9687eab26efc5460c",
6+
"score": 12300,
7+
"player": "0xea5eE3665C8E4289842FaC79DFB3FA0d129fc735"
8+
},
9+
{
10+
"id": "0x76d0dbabfe810803768256d2936d42151d0c2aa8689896348626c25d697fb7db",
11+
"score": 19000,
12+
"player": "0xea5eE3665C8E4289842FaC79DFB3FA0d129fc735"
13+
},
14+
{
15+
"id": "0x7cc5fa142ca6cc3b5015a7469ac52b9140e57887f60866d39930111756a55e87",
16+
"score": 0,
17+
"player": "0xc607B0B98ceC1FB6e6d3c72039bEbd76924aE4a4"
18+
},
19+
{
20+
"id": "0x9723281952ec249412c6105cebb2376528a9c06023439ee163fe608ed4acba09",
21+
"score": 12700,
22+
"player": "0xc607B0B98ceC1FB6e6d3c72039bEbd76924aE4a4"
23+
},
24+
{
25+
"id": "0x5bd5ad703723427cfb29ed588bb70b6bf70f32474c7e833637190a6585baeeb9",
626
"score": 0,
7-
"player": "0x94bE2330e995A6A1ef2b563E801Ec04AA9DE03DE",
8-
"isEnded": false
27+
"player": "0xc607B0B98ceC1FB6e6d3c72039bEbd76924aE4a4"
28+
},
29+
{
30+
"id": "0xac898b60757cd2f465ff978c2e0fd41e63a5f4fc977eef67f3f65bbb61619b45",
31+
"score": 0,
32+
"player": "0xc607B0B98ceC1FB6e6d3c72039bEbd76924aE4a4"
33+
},
34+
{
35+
"id": "0x1ff2d75adf4be95c4bd827127c30924d945e8a354209b04de2217a73d15e417d",
36+
"score": 76940,
37+
"player": "0xc607B0B98ceC1FB6e6d3c72039bEbd76924aE4a4"
38+
},
39+
{
40+
"id": "0xb3e989fef8ee7aaaebf399d788694759366724c40874fa046fbd8f707e82c697",
41+
"score": 41130,
42+
"player": "0xc607B0B98ceC1FB6e6d3c72039bEbd76924aE4a4"
43+
},
44+
{
45+
"id": "0x357550192b1fbf93b963ff8c6a71cf522d279dd370a4c026e4aaef5dc51b668e",
46+
"score": 0,
47+
"player": "0xc607B0B98ceC1FB6e6d3c72039bEbd76924aE4a4"
48+
},
49+
{
50+
"id": "0xf2564ceb6327915a13709289af8c4d5b5c7104cee2c72494b732068d8839964e",
51+
"score": 41980,
52+
"player": "0x5CDe9727B12E3FD211a9Ec961E2e2f5767CA81B3"
53+
},
54+
{
55+
"id": "0x05efd499ec1720ed126b09322f575a54fa02535b0de22f7d8719b0a2de781488",
56+
"score": 23800,
57+
"player": "0xC2FdE45f9E0a77005493930f72819Fcf70210464"
58+
},
59+
{
60+
"id": "0xfb7ed74217e7955100c3154598caf03ce6da7c8441e278027f1679b9d5749bc5",
61+
"score": 19400,
62+
"player": "0x372e2D6f74eFA2C5A4C72DAC4A31da09E8505995"
63+
},
64+
{
65+
"id": "0x9cb51304cfdac6805aa3740c5bdcf1e5c027d1fce990fb93989305e18cd9817d",
66+
"score": 14400,
67+
"player": "0x372e2D6f74eFA2C5A4C72DAC4A31da09E8505995"
68+
},
69+
{
70+
"id": "0x72c75565034b1aa146b45f0e2f46e4c53707b94766596ccb5a5aade4315e3a94",
71+
"score": 7240,
72+
"player": "0x372e2D6f74eFA2C5A4C72DAC4A31da09E8505995"
73+
},
74+
{
75+
"id": "0xd679cbb4f8b671f7ad90f9ee7f936934095e36a33a39719e4bd732196b71bfe1",
76+
"score": 4840,
77+
"player": "0x372e2D6f74eFA2C5A4C72DAC4A31da09E8505995"
978
}
1079
]
1180
}

rollup/stackr/action.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,5 @@ export const EndGameSchema = new ActionSchema("endGame", {
88
gameId: SolidityType.UINT,
99
timestamp: SolidityType.UINT, // nonce
1010
score: SolidityType.UINT,
11-
gameInputs: [
12-
{
13-
v: SolidityType.STRING,
14-
},
15-
],
11+
gameInputs: SolidityType.STRING,
1612
});

rollup/stackr/machine.ts

-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { transitions } from "./transitions";
66
interface GameState {
77
score: number;
88
player: string;
9-
isEnded: boolean;
109
}
1110

1211
interface RawState {
@@ -38,7 +37,6 @@ export class AppState extends State<RawState, WrappedState> {
3837
id,
3938
score: wrappedState.games[id].score,
4039
player: wrappedState.games[id].player,
41-
isEnded: wrappedState.games[id].isEnded,
4240
}));
4341
return { games };
4442
},

rollup/stackr/transitions.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { STF, Transitions } from "@stackr/sdk/machine";
22
import { hashMessage } from "ethers";
3-
import { GameMode } from "../../client/game/gameMode";
3+
import { ACTIONS, GameMode } from "../../client/game/gameMode";
44
import { World } from "../../client/game/world";
55
import { AppState } from "./machine";
66

@@ -11,7 +11,7 @@ export type CreateGame = {
1111
export type ValidateGameInput = {
1212
gameId: number;
1313
score: number;
14-
gameInputs: { v: string }[];
14+
gameInputs: string;
1515
};
1616

1717
const startGame: STF<AppState, ValidateGameInput> = {
@@ -24,7 +24,6 @@ const startGame: STF<AppState, ValidateGameInput> = {
2424
id: gameId,
2525
score: 0,
2626
player: msgSender,
27-
isEnded: false,
2827
};
2928

3029
emit({
@@ -43,7 +42,7 @@ const endGame: STF<AppState, ValidateGameInput> = {
4342
throw new Error("Game not found");
4443
}
4544

46-
if (games[gameId].isEnded) {
45+
if (games[gameId].score > 0) {
4746
throw new Error("Game already ended");
4847
}
4948

@@ -52,17 +51,20 @@ const endGame: STF<AppState, ValidateGameInput> = {
5251
}
5352

5453
const world = new World();
55-
const gameMode = new GameMode(world);
56-
for (const t of gameInputs) {
57-
gameMode.deserializeAndUpdate(1 / 60, t);
54+
const gameMode = new GameMode(world, { gameId });
55+
const ticks = gameInputs
56+
.split(",")
57+
.map((tick) => Number(tick).toString(2).padStart(ACTIONS.length, "0"));
58+
59+
for (let i = 0; i < ticks.length; i++) {
60+
gameMode.deserializeAndUpdate(1 / 60, ticks[i]);
5861
}
5962

6063
if (world.score !== score) {
6164
throw new Error(`Failed to replay: ${world.score} !== ${score}`);
6265
}
6366

6467
games[gameId].score = score;
65-
games[gameId].isEnded = true;
6668

6769
return state;
6870
},

0 commit comments

Comments
 (0)