OpenWorldVGDL/javascript/core/games.js

826 lines
22 KiB
JavaScript
Raw Normal View History

2025-01-17 13:42:05 +00:00
import { defaultDict, includesArray, initializeDistribution } from "./tools.js";
import { colorDict, DARKGRAY, GOLD } from "./ontology/constants.js";
import { EOS, Immovable, VGDLSprite } from "./ontology/vgdl-sprite.js";
import { MovingAvatar } from "./ontology/avatar.js";
import { Termination } from "./ontology/termination.js";
import { scoreChange, stochastic_effects } from "./ontology/effect.js";
import { Resource } from "./ontology/resource";
import { ContinuousPhysics, distance, quickDistance } from "./ontology/physics.js";
const MAX_SPRITES = 10000;
export class BasicGame {
default_mapping = {
w: ["wall"],
A: ["avatar"],
};
block_size = 10;
load_save_enabled = true;
disableContinuousKeyPress = true;
image_dir = "../sprites";
args = {};
score = 0;
bonus_score = 0;
real_start_time = 0;
real_time = 0;
time = 0;
ended = false;
num_sprites = 0;
kill_list = [];
all_killed = [];
frame_rate = 20;
sprite_constr = {
wall: [Immovable, { color: DARKGRAY }, ["wall"]],
avatar: [MovingAvatar, {}, ["avatar"]],
};
// z-level of sprite types (in case of overlap)
sprite_order = ["wall", "avatar"];
// contains instance lists
/**
* @type {Object.<string, VGDLSprite[]>}
*/
sprite_groups = {};
// which sprite types (abstract or not) are singletons?
singletons = [];
// collision effects (ordered by execution order)
collision_eff = [];
collision_types = [];
playback_actions = [];
playbacx_index = 0;
// for reading levels
char_mapping = {};
// temination criteria
terminations = [new Termination()];
// conditional criteria
conditions = [];
// resource properties
resources_limits = new defaultDict(2);
resources_colors = new defaultDict(GOLD);
is_stochastic = false;
_lastsaved = null;
win = null;
effectList = []; // list of effects this happened this current time step
spriteDistribution = {};
movement_options = {};
all_objects = null;
lastcollisions = {};
steps = 0;
gameStates = [];
realGameStates = [];
keystate = {};
EOS = new EOS();
sprite_bonus_granted_on_timestep = -1; // to ensure you only grant bonus once per timestep (since you check _isDone() multiple times)
timeout_bonus_granted_on_timestep = -1; // to ensure you only grant bonus once per timestep (since you check _isDone() multiple times)
shieldedEffects = {};
paused = true;
FPS = false;
use_frame = false;
objectTypes = {};
no_players = 1;
ignoredattributes = [
"stypes",
"name",
"lastmove",
// 'color',
"lastrect",
"resources",
"physicstype",
"physics",
"rect",
"alternate_keys",
"res_type",
"stype",
"ammo",
"draw_arrow",
"shrink_factor",
"prob",
"is_stochastic",
"cooldown",
"total",
"is_static",
"noiseLevel",
"angle_diff",
"only_active",
"airsteering",
"strength",
];
constructor(args) {
this.args = args;
for (const argsKey in args) {
this[argsKey] = args[argsKey];
}
this.reset();
}
reset = () => {
this.score = 0;
this.bonus_score = 0;
this.real_start_time = 0;
this.real_time = 0;
this.time = 0;
this.ended = false;
this.num_sprites = 0;
this.kill_list = [];
this.all_killed = [];
this.paused = true;
this.shieldedEffects = {};
};
resetLevel = () => {
this.reset();
this.buildLevel(this.level);
};
buildLevel = (lstr) => {
let lines = lstr
.split("\n")
.map((l) => {
return l.trimEnd();
})
.filter((l) => {
return l.length > 0;
});
let lengths = lines.map((line) => line.length);
// console.assert(Math.min.apply(null, lengths) === Math.max.apply(null, lengths), "Inconsistent line lengths");
this.width = Math.max(...lengths);
this.height = lines.length;
// console.assert(this.width > 1 && this.height > 1, "Level too small");
//Set up resources
for (let res_type in this.sprite_constr) {
if (!this.sprite_constr.hasOwnProperty(res_type)) continue;
let [sclass, args, _] = this.sprite_constr[res_type];
if (new sclass(0, 0, args) instanceof Resource) {
if (args["res_type"]) {
res_type = args["res_type"];
}
if (args["color"]) {
this.resources_colors[res_type] = args["color"];
}
if (args["limit"]) {
this.resources_limits[res_type] = args["limit"];
}
} else {
this.sprite_groups[res_type] = [];
this.sprite_constr[res_type][1]["Z"] =
this.sprite_order.indexOf(res_type);
}
}
// create sprites
lines.forEach((line, row) => {
for (const col in line) {
let c = line[col];
if (c in this.char_mapping) {
const pos = [parseInt(col), row];
this._createSprite(this.char_mapping[c], pos);
} else if (c in this.default_mapping) {
const pos = [parseInt(col), row];
this._createSprite(this.default_mapping[c], pos);
}
}
});
this.kill_list = [];
this.collision_eff.forEach((item) => {
const effect = item[2];
if (stochastic_effects.indexOf(effect) !== -1) this.is_stochastic = true;
});
this.level = lstr;
// console.log(this.sprite_order)
};
getPath = (start, end) => {};
randomizeAvatar = () => {
if (this.getAvatars().length === 0) this._createSprite(["avatar"], [0, 0]);
};
current_avatar_idx = 0;
_createSprite = (keys, pos) => {
let res = [];
keys.forEach((key) => {
if (this.num_sprites > MAX_SPRITES) {
console.log("Sprite limit reached.");
return;
}
let [sclass, args, stypes] = this.sprite_constr[key];
let anyother = false;
stypes.reverse().forEach((pk) => {
if (this.singletons.contains(pk)) {
if (this.numSprites(pk) > 0) {
anyother = true;
}
}
});
if (anyother) return;
args.key = key;
let s = new sclass(pos, [1, 1], args);
s.stypes = stypes;
if(s instanceof MovingAvatar){
s.playerID = this.current_avatar_idx;
this.current_avatar_idx += 1;
}
if (this.sprite_groups[key]) this.sprite_groups[key].push(s);
else this.sprite_groups[key] = [s];
this.num_sprites += 1;
if (s.is_stochastic) this.is_stochastic = true;
res.push(s);
});
return res;
};
_createSprite_cheap = (key, pos) => {
const [sclass, args, stypes] = this.sprite_constr[key];
const s = new sclass(pos, [1, 1], args);
s.stypes = stypes;
this.sprite_groups[key].push(s);
this.num_sprites += 1;
return s;
};
_iterAll = (ignoreKilled = false, filter_noncollision = false) => {
if (this.sprite_order[this.sprite_order.length - 1] !== "avatar") {
this.sprite_order.remove("avatar");
this.sprite_order.push("avatar");
}
/**
* @type {VGDLSprite[]}
*/
const result = this.sprite_order.reduce((base, key) => {
if (this.sprite_groups[key] === undefined) return base;
if (ignoreKilled) {
return base.concat(
this.sprite_groups[key].filter((s) => !this.kill_list.includes(s)),
);
}
return base.concat(this.sprite_groups[key]);
}, []);
if (filter_noncollision) {
return result.filter((s) =>
s.stypes.filter(x => this.collision_types.includes(x)).length > 0
)
}
return result;
};
_iterAllExcept = (keys) => {
if (this.sprite_order[this.sprite_order.length - 1] !== "avatar") {
this.sprite_order.remove("avatar");
this.sprite_order.push("avatar");
}
return this.sprite_order.reduce((base, key) => {
if (key in keys) return base;
if (this.sprite_groups[key] === undefined) return base;
return base.concat(this.sprite_groups[key]);
}, []);
};
numSprites = (key) => {
const deleted = this.kill_list.filter((s) => {
return s.stypes[key];
}).length;
// if (key in this.sprite_groups) {
// //避免找父类情况
// if(this.sprite_groups[key].length !== 0)
// return this.sprite_groups[key].length-deleted;
// }
return (
this._iterAll().filter((s) => {
return s.stypes.contains(key);
}).length - deleted
); // Should be __iter__ - deleted
};
/**
*
* @param {string} key sprite type
* @returns {list[Object]}
*/
getSprites = (key) => {
if (this.sprite_groups[key] instanceof Array)
return this.sprite_groups[key].filter((s) => {
return this.kill_list.indexOf(s) === -1;
});
else
return this._iterAll().filter((s) => {
return s.stypes.contains(key) && this.kill_list.indexOf(s) === -1;
});
};
getAvatars = (key) => {
const res = [];
if (this.sprite_groups.hasOwnProperty(key)) {
const ss = this.sprite_groups[key];
if (ss && ss[0] instanceof MovingAvatar)
res.concat(ss.filter((s) => this.kill_list.indexOf(s) === -1));
}
return res;
};
getSubTypes = (key) => {
const getSub = (dict, key) => {
if (dict.hasOwnProperty(key)) {
return dict[key];
} else if (Object.keys(dict).length === 0) {
return null;
} else {
let result = null;
for (const child in dict) {
const a = getSub(dict[child], key);
if (a !== null) result = a;
}
return result;
}
};
return getSub(this.objectTypes, key);
};
getObjects = () => {
const obj_list = {};
const fs = this.getFullState();
const obs = Object.copy(fs["objects_cur"]);
for (let obj_type in obs) {
this.getSprites(obj_type).forEach((obj) => {
const features = {
color: colorDict[obj.color.toString()],
row: [obj.location.y],
};
const type_vector = {
color: colorDict[obj.color.toString()],
row: [obj.location.y],
};
obj_list[obj.ID] = {
sprite: obj,
position: [obj.location.x, obj.location.y],
features: features,
type: type_vector,
};
});
}
return obj_list;
};
getFullState = () => {
const ias = this.ignoredattributes;
const obs = {};
const killed = {};
/**
* @type {VGDLSprite[]}
*/
const objects = [];
const actions = Object.keys(this.keystate).filter((key) => {
return this.keystate[key];
});
this.steps += actions.length;
for (const key in this.sprite_groups) {
if (!this.sprite_groups.hasOwnProperty(key)) continue;
const ss = {};
const order = this.sprite_order.indexOf(key);
this.getSprites(key).forEach((s) => {
const attrs = {};
Object.keys(s).forEach((a) => {
let val = s[a];
if (ias.indexOf(a) === -1) {
attrs[a] = val;
}
attrs["Z"] = order;
});
if (s.resources) {
attrs["resources"] = s.resources; // Should be object
}
ss[s.ID] = Object.copy(attrs);
objects.push(s);
});
obs[key] = Object.copy(ss);
}
const object_cur = Object.copy(obs);
return {
frame: this.time,
score: this.bonus_score,
ended: this.ended,
win: this.win,
objects: objects,
objects_cur: object_cur,
killed: killed,
actions: actions,
events: this.effectList,
real_time: this.real_time,
};
};
/**
* Updates all sprites in the game.
* @param {number} delta - The time delta since the last update.
* This method iterates through all sprites and calls their update method, passing the game instance.
* If a sprite is not crashed, it will be updated; otherwise, it is skipped.
*/
_updateAll = (delta) => {
this._iterAll().forEach((sprite) => {
// try {
if (!sprite.crashed) sprite.update(this, delta);
// } catch (err) {
// if ((!sprite.crashed)) {
// console.error('could not update', sprite.name)
// throw err
// sprite.crashed = true;
// }
// }
});
};
_subUpdate = (sub_idx, sum_idx) => {
this._iterAll().forEach((sprite) => {
if (!sprite.crashed) sprite.subUpdate(this, sub_idx, sum_idx);
});
}
_clearAll = (onscreen = true) => {
this.kill_list.forEach((s) => {
this.all_killed.push(s);
this.sprite_groups[s.name].remove(s);
});
this.kill_list = [];
this.shieldedEffects = {};
};
_updateCollisionDict = (changedsprite) => {
for (let key in changedsprite.stypes) {
if (key in this.lastcollisions) delete this.lastcollisions[key];
}
};
_terminationHandling = () => {
let break_loop = false;
this.terminations.forEach((t) => {
//if (break_loop) return;
if (break_loop) return;
const [ended, win] = t.isDone(this);
this.ended = ended;
if (this.ended) {
if (win) {
//if (this.score <= 0)
//this.score += 1;
this.win = true;
} else {
//this.score -= 1;
this.win = false;
}
break_loop = true;
}
});
};
_getAllSpriteGroups = () => {
const lastcollisions = {};
this.collision_eff.forEach((eff) => {
const [class1, class2, effect, kwargs] = eff;
[class1, class2].forEach((sprite_class) => {
if (!(sprite_class in lastcollisions)) {
let sprite_array = [];
if (sprite_class in this.sprite_groups) {
sprite_array = this.sprite_groups[sprite_class].slice();
} else {
const sprites_array = [];
Object.keys(this.sprite_groups).forEach((key) => {
const sprites = this.sprite_groups[key].slice();
if (sprites.length && sprites[0].stypes.contains(sprite_class)) {
sprite_array = sprite_array.concat(sprites);
}
});
}
lastcollisions[sprite_class] = sprite_array;
}
});
});
return Object.assign(lastcollisions, this.sprite_groups);
};
_multi_effect = () => {
function r(sprite, partner, game, kwargs) {
let value;
for (let i = 0; i < arguments.length; i++) {
value = arguments[i](sprite, partner, game, kwargs);
}
return value;
}
return r;
};
get_effect = (stypes1, stypes2) => {
const res = [];
this.collision_eff.forEach((eff) => {
const class1 = eff[0];
const class2 = eff[1];
const eclass = eff[2];
if (
this.shieldedEffects[class1] && includesArray(this.shieldedEffects[class1], [class2, eclass])
) {
// console.log(
// `[GAMES] Check shield ${class1}, ${class2}, return`,
// );
return;
}
if (stypes1.includes(class1) && stypes2.includes(class2))
res.push({ reverse: false, effect: eff[2], kwargs: eff[3] });
else if (stypes1.includes(class2) && stypes2.includes(class1))
res.push({ reverse: true, effect: eff[2], kwargs: eff[3] });
});
return res;
};
_effectHandling = () => {
//最多处理碰撞7次
for (let iter_time = 0; iter_time < 7; iter_time++) {
// console.log("EffectHandling", iter_time)
this.updateCollision();
if (this.collision_set.length === 0) return;
// 确保collision按照顺序执行
for(let eff of this.collision_eff){
const class1 = eff[0]
const class2 = eff[1]
const eclass = eff[2]
const used_collision = [];
for (const collision of this.collision_set) {
const stypes1 = collision[0].stypes;
const stypes2 = collision[1].stypes;
let effects = [];
if (collision[1] === "EOS" || collision[1] === "eos") {
if (
stypes1.includes(class1) &&
(class2 === "EOS" || class2 === "eos")
)
effects = [{ reverse: false, effect: eff[2], kwargs: eff[3] }];
}
else {
if (
this.shieldedEffects[class1] && includesArray(this.shieldedEffects[class1], [class2, eclass])
) {
return;
}
if (stypes1.includes(class1) && stypes2.includes(class2))
effects.push({ reverse: false, effect: eff[2], kwargs: eff[3] });
else if (stypes1.includes(class2) && stypes2.includes(class1))
effects.push({ reverse: true, effect: eff[2], kwargs: eff[3] });
};
if (effects.length === 0) continue;
used_collision.push(collision);
for (const effect_set of effects) {
let [sprite, partner] = [collision[0], collision[1]];
if (effect_set.reverse) {
[sprite, partner] = [collision[1], collision[0]];
}
effect_set.effect(sprite, partner, this, effect_set.kwargs);
}
}
// this.collision_set = this.collision_set.filter((c) => !used_collision.includes(c));
}
this.collision_set = [];
}
};
run = (on_game_end) => {
this.on_game_end = on_game_end;
return this.startGame;
};
presskey = (keyCode, playerID = 0) => {
// console.log(`Press Button: ${keyCode}`)
if (this.key_handler === "Pulse") return;
const key = `${keyCode}${playerID}`;
// console.log(`Presskey`, key)
this.keystate[key] = true;
// this.key_to_clean?.push(key);
// if (this.key_handler === "Pulse") {
// this.update(0, true);
// }
};
presskeyUp = (keyCode, playerID = 0) => {
const key = `${keyCode}${playerID}`;
if (this.key_handler === "Pulse") {
this.keystate[key] = true;
this.key_to_clean?.push(key);
return;
}
this.keystate[key] = false;
this.key_to_clean?.push(key);
// this.update(0, true)
};
// updateTime = 1000 / 10;
updateTime = 1/15;
currentTime = 0;
collision_set = [];
addCollisions = (a, b) => {
return;
};
addShield = (a, stype, ftype) => {
for(const a_type of a){
if(!this.shieldedEffects[a_type])
this.shieldedEffects[a_type] = []
if(includesArray(this.shieldedEffects[a_type], [stype, ftype])) continue;
this.shieldedEffects[a_type].push([stype, ftype]);
}
// console.log("add shield", a, stype, ftype, this.shieldedEffects)
};
updateCollision = () => {
//TODO: 使用最简单的方法实现,到非格子的方法可能会有问题
// console.log("iter all")
const allSprites = this._iterAll(true, true);
// console.log("updateCollision", allSprites.length)
for (let i = 0; i < allSprites.length; i++) {
const sprite1 = allSprites[i];
if (
sprite1.location.x < 0 ||
sprite1.location.x > this.width ||
sprite1.location.y < 0 ||
sprite1.location.y > this.height
) {
this.collision_set.push([sprite1, "EOS"]);
}
for (let j = i + 1; j < allSprites.length; j++) {
const sprite2 = allSprites[j];
const dist = quickDistance(sprite1, sprite2);
if (dist <= 0.99) {
this.collision_set.push([sprite1, sprite2]);
}
}
}
};
/**
*
* @param {number} delta, time in seconds
* @param {boolean} now, if true, update will be called immediately
* @returns {null | string | undefined} return null if game is paused, return string if game is ended, return undefined if game is running
*/
update = (delta, now = false) => {
// if (this.key_handler === "Pulse") {
// if (!now) return;
// }
if (this.use_frame === false){
if (!now) {
this.currentTime += delta;
// console.log("BasicGame update", now, this.use_frame, delta, this.use_frame, this.currentTime, this.updateTime, this.currentTime < this.updateTime)
if (this.currentTime < this.updateTime) {
// console.log("return")
return;
}
this.currentTime %= this.updateTime;
}
}
if (this.paused) return "paused";
if (this.ended) {
this.paused = true;
this.on_game_end(this.getFullState());
return this.win;
}
if(this.use_frame === true)
this.time += delta * 15;
else this.time ++;
this._terminationHandling();
this._clearAll();
this._updateAll();
this._effectHandling();
for (const keycode of this.key_to_clean) {
this.keystate[keycode] = false;
}
this.key_to_clean = [];
this.collision_set = [];
};
/**
* Start the game.
*/
startGame = () => {
this.reset();
this.paused = false;
this.key_to_clean = [];
let sprite_types = [Immovable];
this.all_objects = this.getObjects(); // Save all objects, some which may be killed in game
const objects = this.getObjects();
this.spriteDistribution = {};
this.movement_options = {};
Object.keys(objects).forEach((sprite) => {
this.spriteDistribution[sprite] = initializeDistribution(sprite_types);
this.movement_options[sprite] = { OTHER: {} };
sprite_types.forEach((sprite_type) => {
this.movement_options[sprite][sprite_type] = {};
});
});
// This should actually be in a game loop function, or something.
// this._clearAll();
Object.keys(objects).forEach((sprite_number) => {
let sprite = objects[sprite_number];
if (!(this.spriteDistribution in sprite)) {
this.all_objects[sprite] = objects[sprite];
this.spriteDistribution[sprite] = initializeDistribution(sprite_types);
this.movement_options[sprite] = { OTHER: {} };
sprite_types.forEach((sprite_type) => {
this.movement_options[sprite][sprite_type] = {};
});
}
});
this.keystate = {};
this.keywait = {};
// cache collision types
this.collision_types = [];
this.collision_eff.forEach((eff) => {
const object = eff[0];
const subject = eff[1];
if(!this.collision_types.includes(object))
this.collision_types.push(object)
if(!this.collision_types.includes(subject))
this.collision_types.push(subject)
});
// console.log(this.collision_types)
};
getPossibleActions = () => {
return this.getAvatars()[0].declare_possible_actions();
};
}