Pular para o conteúdo principal

@cactus-agents/games

SDK framework-agnostic para jogos de cassino, categorias, providers, busca, start-game, votos e estatisticas.

Instalacao

pnpm add @cactus-agents/games

Uso recomendado

Use createGamesFromClient quando voce ja estiver usando @cactus-agents/api-client:

import { createApiClient } from "@cactus-agents/api-client";
import { createGamesFromClient } from "@cactus-agents/games";

const client = createApiClient({ baseUrl, tenant, language });
const games = createGamesFromClient(client);

const rows = await games.getPageRows("home");
const base = await games.getBase();
const list = await games.getList({ categories: ["slots"], page: 1, perPage: 24 });

API publica

Exports principais:

  • createGamesService(fetcher), createGamesFromClient(client)
  • createLegacyGamesService(fetcher, homeConfig), createLegacyGamesFromClient(client, homeConfig)
  • createVotesOrchestrator(...)
  • Transforms: transformGame, transformProvider, transformCategory, transformHomeRow, transformBaseData, transformGameListResponse, transformStartGameResponse, transformTopWin, transformLastWin, transformGameTopWinsDl, transformGameHighPayers, transformGameHighPayerEntry, transformGameStatistics, transformGameStatisticsDl, transformGameVote, transformGameVoteCount
  • Helpers: filterGames, foldForSearch, orderByStats, getProviderIframeConfig, applyIsSoftSwiss, parseCasinoProviderMessage
  • Constante: HIGH_PAYERS_WINDOW_TO_METRIC

Tipos principais:

  • Game, GameProvider, GameType, GameRange, Provider, CustomCategory
  • PageSlug, HomeRow, HomeRowType, BaseData
  • GameListParams, GameListResponse, GameListFacets
  • StartGamePlatform, StartGameParams, StartGameResponse
  • TopWin, LastWin, GameTopWin, GameHighPayers, GameHighPayersWindow
  • StatsPeriod, GameStatistics
  • VoteChoice, GameUserVote, GameVoteCount, GameVoteState
  • GamesService, GamesFetcher

Referencia de exports: front-cactus-core/packages/games/src/index.ts.

:::caution GameVote não existe O tipo do voto do usuário é GameUserVote ({ casinoGameId: string; choice: VoteChoice }). Existem também GameVoteRaw (shape da API) e GameVoteState ({ userVote, counts }). Nenhum export chamado GameVote. :::

GamesFetcher

O GamesFetcher e a interface de abstraction de HTTP usada pelo createGamesService. Deve implementar os metodos get, post e delete:

  • get — usado pela maioria dos endpoints de leitura
  • post — usado por vote (criar voto)
  • delete — usado por removeVote (remover voto)

O createGamesFromClient adapta o ApiClient para essa interface automaticamente.

Endpoints do GamesService

MetodoVerboEndpointObservacao
getPageRows(page)GET/casino-games/page/{page}Rows curadas de home / cassino / cassino_live
getHome()GET/casino-games/home@deprecated — use getPageRows("home")
getBase()GET/casino-games/list/baseCategorias + providers
getList(params?)GET/casino-games/list/Paginado: categories[], providers[], term, page, per_page
getDetail(slug)GET/casino-games?slug={slug}Detalhe de um jogo
startGame(params)GET/start-game-v2slug, platform, use_demo, currency? — autenticado
getTopWins()GET/bff/games/top-wins-dlMaiores ganhos recentes
getGameTopWins(slug)GET/bff/games/game-top-wins-dl?slug={slug}Maiores ganhos de um jogo
getLastWins()GET/bff/games/last-winsUltimos ganhos
getHighPayers()GET/bff/games/game-high-payers-dlJogos que mais pagam, por janela
getStats(slug)GET/bff/games/statistics?slug={slug}Stats por periodo
getStatsDl(slug)GET/bff/games/statistics-dl?slug={slug}Variante -dl das stats
getVote(casinoGameId)GET/casino-game-votes?casinoGameId={id}Voto do usuario — autenticado
getVoteCount(gameId)GET/casino-game-votes/count/{id}Contagem likes/dislikes
vote(casinoGameId, choice)POST/casino-game-votes/store/Criar voto — autenticado
removeVote(gameId)DELETE/casino-game-votes/destroy/{id}Remover voto — autenticado

:::caution vote() não aceita boolean A assinatura é vote(casinoGameId: string, choice: VoteChoice), com VoteChoice = "liked" | "disliked". O body enviado é { casinoGameId: Number(id), is_like: choice } — apesar do nome is_like, o valor no wire é a string do enum, não um booleano. Passar true/false não type-checka. :::

Páginas curadas — getPageRows(page)

type PageSlug = "home" | "cassino" | "cassino_live";

getPageRows é o contrato de páginas curadas do modo CASSINO_MODE=api_new: o backoffice monta as rows de cada página e o front só renderiza. getHome() continua exportado e bate em /casino-games/home verbatim (compatibilidade com bases antigas), mas está @deprecated — código novo usa getPageRows("home").

Facets em getList

GameListResponse pode trazer facets:

interface GameListFacets {
providers?: GameListFacetProviderRaw[];
categories?: GameListFacetCategoryRaw[];
}

facets é ausente quando a query não tem filtro — o campo é opcional e o consumidor precisa tratar undefined (não é []).

Helpers de imagem

Os helpers de imagem foram centralizados em @cactus-agents/utils, para que o mesmo contrato sirva para qualquer imagem do projeto:

  • Cloudflare imagedelivery.net
  • thumbs legadas com /mobile e /ipad
  • /api/storage/...
  • imagens externas ou paths locais
import { getImageUrl } from "@cactus-agents/utils";

const logoUrl = getImageUrl(brand.appearance.logo);

:::danger Arte de jogo tem perfil único — não monte ladder próprio Para game.image no front-web-base, use sempre getGameArtworkProps(image, sizes) de ~/utils/game-artwork (que é um wrapper fino sobre getResponsiveImageProps com a ladder de larguras canônica). Não chame getResponsiveImageProps direto com uma ladder sua e não passe aspectRatio/height: o Cloudflare codifica o transform no path, então uma ladder divergente faz a MESMA arte ser baixada duas vezes. O recorte é CSS (object-cover numa caixa de aspect fixo). :::

Estatisticas de jogo

transformGameStatistics converte os campos flat da API em periodos organizados:

import { transformGameStatistics } from "@cactus-agents/games";

const stats = transformGameStatistics(raw);
// stats.slug
// stats.last1Hour, stats.last24Hours, stats.last7Days, stats.last15Days, stats.last30Days
// stats.last1Hour.bets, stats.last1Hour.rtp, ...

Os cinco periodos sao last1Hour, last24Hours, last7Days, last15Days, last30Days. Nao existe last5Minutes.

interface StatsPeriod {
bets: number;
wins: number;
players: number;
rtp: number;
houseEdge: number | null;
averageBetPerPlayer: number | null;
averageWin: number | null;
}

:::note Os tres campos nullable houseEdge, averageBetPerPlayer e averageWin sao number | null — a API pode nao mandar. O front nao calcula derivado (nao faca 1 - rtp): card sem dado deve ser marcado como indisponivel, nao inventado. :::

Integracao server-side (recomendado para cache)

No front-web-base, o servico e encapsulado por GamesCacheService, que adiciona o pipeline do platform-cache (memory + CF Cache API + KV, com stale-while-revalidate). A fabrica e createGamesCacheService(opts) em ~/services/games.server:

import { createGamesCacheService } from "~/services/games.server";

export async function loader({ context }: Route.LoaderArgs) {
const cf = context?.cloudflare?.env;
const waitUntil = context?.cloudflare?.ctx?.waitUntil.bind(context.cloudflare.ctx);

const gamesCacheService = createGamesCacheService({ cloudflareEnv: cf, waitUntil });
const base = await gamesCacheService.getBase();
return { base };
}

A fabrica tambem resolve o CASSINO_MODE da brand e escolhe qual service usar (api_new / legacy / mixed) — o loader nao decide isso.

Para start-game (autenticado), a chamada vai direto na API sem cache:

import { createGamesFromClient } from "@cactus-agents/games";

const client = createClient(env, { request });
const games = createGamesFromClient(client);
const result = await games.startGame({ slug, platform: "WEB", useDemo: false });

Notas de transform

  • transformGame converte demo: 0|1 para boolean e normaliza provider, casino_game_type, range
  • transformProvider converte games (count) para gameCount
  • transformCategory mapeia campos 1:1 incluindo icon
  • transformHomeRow classifica type como widget, custom-category ou custom-section
  • transformGameListResponse converte paginacao snake_case para camelCase
  • transformStartGameResponse extrai gameUrl de multiplos formatos possiveis da API (SoftSwiss vs padrao)

Legacy Service

Funcoes para integrar com o endpoint legado de jogos:

  • createLegacyGamesService(fetcher, homeConfig) — cria service usando GamesFetcher diretamente
  • createLegacyGamesFromClient(client, homeConfig) — cria service a partir de ApiClient

O legacy service usa /casino-games/filter em vez de /casino-games/list. O parametro homeConfig e do tipo LegacyHomeRowConfig[], onde cada item define uma row da home com um dos tipos:

TipoDescricao
"games-api"Row populada via filtro na API de jogos
"providers-api"Row de providers via API
"games-fixed"Row com jogos fixos (slugs hardcoded)
"widget"Row de widget customizado

Provider Config

Configuracao de iframe para providers de jogos:

getProviderIframeConfig(providerSlug, gameUrl?)

Retorna ProviderIframeConfig para o provider especificado:

  • allow — permissions do iframe (ex.: autoplay, fullscreen)
  • allowFullscreen — boolean
  • launchMode — modo de lancamento: "iframe" | "softswiss-sdk" | "srcdoc"

applyIsSoftSwiss(config, isSoftSwiss)

Sobrescreve o launchMode da config quando o jogo e SoftSwiss.

parseCasinoProviderMessage(providerSlug, raw)

Faz parse de postMessage recebidas de iframes de jogos. Retorna acao interpretada por provider:

  • liveg24: mensagem "closeGame" → acao exit
  • amusnet: detecta acoes exit e reload
import { parseCasinoProviderMessage } from "@cactus-agents/games";

window.addEventListener("message", (event) => {
const action = parseCasinoProviderMessage("liveg24", event.data);
if (action === "exit") {
// fechar iframe do jogo
}
});

Votos — createVotesOrchestrator(service)

Encapsula a sequência de votação (paridade com o legado: DELETE → POST → refetch), para o consumidor não reimplementar a ordem:

import { createVotesOrchestrator } from "@cactus-agents/games";

const votes = createVotesOrchestrator(gamesService);

const state = await votes.getState(gameId, { authed: isLoggedIn });
const next = await votes.submit(gameId, "liked");
const cleared = await votes.remove(gameId);
  • getState(id, { authed })authed: false pula o request de voto do usuário (carga de página pública). Erros de getVoteCount propagam aqui, para o caller distinguir "o BFF devolveu zeros" de "a chamada falhou".
  • submit / remove — o refetch pós-mutação é catch-guarded: a mutação já deu certo, então falha no refetch não quebra o fluxo.

Retorna sempre GameVoteState ({ userVote, counts }).

Ordenação por stats — orderByStats(games, statsBySlug, orderBy)

Ordena Game[] por uma métrica de GameStatistics, indexada por slug. Jogos sem stats vão para o fim preservando a ordem original.

import { orderByStats } from "@cactus-agents/games";

const ordered = orderByStats(games, statsBySlug, category.orderBy);

HIGH_PAYERS_WINDOW_TO_METRIC mapeia a janela do endpoint de high-payers para o tipo de métrica:

{ "1hour": "last_minutes", "24hours": "paid_today",
"7days": "paid_week", "15days": "paid_week", "30days": "paid_month" }

Filtro in-memory

foldForSearch(s)

Normalização usada pela busca: remove diacríticos (NFD + strip) e passa para lowercase. Exportada para que buscas locais em subsets do catálogo (página de categoria, de provider, listagens restritas) usem exatamente a mesma normalização da busca global.

foldForSearch("Ração & Café"); // "racao & cafe"

filterGames(games, params)

Filtra um array de Game[] em memoria (client-side). Util para buscas e filtros sem nova chamada a API.

Params:

ParamTipoDescricao
termstring?Busca por nome do jogo
categoriesstring[]?Filtro por categorias
providersstring[]?Filtro por providers
tagsstring[]?Filtro por tags
pagenumber?Pagina atual
perPagenumber?Itens por pagina

Retorno: GameFilterResult

interface GameFilterResult {
data: Game[];
total: number;
currentPage: number;
lastPage: number;
}
import { filterGames } from "@cactus-agents/games";

const result = filterGames(allGames, {
term: "sweet",
categories: ["slots"],
page: 1,
perPage: 20,
});
// result.data — jogos filtrados da pagina
// result.total — total de resultados
// result.lastPage — ultima pagina disponivel