League of Legends(以下LoL)というゲームをプレイしてると一緒にプレイしているフレンドからこんなこと言われました。
フ「チャンピオン(自分が操作するキャラクターの総称)のランダム選択っていつの間にか消えたよな」
私「たしかに。でも乱数使えば簡単に作れそうだよね」
フ「じゃあ作って」
私「・・・」
みたいなやり取りがあり、せっかくなのでMath.randomを使って作っちゃえと作りました。
ついでに乱数ってどんな時使うんだと思い
復習がてらに朝礼の発表とさせていただきます。
でgoogle検索した際のAIの回答は
乱数は、プログラム、統計調査、ゲーム、シミュレーションなど、様々な場面で活用されています。乱数を使うことで、より現実的なシミュレーションや、公正な抽選、安全なパスワードなどを実現することができます。
とのことでいろんなことに使えそう。
Math.random() は 0以上1未満のランダムな小数を返します。
console.log(Math.random()); // 例: 0.234, 0.892 など
Math.random() は小数なので、そのままでは整数にできません。
そこで Math.floor() を使って整数にします。
let num = Math.floor(Math.random() * 5); // 0〜4の整数
中間発表の際にS先生から頂いたコードを名前だけ変えたものです。
下のボタンをクリックすると、発表順がシャッフルされます。
このツールのjs解説:
$(function () {
$("#btn").click(function () {
// リストをシャッフル
// 1を返すとaがbより後に来る
// -1を返すとaがbより前に来る
let list = [1, -1];
$('ol').html(
$('ol>li').sort(function (a, b) {
return list[Math.floor(Math.random() * list.length)];
})
);
});
});
友人にも使ってもらえるように別ページにて公開してるのでこちらでは解説のみを載せます。
const version = '15.10.1'; //現在のlolのバージョン
//ロールの振り分けは手打ち
const roles = {
top: ['Sett', 'Aatrox', 'Riven', 'Malphite', 'Yorick', 'Fiora', 'Gwen', 'Kayle', 'Renekton', 'Urgot', 'Irelia', 'Warwick', 'Darius', 'Ornn', 'Teemo', 'Mordekaiser', 'Singed', 'Camille', 'Quinn', 'Shen', 'DrMundo', 'Gragas', 'Garen', 'Sion', 'Nasus', 'Olaf', 'Gangplank', "Chogath", 'Volibear', 'Jayce', 'Ambessa', 'Jax', 'Gnar', 'Pantheon', 'Kled', 'Tryndamere', 'Trundle', 'Vladimir', 'Yasuo', 'Kennen', 'Ryze', 'Cassiopeia', 'Vayne', 'Heimerdinger', 'Akali', 'MonkeyKing', 'Poppy', 'Yone', 'Illaoi', 'Sylas', 'Varus', 'Rumble', 'Zac', "KSante", 'TahmKench', 'Galio', 'Viktor', 'Aurora', 'Swain', 'Nidalee', 'Udyr'],
jungle: ['Naafiri', 'Kayn', 'Nocturne', 'XinZhao', 'Volibear', 'Gwen', 'JarvanIV', 'Warwick', 'Khazix', 'Yorick', 'Nunu', 'Lillia', 'Viego', 'Fiddlesticks', 'Ekko', 'Kindred', 'MasterYi', 'Shaco', 'Vi', 'Hecarim', 'Elise', 'Graves', 'Diana', 'Talon', 'LeeSin', 'Udyr', 'MonkeyKing', 'Ivern', 'Belveth', 'DrMundo', 'Briar', 'Amumu', 'Evelynn', 'Shyvana', 'Zac', 'RekSai', 'Pantheon', 'Darius', 'Rammus', 'Zyra', 'Karthus', 'Poppy', 'Rengar', 'Taliyah', 'Nidalee', 'Sejuani', 'Gragas', 'Jax', 'Brand', 'Qiyana', 'Skarner', 'Maokai', 'DrMundo'],
mid: ['Naafiri', 'Ahri', 'Anivia', 'Yasuo', 'Sylas', 'Zed', 'Xerath', 'Malzahar', 'Taliyah', 'Zoe', 'Hwei', 'Vex', 'Cassiopeia', 'Diana', 'Galio', 'Viktor', 'Velkoz', 'Quinn', 'Akali', 'Akshan', 'Swain', 'Veigar', 'Katarina', 'TwistedFate', 'Syndra', 'Lux', 'Chogath', 'Orianna', 'Lissandra', 'Talon', 'Kayle', 'Ryze', 'Ekko', 'Qiyana', 'Kassadin', 'AurelionSol', 'Pantheon', 'Neeko', 'Mel', 'Malphite', 'Fizz', 'Irelia', 'Leblanc', 'Yone', 'Garen', 'Gragas', 'Aurora', 'Jayce', 'Kennen', 'Azir', 'Annie', 'Gangplank', 'Brand', 'Ziggs', 'Corki', 'Tristana', 'Smolder'],
bot: ['Jinx', 'Lucian', 'Jhin', 'Tristana', 'MissFortune', 'Vayne', 'Yasuo', 'Nilah', 'Kaisa', 'Caitlyn', 'KogMaw', 'Varus', 'Swain', 'Xayah', 'Twitch', 'Sivir', 'Ashe', 'Ezreal', 'Smolder', 'Draven', 'Aphelios', 'Samira', 'Zeri', 'Corki', 'Hwei', 'Mel', 'Seraphine', 'Ziggs', 'Kalista','Senna'],
support: ['Lulu', 'Thresh', 'Nami', 'Braum', 'Fiddlesticks', 'Sona', 'Janna', 'Milio', 'Elise', 'Soraka', 'Velkoz', 'Rell', 'Alistar', 'Poppy', 'Pyke', 'Karma', 'Senna', 'Bard', 'Nautilus', 'Zilean', 'Taric', 'Blitzcrank', 'Maokai', 'Leona', 'Rakan', 'Pantheon', 'Morgana', 'Yuumi', 'Neeko', 'Zyra', 'Lux', 'Seraphine', 'Shaco', 'Xerath', 'Swain', 'Brand', 'TahmKench', 'Leblanc', 'Galio', 'Renata', 'Zoe', 'Hwei', 'Shen', 'Sylas', 'Mel', 'Gragas', 'Nidalee', 'Annie']
};
let championData = {};
// fetch apiでチャンピオン情報を事前に取得する
fetch(`https://ddragon.leagueoflegends.com/cdn/${version}/data/ja_JP/champion.json`)
.then(response => response.json())
.then(data => {
for (const champKey in data.data) {
const champ = data.data[champKey];
championData[champ.id] = champ.name;
}
});
function showRandomChampion(role) {
let list = [];
// allを押されたとき
if (role === 'all') {
// 全ロールを統合してリストに追加
for (const champs of Object.values(roles)) {
list = list.concat(champs);
}
} else {
//押されたロールのリストだけ追加
list = roles[role];
}
//math関数でランダムにチャンピオンを選出
const randomChampId = list[Math.floor(Math.random() * list.length)];
const imgUrl = `https://ddragon.leagueoflegends.com/cdn/15.8.1/img/champion/${randomChampId}.png`;
const displayName = championData[randomChampId] || randomChampId;
//ランダムに選択したチャンピオンのデータをhtmlに出力
document.getElementById('championContainer').innerHTML = `
<h2>${displayName}</h2>
<img src="${imgUrl}" alt="${displayName}">
`;
}
いくつかぱっと思いついた案を並べてみる
上のツールのチャンピオンを和洋中とかに変えたら簡単にできそう
(料理の画像を集めるのがめんどくさかったのでやってない)
最近みた映画に出てたエニグマという暗号機から着想
換字式暗号:文字を別の文字に置き換えて暗号を導き出す(例:a→e,e→x)
この変換先の文字をページを読み込むたびにランダムで変わるようにしたら暗号解読ゲームとして成り立ちそう
作ったやつ個人的に毎週金曜日に楽しみにしてるコンテンツの一つ
サイコロを3つ振ってその目に応じた役を表示させるだけなので簡単にできそう
作ったやつjsからcssの内容をいじれるのでカラーとかをずっと変わるようにできそう
現実的には目に悪そうなので使い道はあまりなさそう
あとゲーミングデバイスの発光は規則性がちゃんとあるので乱数を使うのは向いてないか
ほかにもこんな使い方どう?みたいなのあればください(懇願)
math.randomメソッドはシード値の設定ができないので再現性のない乱数発生メソッドです。
乱数として正常じゃねと思うだろうし、私もそう思います。ただゲームみたいなものを作りたいと考えた時に、ある程度再現性があったほうが都合が良い時もあります。
例えばDS版ポケモンでは、起動時の本体の時計の時間でシード値が決まり、乱数で出現するポケモンや能力が変わったりしますが、
逆に本体の時計を合わせることでシード値を固定させ同じ乱数を引くことができます。
再現性が無いということで検証などは難しく、検証が必要な物を実装したいときには不向きかもです。
(シード値を固定させることができる乱数もjsでやろうと思えばやれるのですが、その辺は興味があれば調べてみてください...)
(5/22追記)
ページ制作でシード値があったほうが良い実装例として、1つ案を考えてみました。
写真を20枚位用意して、『今日の写真』というページをつくりたい
(→写真を毎日日替わりで1枚表示させたい)
今日の日付からタイムスタンプを取得してそれをシード値として利用し、乱数を発生させる
※ここでMath.randomを利用するとページを読み込むたびに乱数が変わるため、『今日の写真』が更新するたびに変わってしまう。
日付によって発生する乱数が固定されたので、今日の一枚は再読み込みをしても同じ写真が表示される(はず)
できました。3ページ作りました。
cssの装飾は勿論無い、簡素なものになりますが、カンプ代わりに作ったメモ書きです
条件
1.20枚の写真をこちらで用意する
2.ページにはh1で「今日の写真」
その下に写真を一枚表示
その他操作できるものはない
3.写真の名前は1.jpg~20.JPGとする
①math.random()を使って、20枚の写真をランダムに1枚表示
→再読み込みすれば別の写真が表示されるはず
②jsで現在の年月日の午前0時のタイムスタンプを取得。その数値をシード値として、再現性のある乱数を生成。その数値に応じて画像を1枚表示
→日付を跨がないと別の写真にならないはず
③h1の下に日付入力欄を表示。入力された日の午前0時のタイムスタンプを取得。その数値をシード値として、再現性のある乱数を生成。その数値に応じて画像を一枚表示
→②のデモとして用意。入力された日付が変わるごとに写真が変わるはず(もちろんたまたま同じ写真が連続して出る可能性もあるはず。例えば2025/05/07では1.jpgが表示。この後5/8では2.jpgが表示。また5/7を入力しても1.jpgが表示されるはず)
まず①から。Math.random()を使ってランダムに表示させています。
"use strict";
//0~19までの整数をランダム生成したのちに、画像ファイル名が1~20なので最後1足す
const randomIndex = Math.floor(Math.random() * 20) + 1;
//htmlのphoto要素のsrcを更新(上で出た数字の画像ファイルを読み込む)
document.getElementById('photo').src = `images/${randomIndex}.JPG`;
このscriptを使ったページ
想定通り再読み込みで別の写真が表示されました。
では②。今日の日付を取得してその日の午前0時0分のタイムスタンプを取得。それをシード値として乱数を発生させています。
参考サイト:線形合同法についての英語wiki
"use strict";
// シード値から乱数を生成する関数(線形合同法)
// ぶっちゃけm,a,cには何入れてもいいけど、下の数値が安定するらしい
// ソース(https://en.wikipedia.org/wiki/Linear_congruential_generator)
function seededRandom(seed) {
//m,a,cは定数
const m = 2 ** 31;
const a = 1103515245;
const c = 12345;
seed = (a * seed + c) % m;
//できたシード値をmで割って1以下の小数点にしています。
return seed / m;
}
//現在の日時を取得
const now = new Date();
now.setHours(0, 0, 0, 0); // 午前0時に固定
//その日のミリ秒のタイムスタンプを取得してseedとして利用
const seed = now.getTime();
const rand = seededRandom(seed);
//seed値を1以下にした理由がここで、Math.floor()で0~19に数字を作る必要があった
const photoIndex = Math.floor(rand * 20) + 1;
document.getElementById('photo').src = `images/${photoIndex}.JPG`;
このscriptを使ったページ
ページを更新しても画像は変わりません。あとは翌日画像が変わっていれば成功でしょう
③です。これは朝礼用に日付を操作できるものになってます。
"use strict";
//これは②と一緒
function seededRandom(seed) {
const m = 2 ** 31;
const a = 1103515245;
const c = 12345;
seed = (a * seed + c) % m;
return seed / m;
}
//デモ用で日付を入力欄を取得しています。
const input = document.getElementById('dateInput');
//ユーザーが日付を変更したとき中の関数を実行します。
input.addEventListener('change', () => {
const selectedDate = new Date(input.value);
selectedDate.setHours(0, 0, 0, 0);
const seed = selectedDate.getTime();
const rand = seededRandom(seed);
const photoIndex = Math.floor(rand * 20) + 1;
document.getElementById('photo').src = `images/${photoIndex}.JPG`;
});
このscriptを使ったページ
ページを更新すれば日付情報が消えるので画像も表示されませんが、同じ日付を選んでも同じ画像が表示されているので成功でしょう
以上を朝礼『技術シェア』の再現性のある乱数の生成についての発表とさせてください。させていただきます。