\n \n \n \n \n
\n Enjoying Chronoodle? Share your thoughts in a brief\n
\n Survey\n .\n
\n \n ',"neutral",!0,0,(()=>{e.dismissedCount+=1,this.saveToStorage(surveyKey,e)}));if(t){const o=t.querySelector(".survey-link");o&&o.addEventListener("click",(()=>{e.surveyClickedCount+=1,this.saveToStorage(surveyKey,e)}))}}static showSurvey(){const e="survey_0",t=this.getFromStorage(e)||{dismissedCount:0,surveyClickedCount:0};if(t.dismissedCount>=2||t.surveyClickedCount>=2)return;const o=this.showToast('\n \n
\n \n \n \n \n \n
\n Enjoying Chronoodle? Share your thoughts in a brief\n
\n Survey\n .\n
\n
\n ',"neutral",!0,0,(()=>{t.dismissedCount+=1,this.saveToStorage(e,t)}));if(o){const r=o.querySelector(".survey-link");r&&r.addEventListener("click",(()=>{t.surveyClickedCount+=1,this.saveToStorage(e,t)}))}}static showToast(e,t="neutral",o=!0,r=3e3,n){const i=document.createElement("div");i.className=`toast ${t}`;const s=document.createElement("button");return s.className="close-btn",s.innerHTML="×",s.onclick=()=>{"function"==typeof n&&n(),i.remove()},i.innerHTML=e,o&&i.appendChild(s),document.getElementById("toaster").appendChild(i),r>0&&setTimeout((()=>{i.remove()}),r),i}static async LogReferral(e=null){const t=e=>new URLSearchParams(window.location.search).get(e);if((()=>{if(!document.referrer)return!1;return new URL(document.referrer).hostname===window.location.hostname})())return;let o={device_uuid:this.getDeviceUuid(),session_uuid:await this.getSessionUuid()};const r=t("utm_source"),n=t("utm_medium"),i=t("utm_campaign"),s=t("utm_term"),a=t("utm_content"),d=t("ref"),c=document.referrer;if(r&&(o.utm_source=r),n&&(o.utm_medium=n),i&&(o.utm_campaign=i),s&&(o.utm_term=s),a&&(o.utm_content=a),c&&(o.referrer_url=c),d&&(o.referral_code=d.slice(0,500)),o.landing_url=window.location.href,c||r||i||d){const t=`referral-logged-${d||"none"}`;if(sessionStorage.getItem(t))return;sessionStorage.setItem(t,"1"),fetch(window.chronoodleConfig.saveReferralEndpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}).then((e=>e.json())).then((t=>{e&&e(t)})).catch((e=>{console.error("Error logging referral:",e)}))}}static getQueryStringParam(e){return e?new URLSearchParams(window.location.search).get(e):null}static isQueryStringParamPresent(e){return!!e&&null!==this.getQueryStringParam(e)}static async startSession(){let e=this.getFromStorage("sessionUuid",sessionStorage),t=this.getFromStorage("deviceUuid");if(t||(t=`device_${this.generateUuid()}`,this.saveToStorage("deviceUuid",t)),!e){e=`session_${this.generateUuid()}`;const t=(new Date).toISOString();this.saveToStorage("sessionUuid",e,sessionStorage),this.saveToStorage("sessionStartDateTime",t,sessionStorage),this.isBot()||this.logSessionStart()}return e}static async getSessionUuid(){return await this.startSession()}static async logSessionStart(){const e={environment:this.getEnvironment(),deviceUuid:this.getDeviceUuid(),sessionUuid:await this.getSessionUuid(),deviceInfo:this.getDeviceInfo(),userAgent:navigator.userAgent};await(new OodleApi).post("/api/save-session-data",e)}static getUsernameInitials(){try{const e=this.getUsername().toUpperCase();return e?e.length<2?"":e.substring(0,1):""}catch(e){return"X"}}static getLocalYYYYMMDD(){const e=new Date;return`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")}`}static parseLocalDateString(e){const[t,o,r]=e.split("-").map(Number);return new Date(t,o-1,r)}static formatSmartDate(e){if(!e)return null;const t=e instanceof Date?e:this.parseLocalDateString(e),o=new Date,r=(e,t)=>e.getFullYear()===t.getFullYear()&&e.getMonth()===t.getMonth()&&e.getDate()===t.getDate(),n=new Date;return n.setDate(o.getDate()-1),r(t,o)?"TODAY":r(t,n)?"YESTERDAY":t.toLocaleDateString(void 0,{year:"numeric",month:"long",day:"numeric",weekday:"long"})}static getUserStartDate(){const e=this.Jwt.getClaim("createdAt");if(e){const t=Date.parse(e+"Z");if(!isNaN(t))return new Date(t)}return null}static getUsername(){return this.Jwt.getClaim("username")}static saveToStorage(e,t,o=localStorage){if("function"!=typeof o.setItem)throw new Error("Invalid storage provider: must implement setItem");o.setItem(e,JSON.stringify(t))}static getFromStorage(e,t=localStorage){if("function"!=typeof t.getItem)throw new Error("Invalid storage provider: must implement getItem");const o=t.getItem(e);try{return JSON.parse(o)}catch{return o}}static removeFromStorage(e,t=localStorage){if("function"!=typeof t.removeItem)throw new Error("Invalid storage provider: must implement removeItem");try{t.removeItem(e)}catch{return!1}return!0}static clearLocalStorageByPrefix(e){if("string"==typeof e&&""!==e.trim())for(const t in localStorage)t.startsWith(e)&&localStorage.removeItem(t);else console.warn("clearLocalStorageByPrefix: invalid prefix provided")}static saveToStorageWithExpiry(e,t,o){const r={value:t,expiry:(new Date).getTime()+o};this.saveToStorage(e,r)}static getFromStorageWithExpiry(e){const t=this.getFromStorage(e);if(!t)return null;return(new Date).getTime()>t.expiry?(this.removeFromStorage(e),null):t.value}static getDeviceUuid(){let e=this.getFromStorage("deviceUuid");return e||(e=`device_${this.generateUuid()}`,this.saveToStorage("deviceUuid",e)),e}static getEnvironment(){return window.chronoodleConfig.environment.toUpperCase()}static getDeviceInfo(){const e=navigator.platform;let t="Unknown OS";/Android/.test(navigator.userAgent)?t="Android":/iPhone|iPad|iPod/.test(navigator.userAgent)?t="iOS":e.includes("Mac")?t="MacOS":e.includes("Win")?t="Windows":e.includes("Linux")&&(t="Linux");const o=navigator.userAgent;let r="Unknown Browser";o.includes("Chrome")&&!o.includes("Chromium")?r="Chrome "+o.match(/Chrome\/(\d+\.\d+)/)[1]:o.includes("Firefox")?r="Firefox "+o.match(/Firefox\/(\d+\.\d+)/)[1]:o.includes("Safari")&&!o.includes("Chrome")?r="Safari "+o.match(/Version\/(\d+\.\d+)/)[1]:o.includes("MSIE")||o.includes("Trident")?r="Internet Explorer":o.includes("Edg")&&(r="Edge "+o.match(/Edg\/(\d+\.\d+)/)[1]);return`${t} | ${r} | ${/Mobi|Android/i.test(o)?"Mobile":"Desktop"} | ${`${window.screen.width}x${window.screen.height}`} | ${Intl.DateTimeFormat().resolvedOptions().timeZone} | ${navigator.language||navigator.userLanguage}`}static generateUuid(){return"undefined"!=typeof crypto&&"function"==typeof crypto.randomUUID?crypto.randomUUID():([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(e=>(e^crypto.getRandomValues(new Uint8Array(1))[0]&15>>e/4).toString(16)))}static isBot(){const e=navigator.userAgent.toLowerCase();return!!["bot","crawl","spider","slurp","mediapartners","googlebot","bingbot","baiduspider","yandex","duckduckbot","facebot","ia_archiver"].some((t=>e.includes(t)))||(!!navigator.webdriver||(0===window.screen.width||0===window.screen.height))}static mergeUniqueInArray(e,t){return[...new Set([...e,...t])]}static toggleDebugMode(){const e=document.getElementById("debug");e.classList.contains("open")?e.classList.remove("open"):e.classList.add("open")}static getFeatures(){const e=this.getFromStorage("featureFlags")||[],t=[],o=["enabled"],r=[{name:"'You Are Here' Mode",code:"you_are_here_mode",description:"Adds a visual callout to the 'Now' label in the game.",enabled:!1,rendered:!0,deletable:!1},{name:"Time Flip (Early Access)",code:"time_flip_mode",description:"Flips the chronological display order. When enabled, 'The Big Bang' will be at the top and 'Now' goes to the bottom.",enabled:!1,rendered:!0,deletable:!1},{name:"User Accounts",code:"user_accounts",description:"Sign in and save progress to your Oodle Games account",enabled:!0,rendered:!1,deletable:!1}];r.forEach((e=>{e.rendered||t.push(e.code)}));const n=e.filter((e=>!t.includes(e.code)));return r.forEach((e=>{const t=n.find((t=>t.code===e.code));t&&e.rendered?Object.keys(e).forEach((r=>{o.includes(r)||(t[r]=e[r])})):n.push(e)})),n.sort(((e,t)=>{const o=e.name.replace(/^[^a-zA-Z0-9]+/,"").toLowerCase(),r=t.name.replace(/^[^a-zA-Z0-9]+/,"").toLowerCase();return o.localeCompare(r)})),this.saveToStorage("featureFlags",n),n}static isFeatureAdded(e){return(this.getFromStorage("featureFlags")||[]).some((t=>t.code===e))}static disableFeature(e){let t=this.getFromStorage("featureFlags")||[];const o=t.findIndex((t=>t.code===e));-1!==o&&(t[o].enabled=!1,this.saveToStorage("featureFlags",t)),document.dispatchEvent(new Event("featureChange")),this.addTelemetry("FEATURE_FLAG",{code:e,enabled:!1})}static enableFeature(e){let t=this.getFromStorage("featureFlags")||[];const o=t.findIndex((t=>t.code===e));-1!==o&&(t[o].enabled=!0),this.saveToStorage("featureFlags",t),document.dispatchEvent(new Event("featureChange")),this.addTelemetry("FEATURE_FLAG",{code:e,enabled:!0})}static removeFeature(e){let t=this.getFromStorage("featureFlags")||[];t=t.filter((t=>t.code!==e)),this.saveToStorage("featureFlags",t),document.dispatchEvent(new Event("featureChange"))}static isFeatureEnabled(e){return(this.getFromStorage("featureFlags")||[]).some((t=>t.code===e&&t.enabled))}static async loadFeaturesBySecret(e){const t=await(new OodleApi).post("/api/v1/feature/get-by-secret",{secret:e});if(t.success&&t.data&&Array.isArray(t.data.features)&&t.data.features.length>0){const e=t.data.features,o=this.getFromStorage("featureFlags")||[];return e.forEach((e=>{o.find((t=>t.code===e.code))||o.push(e)})),this.saveToStorage("featureFlags",o),document.dispatchEvent(new Event("featureChange")),o}return null}static async sendLogToServer(e="INFO",t=null,o="",r="",n=""){const i={logLevel:e,logKey:t,title:o,message:r,details:n,deviceUuid:this.getDeviceUuid()||null,sessionUuid:await this.getSessionUuid()||null,source:"chronoodle.com"};(new OodleApi).enqueuePost("/api/v1/log",i)}static async addTelemetry(e,t){const o="undefined"!=typeof chronoodle,r=o&&chronoodle.currentChallengeKey?chronoodle.currentChallengeKey:null,n=o&&chronoodle.currentChallenge?String(chronoodle.currentChallenge.gameId):null,i=o&&chronoodle.getGameStateText?chronoodle.getGameStateText():null,s={environment:this.getEnvironment(),deviceUuid:this.getDeviceUuid(),sessionUuid:await this.getSessionUuid(),gameKey:r,gameId:n,eventType:e,gameData:t,gameState:i};await(new OodleApi).enqueuePost("/api/save-game-data",s)}static setCookie(e,t,o={}){let r=`${encodeURIComponent(e)}=${encodeURIComponent(t)}`;if(o.expires)if("number"===o.expires){const e=new Date;e.setTime(e.getTime()+1e3*o.expires),r+=`; expires=${e.toUTCString()}`}else o.expires instanceof Date&&(r+=`; expires=${o.expires.toUTCString()}`);o.path&&(r+=`; path=${o.path}`),o.domain&&(r+=`; domain=${o.domain}`),o.secure&&(r+="; secure"),o.sameSite&&(r+=`; SameSite=${o.sameSite}`),document.cookie=r}static getCookie(e){return document.cookie.split("; ").reduce(((e,t)=>{const[o,r]=t.split("=");return e[decodeURIComponent(o)]=decodeURIComponent(r),e}),{})[e]||null}static deleteCookie(e,t={}){let o=`${encodeURIComponent(e)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT`;t.path&&(o+=`; path=${t.path}`),t.domain&&(o+=`; domain=${t.domain}`),document.cookie=o}static createScrim(e="light",t=3){const o=document.createElement("div");return o.classList.add("basic-scrim"),"dark"===e&&o.classList.add("dark"),o.style.zIndex=t,document.body.appendChild(o),o}}"undefined"!=typeof module&&void 0!==module.exports&&(module.exports=Utils)
“Holes” Results & References Chronoodle relies on quality resources to ensure historical accuracy. By visiting these sources, you’re supporting the original authors and helping promote knowledge-sharing.
The first photo of a black hole is released 2019
Google Doodle first celebrates the "Hole Puncher" Nov 2017
British Masters Golf introduces the world's first "hole cam" Sep 2017
The "Wisconsin 'Cheesehead hat'" is invented 1987
The hole in the ozone layer is discovered 1985
The term "plot hole" comes into use 1949
Oxford English Dictionary
Captain Hanson Gregory invents the donut hole 1847
Impersonating: @hoodler
Exit Impersonation DEBUG MODE Copy game data Exit debug mode You can re-enable debug mode in Settings.