Melhor maneira de codificar o sistema de Conquistas

85

Estou pensando na melhor maneira de projetar um sistema de conquistas para usar em meu site. A estrutura do banco de dados pode ser encontrada em Best way to tell 3 ou mais consecutivos records ausentes e este thread é realmente uma extensão para obter as idéias dos desenvolvedores.

O problema que tenho com muita conversa sobre sistemas de emblemas / conquistas neste site é apenas isso - é tudo conversa e nenhum código. Onde estão os exemplos reais de implementação de código?

Proponho aqui um design com o qual espero que as pessoas possam contribuir e, com sorte, criem um bom design para a codificação de sistemas de realização extensíveis. Não estou dizendo que seja o melhor, longe disso, mas é um possível ponto de partida.

Fique à vontade para contribuir com suas idéias.


minha ideia de design de sistema

Parece que o consenso geral é criar um "sistema baseado em eventos" - sempre que um evento conhecido ocorre como uma postagem é criada, excluída, etc., ele chama a classe de evento assim ..

$event->trigger('POST_CREATED', array('id' => 8));

A classe de evento então descobre quais emblemas estão "ouvindo" este evento, em seguida, requiresnaquele arquivo, e cria uma instância dessa classe, assim:

require '/badges/' . $file;
$badge = new $class;

Em seguida, chama o evento padrão passando os dados recebidos quando triggerfoi chamado;

$badge->default_event($data);

os emblemas

É aqui que a verdadeira magia acontece. cada emblema tem sua própria consulta / lógica para determinar se um emblema deve ser concedido. Cada crachá é definido, por exemplo, neste formato:

class Badge_Name extends Badge
{
 const _BADGE_500 = 'POST_500';
 const _BADGE_300 = 'POST_300';
 const _BADGE_100 = 'POST_100';

 function get_user_post_count()
 {
  $escaped_user_id = mysql_real_escape_string($this->user_id);

  $r = mysql_query("SELECT COUNT(*) FROM posts
                    WHERE userid='$escaped_user_id'");
  if ($row = mysql_fetch_row($r))
  {
   return $row[0];
  }
  return 0;
 }

 function default_event($data)
 {
  $post_count = $this->get_user_post_count();
  $this->try_award($post_count);
 }

 function try_award($post_count)
 {
  if ($post_count > 500)
  {
   $this->award(self::_BADGE_500);
  }
  else if ($post_count > 300)
  {
   $this->award(self::_BADGE_300);
  }
  else if ($post_count > 100)
  {
   $this->award(self::_BADGE_100);
  }

 }
}

awardA função vem de uma classe estendida Badgeque basicamente verifica se o usuário já recebeu esse emblema, caso contrário, atualizará a tabela de banco de dados do emblema. A classe de crachás também se encarrega de recuperar todos os crachás de um usuário e devolvê-los em uma matriz, etc (para que os crachás possam ser exibidos, por exemplo, no perfil do usuário)

e quando o sistema é implementado pela primeira vez em um site já ativo?

Há também uma consulta de trabalho "cron" que pode ser adicionada a cada emblema. A razão para isso é porque quando o sistema de crachás é implementado e inicializado pela primeira vez, os crachás que já deveriam ter sido ganhos ainda não foram concedidos porque este é um sistema baseado em eventos. Portanto, um trabalho CRON é executado sob demanda para cada emblema para premiar tudo o que precisa ser. Por exemplo, o trabalho CRON para o acima seria assim:

class Badge_Name_Cron extends Badge_Name
{

 function cron_job()
 {
  $r = mysql_query('SELECT COUNT(*) as post_count, user_id FROM posts');

  while ($obj = mysql_fetch_object($r))
  {
   $this->user_id = $obj->user_id; //make sure we're operating on the right user

   $this->try_award($obj->post_count);
  }
 }

}

Como a classe cron acima estende a classe de emblema principal, ela pode reutilizar a função lógica try_award

A razão pela qual eu criei uma consulta especializada para isso é que poderíamos "simular" eventos anteriores, ou seja, percorrer cada postagem do usuário e acionar a classe de evento como $event->trigger()se fosse muito lento, especialmente para muitos emblemas. Em vez disso, criamos uma consulta otimizada.

qual usuário recebe o prêmio? tudo sobre como premiar outros usuários com base no evento

A função da Badgeclasse awardatua user_id- eles sempre receberão o prêmio. Por padrão, o emblema é concedido à pessoa que CAUSOU o evento acontecer, ou seja, o ID do usuário da sessão (isso é verdadeiro para a default_eventfunção, embora o trabalho CRON obviamente passe por todos os usuários e conceda usuários separados)

Então, vamos dar um exemplo, em um site de desafio de codificação, os usuários enviam sua entrada de codificação. O administrador então avalia as inscrições e, quando concluídas, publica os resultados na página de desafio para que todos possam ver. Quando isso acontece, um evento POSTED_RESULTS é chamado.

Se você quiser premiar os usuários por todas as entradas postadas, digamos, se eles foram classificados entre os 5 primeiros, você deve usar o cron job (embora tenha em mente que isso será atualizado para todos os usuários, não apenas para aquele desafio os resultados foram postados para)

Se você deseja direcionar uma área mais específica para atualizar com o cron job, vamos ver se há uma maneira de adicionar parâmetros de filtragem ao objeto do cron job e obter a função cron_job para usá-los. Por exemplo:

class Badge_Top5 extends Badge
{
   const _BADGE_NAME = 'top5';

   function try_award($position)
   {
     if ($position <= 5)
     {
       $this->award(self::_BADGE_NAME);
     }
   }
}

class Badge_Top5_Cron extends Badge_Top5
{
   function cron_job($challenge_id = 0)
   {
     $where = '';
     if ($challenge_id)
     {
       $escaped_challenge_id = mysql_real_escape_string($challenge_id);
       $where = "WHERE challenge_id = '$escaped_challenge_id'";
     }

     $r = mysql_query("SELECT position, user_id
                       FROM challenge_entries
                       $where");

    while ($obj = mysql_fetch_object($r))
   {
      $this->user_id = $obj->user_id; //award the correct user!
      $this->try_award($obj->position);
   }
}

A função cron ainda funcionará mesmo se o parâmetro não for fornecido.

Gary Green
fonte
2
Está relacionado, mas não é duplicado. Por favor, leia o segundo parágrafo. "O problema que tenho com muita conversa sobre sistemas de emblemas / conquistas neste site é apenas isso - é tudo conversa e nenhum código. Onde estão os exemplos reais de implementação de código?"
Gary Green
1
bem, escrever código funcional só é viável até certo ponto. Eu diria que é bastante normal que as pessoas forneçam apenas a teoria, uma vez que qualquer implementação seria muito complexa.
Gordon

Respostas:

9

Implementei um sistema de recompensa uma vez no que você chamaria de banco de dados orientado a documentos (isso era uma lama para os jogadores). Alguns destaques da minha implementação, traduzidos para PHP e MySQL:

  • Cada detalhe do crachá é armazenado nos dados do usuário. Se você usar o MySQL, eu teria certeza de que esses dados estão em um registro por usuário no banco de dados para desempenho.

  • Cada vez que a pessoa em questão faz algo, o código aciona o código do crachá com um determinado sinalizador, por exemplo sinalizador ('POST_MESSAGE').

  • Um evento também pode acionar um contador, por exemplo, uma contagem do número de postagens. aumentar_conta ('POST_MESSAGE'). Aqui você pode ter uma verificação (seja por um gancho, ou apenas tendo um teste neste método) se a contagem de POST_MESSAGE for> 300, então você deve recompensar um emblema, por exemplo: flag ("300_POST").

  • No método flag, colocaria o código para recompensar os emblemas. Por exemplo, se a bandeira 300_POST é enviada, então o emblema recompensa_badge ("300_POST") deve ser chamado.

  • No método sinalizador, você também deve ter os sinalizadores anteriores do usuário presentes. então você poderia dizer que quando o usuário tem FIRST_COMMENT, FIRST_POST, FIRST_READ você concede o emblema ("NOVO USUÁRIO"), e quando você obtém 100_COMMENT, 100_POST, 300_READ você pode conceder o emblema ("EXPERIENCED_USER")

  • Todos esses sinalizadores e emblemas precisam ser armazenados de alguma forma. Use alguma maneira em que você pense nas bandeiras como bits. Se você quiser que isso seja armazenado de forma realmente eficiente, pense neles como bits e use o código abaixo: (Ou você poderia apenas usar uma string simples "000000001111000" se não quiser essa complexidade.

$achievments = 0;
$bits = sprintf("%032b", $achievements);

/* Set bit 10 */
$bits[10] = 1;

$achievements = bindec($bits);

print "Bits: $bits\n";
print "Achievements: $achievements\n";

/* Reload */

$bits = sprintf("%032b", $achievments);

/* Set bit 5 */
$bits[5] = 1;

$achievements = bindec($bits);

print "Bits: $bits\n";
print "Achievements: $achievements\n";
  • Uma boa maneira de armazenar um documento para o usuário é usar json e armazenar os dados do usuário em uma única coluna de texto. Use json_encode e json_decode para armazenar / recuperar os dados.

  • Para rastrear a atividade nos dados de alguns usuários manipulados por algum outro usuário, adicione uma estrutura de dados ao item e use contadores lá também. Por exemplo, contagem de leitura. Use a mesma técnica descrita acima para conceder emblemas, mas a atualização deve ir para a postagem dos próprios usuários. (Por exemplo, o artigo dizia crachá 1000 vezes).

Knubo
fonte
1
A tendência clássica em sistemas de crachás é adicionar um novo campo para a nova estatística à sua tabela. Para mim, isso parece uma saída um pouco fácil e uma má ideia porque o seu armazenamento de dados espelhados que podem ser calculados a partir de dados já na tabela (talvez um simples COUNT () que é MUITO rápido em tabelas MyISAM, será 100% preciso). Se o seu objetivo for desempenho, você precisará fazer uma atualização E selecionar para obter o valor atual, por exemplo, post_count para verificar se um emblema deve ser concedido. Você só pode precisar de uma consulta, COUNT (*). Eu concordo que, para dados mais complexos, haveria um bom motivo para adicionar um campo
Gary Green
5
@Gary Green Não é apenas uma saída fácil, é também escalável e compatível com bancos de dados de documentos. Quanto à correção, você está certo, embora para um sistema de crachás eu prefira que seja rápido e provavelmente correto do que 100% correto e lento. Provavelmente, uma única contagem é rápida, mas quando seu sistema é dimensionado e você tem muitos usuários, essa estratégia não é válida.
Knubo
1
Gosto da ideia de ter apenas uma tabela de definição de crachás e uma tabela de links para vincular os usuários aos crachás e seu progresso atual. Fazendo isso, o noSQL bloqueia você em qualquer esquema no momento e não pode ser mantido quando erros de digitação são encontrados repentinamente ou 1000 novos emblemas são adicionados. Você sempre pode ter um cache de processo em lote em mais armazenamento de documentos para recuperação rápida, mas eu deixaria tudo vinculado.
FlavorScape
2

UserInfuser é uma plataforma de gamificação de código aberto que implementa um serviço de crachás / pontos. Você pode verificar sua API aqui: http://code.google.com/p/userinfuser/wiki/API_Documentation

Eu implementei e tentei manter o número mínimo de funções. Aqui está a API para um cliente php:

class UserInfuser($account, $api_key)
{
    public function get_user_data($user_id);
    public function update_user($user_id);
    public function award_badge($badge_id, $user_id);
    public function remove_badge($badge_id, $user_id);
    public function award_points($user_id, $points_awarded);
    public function award_badge_points($badge_id, $user_id, $points_awarded, $points_required);
    public function get_widget($user_id, $widget_type);
}

O resultado final é mostrar os dados de forma significativa por meio do uso de widgets. Esses widgets incluem: caixa de troféu, tabela de classificação, marcos, notificações ao vivo, classificação e pontos.

A implementação da API pode ser encontrada aqui: http://code.google.com/p/userinfuser/source/browse/trunk/serverside/api/api.py

Navraj Chohan
fonte
1
isso é baseado em PHP? A pergunta é baseada em PHP
Lenin Raj Rajasekaran
1
Possui vínculos PHP, mas o código do lado do servidor é escrito em Python.
Navraj Chohan
0

Conquistas podem ser onerosas e ainda mais se você tiver que adicioná-las mais tarde, a menos que você tenha uma Eventclasse bem formada .

Isso leva à minha técnica de implementação de realizações.

Gosto de dividi-los primeiro em 'categorias' e, dentro deles, há níveis de realização. ou seja, uma killscategoria em um jogo pode ter um prêmio de 1 para a primeira morte, 10 dez mortes, 1000 mil mortes etc.

Em seguida, para a espinha dorsal de qualquer bom aplicativo, a classe que trata de seus eventos. Novamente imaginando um jogo com mortes; quando um jogador mata algo, coisas acontecem. A morte é anotada, etc, e é melhor tratada em um local centralizado, como e uma Eventsclasse que pode enviar informações para outros locais envolvidos.

Isso se encaixa perfeitamente lá, que no método adequado, instancie sua Achievementsclasse e verifique se o jogador deve ter uma.

Como construir a Achievementsclasse é trivial, apenas algo que verifica o banco de dados para ver se o jogador tem tantas mortes quantas são necessárias para a próxima conquista.

Eu gosto de armazenar as realizações do usuário em um BitField usando Redis, mas a mesma técnica pode ser usada no MySQL. Ou seja, você pode armazenar as conquistas do jogador como um inte então andaquele int com a parte que você definiu como essa conquista para ver se ele já a ganhou. Dessa forma, ele usa apenas uma única intcoluna no banco de dados.

A desvantagem disso é que você precisa organizá-los bem e provavelmente precisará fazer alguns comentários em seu código para se lembrar a que 2 ^ 14 corresponde mais tarde. Se suas realizações são enumeradas em sua própria tabela, então você pode simplesmente fazer 2 ^ pk, onde pkestá a chave primária da tabela de realizações. Isso torna o cheque algo como

if(((2**$pk) & ($usersAchInt)) > 0){
  // fire off the giveAchievement() event 
} 

Desta forma, você pode adicionar conquistas mais tarde e isso vai se encaixar bem, apenas NUNCA mude a chave primária das conquistas já concedidas.

NappingRabbit
fonte