drop();
// ------------------------------------------------------------------ stores
$stores = [];
foreach ($this->stores as [$code, $name, $kind, $color, $text]) {
$stores[$code] = [
'code' => $code,
'name' => $name,
'kind' => $kind,
'color' => $color,
'textcolor' => $text,
'logo' => '/img/stores/' . $code . '.png',
];
}
$db->selectCollection('stores')->insertMany(array_values($stores));
CLI::write('Inserted ' . count($stores) . ' stores.', 'green');
if (! is_dir(self::COVERS_DIR)) {
mkdir(self::COVERS_DIR, 0777, true);
}
// ------------------------------------------------------------------ games + deals + history
$officialCodes = array_keys(array_filter($stores, static fn ($s) => $s['kind'] === 'official'));
$keyshopCodes = array_keys(array_filter($stores, static fn ($s) => $s['kind'] === 'keyshop'));
$gamesBatch = [];
$dealsBatch = [];
$historyBatch = [];
$now = time();
foreach ($this->games as $i => [$title, $base, $year, $genres, $platforms, $score, $rating]) {
$slug = url_title($title, '-', true);
// all-time low: 25% - 60% of base
$hlFactor = 0.25 + (mt_rand(0, 35) / 100);
$hlPrice = round($base * $hlFactor, 2);
$hlPrice = $this->prettyPrice($hlPrice);
// historical low store + date (3-18 months ago)
$hlStoreCode = $this->pick(array_merge($officialCodes, $keyshopCodes));
$hlDaysAgo = mt_rand(90, 540);
$hlDate = $now - $hlDaysAgo * 86400;
// ---- official deals
$nOfficial = mt_rand(2, 5);
$officialStores = $this->pickN($officialCodes, $nOfficial);
// best current official price: between HL and 85% of base
$bestOfficial = round($hlPrice * (1 + mt_rand(0, 60) / 100), 2);
$bestOfficial = min($bestOfficial, round($base * 0.85, 2));
$bestOfficial = $this->prettyPrice($bestOfficial);
$officialDeals = [];
foreach ($officialStores as $j => $sc) {
$price = $j === 0
? $bestOfficial
: $this->prettyPrice(round($bestOfficial * (1 + mt_rand(5, 45) / 100), 2));
$price = min($price, $this->prettyPrice(round($base * 0.95, 2)));
$cut = round((1 - $price / $base) * 100);
$officialDeals[] = $this->dealDoc($slug, $sc, 'official', $price, $base, $cut, $hlPrice, $platforms, $title);
}
// ---- keyshop deals (usually cheaper, more stores)
$nKey = mt_rand(3, 8);
$keyStores = $this->pickN($keyshopCodes, $nKey);
$bestKey = $this->prettyPrice(round(max($hlPrice * (0.88 + mt_rand(0, 20) / 1000), 1.5), 2));
$bestKey = min($bestKey, $bestOfficial);
$keyshopDeals = [];
foreach ($keyStores as $j => $sc) {
$price = $j === 0
? $bestKey
: $this->prettyPrice(round($bestKey + mt_rand(0, 300) / 100, 2));
$cut = round((1 - $price / $base) * 100);
$keyshopDeals[] = $this->dealDoc($slug, $sc, 'keyshop', $price, $base, $cut, $hlPrice, $platforms, $title);
}
$dealsBatch = array_merge($dealsBatch, $officialDeals, $keyshopDeals);
// ---- price history: 2 official + 2 keyshop stores, point every 5 days for ~540 days
$histStores = array_merge(
$this->pickN($officialCodes, 2),
$this->pickN($keyshopCodes, 2)
);
foreach ($histStores as $sc) {
$kind = $stores[$sc]['kind'];
// this store's floor: make one store hit the all-time low
$floor = ($sc === $hlStoreCode) ? $hlPrice : $this->prettyPrice(round($hlPrice * (1.05 + mt_rand(0, 50) / 100), 2));
for ($d = 540; $d >= 0; $d -= 5) {
$ts = $now - $d * 86400;
// sales pattern: occasional dips (sales every ~2-4 months)
$inSale = (mt_rand(1, 72) <= 7);
if ($kind === 'keyshop') {
// keyshops are consistently low & noisy
$price = round($hlPrice * (1.0 + mt_rand(0, 90) / 100), 2);
} elseif ($inSale) {
$price = round($floor * (1 + mt_rand(0, 25) / 100), 2);
} else {
$price = round($base * (0.95 + mt_rand(0, 8) / 100), 2);
}
if ($price < $floor && $sc !== $hlStoreCode) {
$price = $floor;
}
$historyBatch[] = [
'game_slug' => $slug,
'store_code' => $sc,
'kind' => $kind,
'ts' => new UTCDateTime($ts * 1000),
'price' => round($price, 2),
];
}
}
$bestDealAll = $this->minDeal(array_merge($officialDeals, $keyshopDeals));
$activeDeals = count($officialDeals) + count($keyshopDeals);
$trending = mt_rand(35, 100) + $bestDealAll['cut'] * 2;
$palette = $this->palettes[$i % count($this->palettes)];
$coverFile = '/img/covers/' . $slug . '.svg';
$this->writeCover($slug, $title, $palette);
$gamesBatch[] = [
'slug' => $slug,
'title' => $title,
'base_price' => $base,
'genres' => $genres,
'platforms' => $platforms,
'score' => $score,
'rating_pct' => $rating,
'release_date' => new UTCDateTime(mktime(0, 0, 0, mt_rand(1, 12), mt_rand(1, 28), $year) * 1000),
'all_time_low' => [
'price' => $hlPrice,
'store' => $hlStoreCode,
'kind' => $stores[$hlStoreCode]['kind'],
'date' => new UTCDateTime($hlDate * 1000),
],
'best_official' => [
'price' => $officialDeals[0]['price'],
'store' => $officialDeals[0]['store_code'],
'cut' => $officialDeals[0]['cut'],
],
'best_keyshop' => [
'price' => $keyshopDeals[0]['price'],
'store' => $keyshopDeals[0]['store_code'],
'cut' => $keyshopDeals[0]['cut'],
],
'active_deals' => $activeDeals,
'trending' => $trending,
'cover' => ['file' => $coverFile, 'from' => $palette[0], 'to' => $palette[1]],
'developer' => $this->pick(['Studio Nova', 'Pixel Forge', 'Iron Tower Games', 'Blue Moon Interactive', 'Red Circuit', 'Crystal Works', 'Dark Harbor Studio', 'Ember Games']),
'publisher' => $this->pick(['Nova Publishing', 'PixelWorks', 'Global Games', 'East Coast Interactive', 'Stellar Publishing', 'Iron Crown']),
'length' => ['main' => mt_rand(6, 60), 'completion' => mt_rand(15, 150), 'perfect' => mt_rand(25, 250)],
'age_rating' => $this->pick(['E (Everyone)', 'E10+', 'T (Teen)', 'M (Mature 17+)']),
'description' => $this->makeDescription($title, $genres),
'sys_reqs' => [
'os' => $this->pick(['Windows 10 64-bit', 'Windows 11 64-bit']),
'cpu' => $this->pick(['Intel Core i5-8400', 'AMD Ryzen 5 2600', 'Intel Core i7-9700K', 'AMD Ryzen 7 3700X']),
'ram' => $this->pick(['8 GB', '16 GB']),
'gpu' => $this->pick(['NVIDIA GTX 1060', 'AMD RX 580', 'NVIDIA RTX 2060', 'NVIDIA RTX 3060']),
'storage' => mt_rand(15, 120) . ' GB',
],
];
}
$db->selectCollection('games')->insertMany($gamesBatch);
CLI::write('Inserted ' . count($gamesBatch) . ' games.', 'green');
// deals in chunks
foreach (array_chunk($dealsBatch, 500) as $chunk) {
$db->selectCollection('deals')->insertMany($chunk);
}
CLI::write('Inserted ' . count($dealsBatch) . ' deals.', 'green');
foreach (array_chunk($historyBatch, 1000) as $chunk) {
$db->selectCollection('price_history')->insertMany($chunk);
}
CLI::write('Inserted ' . count($historyBatch) . ' price history points.', 'green');
// ------------------------------------------------------------------ vouchers / news / prepaids
$this->seedVouchers($db);
$this->seedNews($db);
$this->seedPrepaids($db);
$this->seedSubscriptions($db);
$this->seedBundles($db);
$this->seedFranchises($db);
// ------------------------------------------------------------------ indexes
Mongo::collection('games')->createIndex(['slug' => 1], ['unique' => true]);
Mongo::collection('games')->createIndex(['trending' => -1]);
Mongo::collection('deals')->createIndex(['game_slug' => 1, 'price' => 1]);
Mongo::collection('deals')->createIndex(['store_code' => 1]);
Mongo::collection('price_history')->createIndex(['game_slug' => 1, 'kind' => 1, 'ts' => 1]);
CLI::write('Indexes created.', 'green');
CLI::write('Done. Generated covers in public/img/covers.', 'green');
}
private function seedVouchers($db): void
{
$vouchers = [
// store_code, title, code, discount, min_spend, valid_until(days), description
['eneba', '10% off sitewide', '10GGFEB', 10, 0, 30, '10% off your entire order at Eneba.'],
['eneba', '12% off select keys', '12GGDEAL', 12, 20, 21, 'Save 12% on orders over $20.'],
['kinguin', '14% off Kinguin', '14GGDEAL', 14, 0, 14, '14% discount on all Kinguin orders.'],
['kinguin', '5% off weekend sale', 'KQ5WEEK', 5, 10, 10, '5% off any purchase over $10.'],
['g2a', 'GG.deals exclusive', 'GG20FEB', 20, 0, 7, 'Exclusive 20% off for GG.deals visitors.'],
['g2a', '8% off G2A', 'GG8FEB', 8, 0, 14, '8% off sitewide at G2A.'],
['gamivo', 'GAMIVO promo', 'GAMIVO', 5, 0, 21, '5% off at GAMIVO marketplace.'],
['fanatical', 'Bundle discount', 'FAN10', 10, 25, 30, '10% off bundles over $25.'],
['gmg', 'Green Man Gaming', 'RATDEAL', 12, 0, 10, '12% off select Green Man Gaming titles.'],
['gamesplanet', 'Gamesplanet code', 'GSPLANET', 7, 0, 21, '7% off all Gamesplanet purchases.'],
['gamersgate', 'GamersGate welcome', 'GATE5', 5, 0, 30, '5% off your first GamersGate order.'],
['2game', '2Game special', '2GAME10', 10, 0, 14, '10% off at 2Game.'],
['wingamestore','WinGameStore', 'WGS15', 15, 30, 21, '15% off orders over $30.'],
['driffle', 'Driffle coupon', 'DRIFF10', 10, 0, 10, '10% off Driffle digital goods.'],
['cdkeys', 'CDKeys weekly', 'CDKEYS5', 5, 0, 7, '5% off CDKeys.com.'],
['instantgaming','Instant Gaming', 'IG4', 4, 0, 30, '4% off Instant Gaming orders.'],
];
$docs = [];
foreach ($vouchers as [$store, $title, $code, $discount, $minSpend, $days, $desc]) {
$docs[] = [
'store' => $store,
'title' => $title,
'code' => $code,
'discount' => $discount,
'min_spend' => $minSpend,
'active' => true,
'expires' => new UTCDateTime((time() + $days * 86400) * 1000),
'created' => new UTCDateTime((time() - mt_rand(1, 20) * 86400) * 1000),
'description'=> $desc,
];
}
$db->selectCollection('vouchers')->insertMany($docs);
CLI::write('Inserted ' . count($docs) . ' vouchers.', 'green');
}
private function seedNews($db): void
{
$templates = [
'deals' => ['title' => 'Weekend Sale: up to %d%% off top games across all stores', 'body' => 'This weekend, %d games have dropped to their historical lows. From AAA blockbusters to indie gems, now is the perfect time to grab that game you have been eyeing. Check the deals page for the full list.'],
'bundles' => ['title' => 'New bundle: get %d games for under $%d', 'body' => 'A fresh bundle just landed on the storefront. Pick up %d hand-picked titles for a fraction of their combined value. As always, we break down whether the bundle is actually worth it.'],
'freebies' => ['title' => '%s is FREE to keep on %s', 'body' => 'You can claim %s for free right now — and it is yours to keep forever once claimed. Head over to %s, sign in and hit the install button before the offer expires.'],
'giveaways' => ['title' => 'Giveaway: win a copy of %s', 'body' => 'We are giving away a copy of %s to one lucky winner. Enter via the community page — a random winner will be picked at the end of the week. Good luck!'],
'subscriptions' => ['title' => 'New titles added to %s this month', 'body' => 'A fresh wave of games just hit the subscription service. Here is everything new and our quick verdicts on what is worth your time this month.'],
];
$cats = array_keys($templates);
$docs = [];
for ($i = 0; $i < 24; $i++) {
$cat = $cats[$i % count($cats)];
$t = $templates[$cat];
$game = $this->games[array_rand($this->games)];
$store = ['Steam', 'GOG', 'Epic Games Store', 'Amazon', 'Humble Store'][array_rand(['Steam', 'GOG', 'Epic Games Store', 'Amazon', 'Humble Store'])];
$title = sprintf($t['title'], mt_rand(30, 90), mt_rand(5, 20), $game[0], $store, $game[0]);
$body = sprintf($t['body'], mt_rand(30, 90), mt_rand(5, 20), $game[0], $store);
$docs[] = [
'title' => $title,
'slug' => url_title($title, '-', true) . '-' . ($i + 1),
'category' => $cat,
'body' => $body,
'game_slug'=> $game[1] ?? null,
'date' => new UTCDateTime((time() - $i * mt_rand(1, 3) * 86400) * 1000),
];
}
$db->selectCollection('news')->insertMany($docs);
CLI::write('Inserted ' . count($docs) . ' news posts.', 'green');
}
private function seedPrepaids($db): void
{
$groups = [
['Steam Gift Cards', 'steam', [5, 10, 20, 50, 100]],
['PlayStation Gift Cards','playstation', [10, 25, 50, 100]],
['Xbox Gift Cards', 'xbox', [10, 25, 50, 100]],
['Nintendo eShop Cards', 'nintendo', [10, 25, 50, 75]],
['Roblox Gift Cards', 'roblox', [10, 25, 50]],
['Amazon Gift Cards', 'amazon', [10, 25, 50, 100]],
['Razer Gold', 'razer', [10, 25, 50]],
['Google Play Cards', 'googleplay',[10, 25, 50]],
];
$docs = [];
foreach ($groups as [$name, $code, $denoms]) {
$offers = [];
foreach ($denoms as $d) {
$offers[] = [
'denomination' => $d,
'price' => round($d * (0.92 + mt_rand(0, 10) / 100), 2),
'store' => $this->pick(['eneba', 'kinguin', 'g2a', 'cdkeys', 'instantgaming']),
];
}
$docs[] = [
'name' => $name,
'code' => $code,
'color' => $this->palettes[array_rand($this->palettes)],
'offers' => $offers,
];
}
$db->selectCollection('prepaids')->insertMany($docs);
CLI::write('Inserted ' . count($docs) . ' prepaid groups.', 'green');
}
private function seedSubscriptions($db): void
{
$subs = [
['Xbox Game Pass Ultimate', 'gamepass', 14.99, 'PC + console + cloud gaming, 500+ games'],
['PC Game Pass', 'gamepass', 9.99, 'Hundreds of PC games included'],
['PlayStation Plus Essential', 'psplus', 9.99, 'Online multiplayer + monthly games'],
['PlayStation Plus Extra', 'psplus', 14.99,'All Essential benefits + catalog'],
['PlayStation Plus Premium', 'psplus', 17.99,'All Extra benefits + classic catalog'],
['EA Play', 'eaplay', 5.99, 'EA games library + trials'],
['Ubisoft+ Classics', 'ubisoft', 7.99, 'Classic Ubisoft games catalog'],
['Nintendo Switch Online', 'nso', 3.99, 'Online play + retro library'],
];
$docs = [];
foreach ($subs as [$name, $brand, $price, $desc]) {
$docs[] = [
'name' => $name,
'brand' => $brand,
'price' => $price,
'period' => $this->pick(['1 Month', '3 Months', '12 Months']),
'desc' => $desc,
'store' => $this->pick(['eneba', 'kinguin', 'g2a', 'instantgaming', 'cdkeys']),
];
}
$db->selectCollection('subscriptions')->insertMany($docs);
CLI::write('Inserted ' . count($docs) . ' subscriptions.', 'green');
}
private function seedBundles($db): void
{
$validSlugs = [];
foreach ($this->games as $g) {
$validSlugs[url_title($g[0], '-', true)] = $g[2]; // slug => base price
}
$docs = [];
for ($i = 0; $i < 10; $i++) {
$slugs = array_keys($validSlugs);
$gameSlugs = $this->pickN($slugs, mt_rand(3, 8));
$baseTotal = 0;
foreach ($gameSlugs as $slug) {
$baseTotal += $validSlugs[$slug];
}
$bundlePrice = round($baseTotal * (0.08 + mt_rand(0, 15) / 100), 2);
$name = $this->pick(['Ultimate', 'Mega', 'Fan', 'Classic', 'Indie', 'Strategy', 'RPG', 'Action']) . ' Bundle #' . ($i + 1);
$docs[] = [
'slug' => url_title($name, '-', true) . '-' . ($i + 1),
'title' => $name,
'game_count' => count($gameSlugs),
'games' => $gameSlugs,
'price' => $bundlePrice,
'regular' => round($baseTotal, 2),
'store' => $this->pick(['humble', 'fanatical', 'gmg', 'indiegala', '2game']),
'active' => true,
'expires' => new UTCDateTime((time() + mt_rand(3, 21) * 86400) * 1000),
'desc' => 'Get ' . count($gameSlugs) . ' hand-picked games for a fraction of their combined value. Pay what you want from $' . number_format($bundlePrice, 2) . '.',
];
}
$db->selectCollection('bundles')->insertMany($docs);
CLI::write('Inserted ' . count($docs) . ' bundles.', 'green');
}
private function seedFranchises($db): void
{
$franchises = [
['resident-evil', 'Resident Evil', ['resident-evil-4']],
['dark-souls', 'Dark Souls', ['dark-souls-iii', 'elden-ring', 'sekiro-shadows-die-twice']],
['witcher', 'The Witcher', ['the-witcher-3-wild-hunt']],
['cyberpunk', 'Cyberpunk', ['cyberpunk-2077']],
['spider-man', 'Marvel\'s Spider-Man', ['marvels-spider-man-remastered', 'marvels-spider-man-2']],
['doom', 'DOOM', ['doom-eternal']],
['red-dead', 'Red Dead', ['red-dead-redemption-2', 'grand-theft-auto-v']],
['age-of-empires', 'Age of Empires', ['age-of-empires-iv']],
['total-war', 'Total War', ['total-war-warhammer-iii']],
['metro', 'Metro', ['metro-exodus']],
['diablo', 'Diablo', ['diablo-iv']],
['starfield', 'Starfield', ['starfield']],
['fallout', 'Fallout', ['starfield', 'the-last-of-us-part-i']],
['fps', 'First-Person Shooters', ['counter-strike-2', 'doom-eternal', 'deep-rock-galactic', 'helldivers-2', 'metro-exodus', 'warhammer-40000-darktide']],
['rpg', 'RPG Classics', ['elden-ring', 'baldurs-gate-3', 'the-witcher-3-wild-hunt', 'cyberpunk-2077', 'disco-elysium', 'divinity-original-sin-2', 'persona-5-royal']],
['strategy', 'Strategy', ['age-of-empires-iv', 'total-war-warhammer-iii', 'cities-skylines-ii', 'factorio', 'rimworld', 'slay-the-spire', 'civilization']],
['co-op', 'Co-op & Multiplayer', ['it-takes-two', 'deep-rock-galactic', 'helldivers-2', 'lethal-company', 'valheim', 'terraria', 'counter-strike-2']],
['indie', 'Indie Gems', ['hollow-knight', 'dead-cells', 'cuphead', 'stardew-valley', 'terraria', 'slay-the-spire', 'outer-wilds', 'lethal-company', 'hades-ii']],
['souls', 'Souls-like', ['elden-ring', 'sekiro-shadows-die-twice', 'dark-souls-iii']],
];
$docs = [];
$validSlugs = [];
foreach ($this->games as $g) {
$validSlugs[url_title($g[0], '-', true)] = true;
}
foreach ($franchises as [$slug, $name, $gameSlugs]) {
$matched = array_values(array_filter($gameSlugs, fn ($s) => isset($validSlugs[$s])));
if ($matched !== []) {
$docs[] = [
'slug' => $slug,
'name' => $name,
'games' => $matched,
];
}
}
$db->selectCollection('franchises')->insertMany($docs);
CLI::write('Inserted ' . count($docs) . ' franchises.', 'green');
}
// ---------------------------------------------------------------------- helpers
private function dealDoc(string $slug, string $store, string $kind, float $price, float $base, int|float $cut, float $hlPrice, array $platforms, string $title): array
{
$drm = match ($platforms[0] ?? 'steam') {
'gog' => 'DRM-free',
'epic' => 'Epic key',
default => 'Steam key',
};
return [
'game_slug' => $slug,
'title' => $title,
'store_code' => $store,
'kind' => $kind,
'price' => round($price, 2),
'regular' => $base,
'cut' => (int) max(0, min(95, $cut)),
'historical_low' => round($price, 2) <= round($hlPrice + 0.01, 2),
'drm' => $drm,
'regional' => $kind === 'keyshop' && mt_rand(1, 10) <= 3,
'expiry' => mt_rand(1, 10) <= 4 ? new UTCDateTime((time() + mt_rand(1, 7) * 86400) * 1000) : null,
'url' => 'https://www.google.com/search?q=' . rawurlencode("buy {$title} {$store}"),
];
}
private function pick(array $items)
{
return $items[array_rand($items)];
}
private function makeDescription(string $title, array $genres): string
{
$openers = [
"Step into the world of {$title}, a game that blends",
"{$title} invites you to experience",
"In {$title}, every choice matters as you explore",
"Prepare yourself for {$title}, an unforgettable journey through",
"{$title} delivers a rich experience combining",
];
$mid = implode(', ', array_slice($genres, 0, 3));
$closers = [
"With stunning visuals, a compelling soundtrack and deep gameplay mechanics, {$title} is a must-play for fans of {$mid}.",
"Whether you are a veteran or newcomer, {$title} offers hours of engaging content with its polished {$mid} systems.",
"Critically praised for its {$mid} design, {$title} pushes the genre forward with bold ideas and tight execution.",
"From its atmospheric world to its refined combat, {$title} is the definitive {$mid} experience of its generation.",
];
return $this->pick($openers) . ' ' . $this->pick($closers);
}
private function pickN(array $items, int $n): array
{
shuffle($items);
return array_slice($items, 0, min($n, count($items)));
}
private function minDeal(array $deals): array
{
usort($deals, static fn ($a, $b) => $a['price'] <=> $b['price']);
return $deals[0];
}
private function prettyPrice(float $p): float
{
if ($p >= 20 && mt_rand(0, 1) === 1) {
return (float) (floor($p) + 0.99);
}
return round($p, 2);
}
private function writeCover(string $slug, string $title, array $palette): void
{
$words = preg_split('/\s+/', $title) ?: [];
$initials = '';
foreach ($words as $w) {
if ($w === '' || ! ctype_alpha($w[0])) {
continue;
}
$initials .= strtoupper($w[0]);
if (strlen($initials) >= 2) {
break;
}
}
if ($initials === '') {
$initials = strtoupper(substr($slug, 0, 2));
}
[$c1, $c2] = $palette;
$pattern = crc32($slug) % 5;
$shapes = match ($pattern) {
0 => ''
. ''
. '',
1 => ''
. ''
. ''
. ''
. '',
2 => ''
. str_repeat(''
. ''
. ''
. ''
. '', 1)
. '',
3 => ''
. ''
. ''
. ''
. ''
. ''
. '',
default => ''
. '',
};
$svg = <<
SVG;
file_put_contents(self::COVERS_DIR . '/' . $slug . '.svg', $svg);
}
}