כל מי שנדרש לפתח משחק אי פעם (כמוני למשל), סביר להניח שנתקל בצורך באקראיות. יש משחקים רבים ומצויינים שאין בהם אלמנטים של אקראיות, כאלה שמזמינים שליטה טכנית מושלמת מתוך תרגול חוזר. סופר מריו עולה לי כדוגמה מהירה, אבל רוב המשחקים של פעם, כלומר ממש-ממש של פעם, היו כאלה. צפויים.
הכלי הבסיסי ביותר של אקראיות ב-JS הוא פונקציית Math.random. זאת פונקציה פשוטה להפליא. היא לא מקבלת שום פרמטר ומחזירה ערך אקראי כלשהו בין 0 (כולל) לבין 1 (לא כולל).
בואו נתחיל לשחק עם זה קצת. נניח שאני רוצה לקבל באקראי 0 או 1. אם נעגל את הערך המתקבל, תצא לנו אחת משתי האפשרויות.
Math.round(Math.random())
שימו לב לניואנס קטנטן. הפונקציה הפנימית מכילה ערך אחד יותר קרוב ל-0 מאשר ל-1, הרי זה 0 בעצמו. אז באופן תיאורטי יש הסתברות גבוהה יותר לקבל 0 מאשר 1, אבל זה לגמרי בשוליים.
מה יקרה אם נרצה לקבל 0 או מספר אחר מ-1? הפתרון עדיין קל. נניח עבור 0 או 5:
Math.round(Math.random()) * 5
וזה עדיין פשוט מאד, גם אם המספר הראשון אינו 0. נניח 3 ו-5.
3 + Math.round(Math.random()) * 2
התכונה הזאת שמאפשרת לנו לקבל אחד משני מספרים יכולה לשמש אותנו עבור מקרים בהם אנחנו לא רוצים לקבל מספרים בכלל, אלא כל דבר שהוא מתוך שתי אפשרויות.
const zeroOrOne = Math.round(Math.random())
const twoOptions = ['one', 'two']
const random = twoOptions[zeroOrOne]
בואו נעבור ליותר מ-2 אפשרויות. בדוגמאות הקודמות הרחבנו את התוצאה המתקבלת. החוכמה כאן היא להגדיל את מרחב האפשרויות מתוכן מתקבלת התוצאה. או במילים אחרות:
Math.random() * n // between 0*n and almost 1*n
אבל בגלל שרובינו בני אדם ואנחנו אוהבים מספרים עגולים, אז הבה נעגילה.
Math.floor(Math.random() * 5) // 0, 1, 2, 3 or 4
תשאלו בצדק למה floor ולא round? כי הראשונה תיתן לנו מרחב אפשרויות זהה למכפיל, בעוד השניה תהיה גדולה ב-1 מהמכפיל (מבינים למה?). היתרון של האפשרות הראשונה היא שיש קשר מובהק בין המספר לתוצאה המתקבלת.
עכשיו נשאר רק לסגור את הסיפור עם קביעת שני הקצוות של של התוצאה המתקבלת: מינימום ומקסימום.
function random(min, max) {
const range = max - min
return min + Math.floor(Math.random() * range)
}
ומה אם אנחנו רוצים לקבל ערך אקראי מתוך רשימה כלשהי? נשלב פונקציה חדשה עם הקודמת.
function getRandomValue(array) {
return array[random(0, array.length)]
}
בונוס למתקדמים
אבל איך מתקבל מספר אקראי בכלל? אם חושבים על זה, אין יותר מדי דברים אקראיים בעבודה של המחשב.
חוץ מהשעון.
יום אחד אכתוב פוסט על Date (הבנתם? יום אחד? Date?), אבל בינתיים נסתפק בפונקציה הזאת:
Date.now()
זאת פונקציה שמחזירה את התאריך הנוכחי בצורה של אלפיות השניה, כאשר תאריך הבסיס הוא 1970.
חשבתי לעצמי "מה יקרה אם אדגום את הזמן הנוכחי ואקח את הסיפרה האחרונה?"
בואו נחלץ את הסיפרה האחרונה ממספר ארוך. יש כל מיני טריקים לעשות את זה. אני אמיר את המספר למחרוזת, אמשוך את התו האחרון ואמיר בחזרה למספר.
function getLastDigitOfNow() {
const str = Date.now().toString()
const lastChar = str.substr(str.length - 1)
return Number(lastChar)
}
לכאורה הכל פשוט, אבל הבדיקה של הפונקציה הזאת היא קצת טריקית… איך מתבקש לבדוק את הפונקציה? להריץ אותה בלולאה ולבדוק את התפלגות התוצאות. אם היא שווה משהו, בהרבה מאד הרצות תהיה התפלגות שווה של כל הספרות בין 0-9. הבעיה היא שלולאות נועלות את ההתקדמות של הקוד ואז יוצא משהו מאד מוזר. כאשר מריצים את הפונקציה מתקבל רק מספר אחד עבור כל התוצאות! אל דאגה, יש פתרון. במקום להריץ ברצף, נשתמש בטיימר.
const a = []
function addTo() {
a.push(getLastDigitOfNow())
if (a.length < 100) {
setTimeout(() => addTo(), 10) // Math.min(10, a.length))
} else {
console.log(a)
}
}
עד כאן נראה בסדר.
אבל בואו נסתכל על התוצאה בפועל. ונשים אותה מול הרצה של פונקציה אקראית סטנדרטית כמו בתחילת המאמר.
זאת התוצאה הסטנדרטית:
[8,3,0,2,8,2,9,0,3,5,1,7,5,9,4,2,0,3,9,4,4,8,2,5,2,5,9,5,4,6,9,4,2,8,7,4,9,0,6,2,8,8,8,2,1,1,3,9,4,5,2,8,0,9,9,6,5,1,3,2,6,1,2,0,4,0,6,3,3,4,1,9,3,9,0,6,3,9,2,4,4,8,3,1,1,6,3,9,5,3,6,0,5,2,0,4,0,7,4,1]
זאת התוצאה של פונקציית הטיימר:
[0,6,9,6,5,6,1,5,6,5,9,9,6,4,5,5,6,1,5,6,6,5,0,6,5,5,5,6,6,6,5,1,5,5,5,5,4,5,5,7,5,5,6,5,9,4,5,5,5,5,5,5,6,5,5,5,5,5,5,5,6,7,6,6,7,7,8,5,5,6,6,5,6,6,6,8,6,5,6,6,6,0,5,6,6,6,6,5,6,5,6,5,6,6,7,6,6,7,5,6]
משהו נראה כאן שונה, נכון? המון מספרים חוזרים על עצמם ברצף. למה?
הסיבה היא שהטיימר מתנהג בצורה צפויה. אם אחת לכמה רגעים הוא דוגם את השעון, הרי שאם הכל מושלם הוא ידגום את השעון בזמנים צפויים לגמרי, מה שייצר תבנית של תוצאות. אני חוזר ומדגיש: זאת בעיה של הדרך שבה אנחנו בודקים, לא הקונספט עצמו! ועדיין יש מה לעשות. אפשר לדגום את הזמן אחת לזמן משתנה. אפשר לשנות את הטיימר לפי אורך המחרוזת. זה ייצר פונקציה איטית מאד. אבל אפשר לנסות טריקים אחרים. לדוגמה, לקבוע את האינטרוול הבא על בסיס המספר שהתקבל. הנה לכם תרגיל מחשבתי: מה אפשר לעשות בשביל להגביר את האקראיות של התוצאות?
אם אתם רוצים לדעת את התפלגות התוצאות שלכם, אפשר להשתמש בפונקציה של Lodash:
_.countBy(array, Math.floor)
לסיכום
תמיד נחמד ללמוד על פתרונות מקובלים לבעיות מוכרות. אבל הרבה יותר מעניין להשתמש בזה בתור קרש קפיצה בשביל להפעיל את המוח ולחשוב יצירתי.
זה היה מאמר ארוך עם הרבה קוד ובלי אף תמונה.
אז הנה תמונה שלך אם את\ה מפתח\ת סיניור\יטה ולא חידשתי לך כלום.