+export const clearAll = names => state => {
+ const changes = names.reduce((acc, cur) => ({ ...acc, [cur]: true }), {});
+ return { ...state, ...changes };
+};
+
+export const unclearAll = names => state => {
+ const changes = names.reduce((acc, cur) => ({ ...acc, [cur]: false }), {});
+ return { ...state, ...changes };
+};
+
+export const countClearedLocations = (state, locations) =>
+ locations.reduce((acc, cur) => state[cur] ? acc + 1 : acc, 0);
+
+export const hasClearedLocations = (state, locations) =>
+ countClearedLocations(state, locations) === locations.length;
+
+export const getLocationStatus = (name, logic, state) => {
+ if (state[name]) return 'cleared';
+ if (logic[name]) return logic[name];
+ return logic.fallback;
+};
+
+export const getCombinedStatus = statuses => {
+ if (statuses.filter(s => s === 'cleared').length === statuses.length) {
+ return 'cleared';
+ }
+ if (statuses.filter(s => ['available', 'cleared'].includes(s)).length === statuses.length) {
+ return 'available';
+ }
+ if (statuses.filter(s => ['unavailable', 'cleared'].includes(s)).length === statuses.length) {
+ return 'unavailable';
+ }
+ return 'partial';
+};
+
+export const aggregateLocationStatus = (names, logic, state) => {
+ const statuses = names.map(name => getLocationStatus(name, logic, state));
+ return getCombinedStatus(statuses);
+};
+
+export const countRemainingLocations = (state, locations) =>
+ locations.reduce((acc, cur) => state[cur] ? acc : acc + 1, 0);
+
+export const getGanonCrystals = (config) => getConfigValue(config, 'ganon-crystals', 7);
+
+export const getGTCrystals = (config) => getConfigValue(config, 'gt-crystals', 7);
+
+export const getGTBoss = (state, which) => state[`gt-${which}-boss`];
+
+export const hasDungeonBoss = (state, dungeon) =>
+ !dungeon.boss || !!state[`${dungeon.id}-boss-defeated`];