La mejor forma de codificar el sistema de logros

85

Estoy pensando en la mejor manera de diseñar un sistema de logros para usar en mi sitio. La estructura de la base de datos se puede encontrar en la mejor manera de saber que faltan 3 o más registros consecutivos y este hilo es realmente una extensión para obtener las ideas de los desarrolladores.

El problema que tengo cuando se habla mucho sobre insignias / sistemas de logros en este sitio web es solo eso: todo habla y no hay código. ¿Dónde están los ejemplos reales de implementación de código?

Propongo aquí un diseño en el que espero que la gente pueda contribuir y, con suerte, cree un buen diseño para codificar sistemas de logros extensibles. No digo que esto sea lo mejor, ni mucho menos, pero es un posible bloque de partida.

No dude en contribuir con sus ideas.


mi idea de diseño de sistema

Parece que el consenso general es crear un "sistema basado en eventos": cada vez que ocurre un evento conocido, como una publicación, se crea, elimina, etc., llama a la clase de evento así.

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

Luego, la clase de evento descubre qué insignias están "escuchando" para este evento, luego requiresese archivo y crea una instancia de esa clase, así:

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

Luego llama al evento predeterminado que pasa los datos recibidos cuando triggerfue llamado;

$badge->default_event($data);

las insignias

Aquí es donde ocurre la verdadera magia. cada insignia tiene su propia consulta / lógica para determinar si se debe otorgar una insignia. Cada insignia se presenta, por ejemplo, en este 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);
  }

 }
}

awardLa función proviene de una clase extendida Badgeque básicamente verifica si el usuario ya recibió esa insignia, si no, actualizará la tabla db de la insignia. La clase de credencial también se encarga de recuperar todas las credenciales de un usuario y devolverlas en una matriz, etc. (para que las credenciales se puedan mostrar, por ejemplo, en el perfil del usuario)

¿Qué pasa cuando el sistema se implementa por primera vez en un sitio que ya está activo?

También hay una consulta de trabajo "cron" que se puede agregar a cada insignia. La razón de esto es porque cuando el sistema de insignias se implementa e inicia por primera vez, las insignias que ya deberían haberse ganado aún no se han otorgado porque se trata de un sistema basado en eventos. Por lo tanto, se ejecuta un trabajo CRON a pedido para cada insignia para otorgar lo que sea necesario. Por ejemplo, el trabajo CRON para lo anterior se vería así:

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 la clase cron anterior extiende la clase de insignia principal, puede reutilizar la función lógica try_award

La razón por la que creo una consulta especializada para esto es aunque podríamos "simular" eventos anteriores, es decir, revisar cada publicación de usuario y activar la clase de evento como $event->trigger()si fuera muy lento, especialmente para muchas insignias. Entonces, en su lugar, creamos una consulta optimizada.

¿Qué usuario recibe el premio? todo sobre premiar a otros usuarios según el evento

La función de Badgeclase awardsigue actuando user_id: siempre se les otorgará el premio. Por defecto, la insignia se otorga a la persona que CAUSÓ que ocurriera el evento, es decir, la identificación del usuario de la sesión (esto es cierto para la default_eventfunción, aunque el trabajo CRON obviamente recorre todos los usuarios y otorga premios a usuarios separados)

Así que tomemos un ejemplo, en un sitio web de desafío de codificación, los usuarios envían su entrada de codificación. Luego, el administrador juzga las entradas y, cuando está completo, publica los resultados en la página del desafío para que todos los vean. Cuando esto sucede, se llama a un evento POSTED_RESULTS.

Si desea otorgar insignias a los usuarios por todas las entradas publicadas, digamos, si se clasificaron entre los 5 primeros, debe usar el trabajo cron (aunque tenga en cuenta que esto se actualizará para todos los usuarios, no solo para ese desafío el se publicaron resultados para)

Si desea apuntar a un área más específica para actualizar con el trabajo cron, veamos si hay una manera de agregar parámetros de filtrado en el objeto del trabajo cron y obtener la función cron_job para usarlos. Por ejemplo:

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);
   }
}

La función cron seguirá funcionando incluso si no se proporciona el parámetro.

Gary Green
fuente
Relacionado (tal vez duplicado): stackoverflow.com/questions/1744747/achievements-badges-system
Gordon
2
Está relacionado pero no duplicado. Por favor lea el segundo párrafo. "El problema que tengo cuando se habla mucho sobre insignias / sistemas de logros en este sitio web es solo eso: todo es charla y no hay código. ¿Dónde están los ejemplos reales de implementación de código?"
Gary Green
1
bueno, escribir código de trabajo solo es factible hasta cierto punto. Yo diría que es bastante normal que la gente te dé solo la teoría, una vez que cualquier implementación sería demasiado compleja.
Gordon

Respuestas:

9

Implementé un sistema de recompensas una vez en lo que llamarías una base de datos orientada a documentos (esto fue un lodo para los jugadores). Algunos aspectos destacados de mi implementación, traducidos a PHP y MySQL:

  • Cada detalle de la insignia se almacena en los datos de los usuarios. Si usa MySQL, me habría asegurado de que estos datos estén en un registro por usuario en la base de datos para el rendimiento.

  • Cada vez que la persona en cuestión hace algo, el código activa el código de la insignia con una bandera determinada, por ejemplo, la bandera ('POST_MESSAGE').

  • Un evento también podría activar un contador, por ejemplo, un recuento del número de publicaciones. Incrementar_cuenta ('POST_MESSAGE'). Aquí puede verificar (ya sea mediante un gancho o simplemente haciendo una prueba en este método) de que si el recuento de POST_MESSAGE es> 300, entonces debería tener una insignia de recompensa, por ejemplo: flag ("300_POST").

  • En el método de la bandera, pondría el código para recompensar las insignias. Por ejemplo, si se envía la Bandera 300_POST, entonces se debe llamar a la insignia Prize_badge ("300_POST").

  • En el método de la bandera, también debe tener presentes las banderas anteriores del usuario. por lo que podría decir que cuando el usuario tiene FIRST_COMMENT, FIRST_POST, FIRST_READ, usted otorga la insignia ("NUEVO USUARIO"), y cuando obtiene 100_COMMENT, 100_POST, 300_READ puede otorgar la insignia ("EXPERIENCED_USER")

  • Todas estas banderas e insignias deben almacenarse de alguna manera. Use alguna forma en la que piense en las banderas como bits. Si desea que esto se almacene de manera realmente eficiente, piense en ellos como bits y use el siguiente código: (O simplemente podría usar una cadena simple "000000001111000" si no desea esta complejidad.

$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";
  • Una buena forma de almacenar un documento para el usuario es usar json y almacenar los datos de los usuarios en una sola columna de texto. Utilice json_encode y json_decode para almacenar / recuperar los datos.

  • Para rastrear la actividad en algunos de los datos de los usuarios manipulados por otro usuario, agregue una estructura de datos en el artículo y use contadores allí también. Por ejemplo, lea el recuento. Utilice la misma técnica descrita anteriormente para otorgar insignias, pero la actualización, por supuesto, debe ir a la publicación de los usuarios propietarios. (Por ejemplo, el artículo lee 1000 veces la insignia).

Knubo
fuente
1
La tendencia clásica en los sistemas de insignias es agregar un nuevo campo para la nueva estadística a su tabla. Para mí, eso parece una salida un poco fácil y una mala idea porque su almacenamiento de datos reflejados que se pueden calcular a partir de los datos que ya están en la tabla (tal vez un simple COUNT () que es MUY rápido en las tablas MyISAM, será 100% preciso). Si el rendimiento era su objetivo, deberá realizar una actualización Y seleccionar obtener el valor actual, por ejemplo, post_count para verificar si se debe otorgar una insignia. Solo podría necesitar una consulta, COUNT (*). Sin embargo
Gary Green
5
@Gary Green No solo es una salida fácil, también es la forma escalable y compatible con bases de datos de documentos. En cuanto a la corrección, tiene razón, aunque para un sistema de insignias prefiero que sea rápido y probablemente correcto que 100% correcto y lento. Una sola cuenta es probablemente rápida, pero cuando su sistema escala y tiene muchos usuarios, no se da que la estrategia se mantenga.
Knubo
1
Me gusta la idea de tener una tabla de definición de insignias y una tabla de enlaces para vincular a los usuarios con las insignias y su progreso actual. Si lo hace, noSQL lo bloquea en cualquier esquema en ese momento y no se puede mantener cuando de repente se encuentran errores tipográficos en las insignias o se agregan 1000 nuevas insignias. Siempre podría tener un proceso por lotes en caché en más del almacén de documentos para una rápida recuperación, pero dejaría las cosas vinculadas.
FlavorScape
2

UserInfuser es una plataforma de gamificación de código abierto que implementa un servicio de insignias / puntos. Puede consultar su API aquí: http://code.google.com/p/userinfuser/wiki/API_Documentation

Lo implementé y traté de mantener al mínimo el número de funciones. Aquí está la API para un 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);
}

El resultado final es mostrar los datos de una manera significativa mediante el uso de widgets. Estos widgets incluyen: vitrina de trofeos, clasificación, hitos, notificaciones en vivo, clasificación y puntos.

La implementación de la API se puede encontrar aquí: http://code.google.com/p/userinfuser/source/browse/trunk/serverside/api/api.py

Navraj Chohan
fuente
1
¿Está basado en PHP? La pregunta está basada en PHP
Lenin Raj Rajasekaran
1
Tiene enlaces PHP, pero el código del lado del servidor está escrito en Python.
Navraj Chohan
0

Los logros pueden ser gravosos y aún más si tienes que agregarlos más tarde, a menos que tengas una Eventclase bien formada .

Esto da paso a mi técnica de implementación de logros.

Me gusta dividirlos primero en 'categorías' y dentro de ellos tengo niveles de logro. es decir, una killscategoría en un juego puede tener un premio de 1 por primera muerte, 10 diez muertes, 1000 mil muertes, etc.

Luego, a la columna vertebral de cualquier buena aplicación, la clase que maneja sus eventos. De nuevo imaginando un juego con asesinatos; cuando un jugador mata algo, suceden cosas. Se anota la muerte, etc., y eso se maneja mejor en una ubicación centralizada, como una Eventsclase que puede enviar información a otros lugares involucrados.

Cae perfectamente en su lugar allí, que en el método adecuado, instancia tu Achievementsclase y verifica si el jugador tiene una.

Como construir la Achievementsclase, es trivial, solo algo que verifica la base de datos para ver si el jugador tiene tantos asesinatos como se requieren para el próximo logro.

Me gusta almacenar los logros de los usuarios en un BitField usando Redis, pero la misma técnica se puede usar en MySQL. Es decir, puedes almacenar los logros del jugador como un inty luego andese int con el bit que has definido como ese logro para ver si ya lo han ganado. De esa manera, usa solo una intcolumna en la base de datos.

La desventaja de esto es que debes tenerlos bien organizados y probablemente necesitarás hacer algunos comentarios en tu código para recordar a qué corresponde 2 ^ 14 más adelante. Si sus logros están enumerados en su propia tabla, entonces puede hacer 2 ^ pk donde pkes la clave principal de la tabla de logros. Eso hace que el cheque sea algo como

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

De esta manera, puede agregar logros más tarde y se combinará bien, solo NUNCA cambie la clave principal de los logros ya otorgados.

SiestaConejo
fuente