Bots de conversa de Telegram amb Google Apps Script

Bots de conversa Exemples Dades pràctiques   Recursos CITCEA
Google Apps Script Projectes Interacció   Inici

Joc de tres en línia

El tres en línia és un joc molt senzill per a dos jugadors. Cada jugador, per torns, va situant les seves fitxes en un tauler de tres per tres caselles. L'objectiu és posar tres fitxes en línia (horitzontal, vertical o diagonal) i impedir que el contrincant ho aconsegueixi abans. En aquest cas, l'usuari jugarà contra el bot. Atès que en aquest joc, si els dos jugadors són experimentats, qui comença té moltes probabilitats de guanyar, s'ha dissenyat de manera que sempre comença l'usuari.

El tauler serà un teclat en línia amb nou tecles, situades en tres fileres de tres. A cada tecla hi mostrarem la fitxa que s'hi ha tirat o la deixarem buida si no se n'hi ha posat cap encara. L'estat del tauler es guarda a la memòria cau per tal de tenir-lo disponible cada cop que s'executa l'script. La clau és l'identificador de l'usuari. Atès que la memòria cau no treballa bé amb vectors, les dades es guarden en format de text; concretament en una seqüència de nou caràcters en la que un 0 representa la casella buida, un 1 una fitxa del bot i un 2 una fitxa de l'usuari. A l'hora de mostrar les fitxes al tauler, s'han agafat els símbols Unicode 🔴 i 🔶, però se'n podria haver escollit qualsevol altre parell.

Tauler

En començar la partida es crea un tauler buit, s'inicialitza el comptador i es faciliten les instruccions. A cada tirada de l'usuari es comprova si s'acaba la partida i, en cas contrari, es fa la tirada del bot. Si la partida encara no ha acabat, s'envia un nou teclat representant la nova configuració del tauler. En aquesta primera versió, el bot posa les seves fitxes en qualsevol casella buida escollida aleatòriament i, per tant, és molt fàcil que guanyi l'usuari. La funció guanyador és la que mira, a cada tirada, si ha guanyat un dels dos jugadors o si el tauler està tot ocupat (i, per tant, no hi ha guanyador).

El programa de l'script és el següent:

var token = "^^34328844:AAFIpk-e7j3UZtYQYQaTduf4hEhnDqIcNXI"; // API Token de Telegram 
var telegramUrl = "https://api.telegram.org/bot" + token;  // URL que comunica el bot de conversa amb Telegram 
// Símbols que fem servir com a fitxes
// 🔴 Fitxa del bot
// 🔶 Fitxa de l'usuari
var fitxes  = [" ","🔴", "🔶"];
function doPost(e){
  var data = JSON.parse(e.postData.contents);  // Llegeix les dades rebudes per JSON i les guarda
  var realitzat = false;
  if (data.callback_query){  // Si és la resposta a un teclat
    var tecla = data.callback_query.data;  // Resposta del teclat
    var id = data.callback_query.from.id;  // Identificador de la finestra on s'ha escrit el missatge
    if(tecla.indexOf("/t") == 0){
      realitzat = true;
      tirada(id, tecla);
    }
  }
  if (data.message){  // Si és una comanda normal
    var text = data.message.text;  // El text enviat
    var id = data.message.chat.id;  // Identificador de la finestra on s'ha escrit el missatge 
    if (text == '/help'){
      realitzat = true;
      var resposta = "Comandes disponibles:\n";
      resposta = resposta + "/help  -  Llista de comandes\n";
      resposta = resposta + "/tres  -  Comença una partida\n";
      resposta = resposta + "/final  -  Finalitza i esborra la partida actual\n";
      sendText(id,resposta);
    }
    if(text == "/tres"){
      realitzat = true;
      inici(id);
    }
    if(text == "/final"){
      realitzat = true;
      final(id);
    }
  }
  if (!realitzat){
    var resposta = "Comanda desconeguda";
    sendText(id,resposta);
  }
}
function inici(chatId){
  var combi = "000000000";  // Inicialment no hi ha cap fitxa
    // Cada tres caràcters és una filera
    // Els valors poden ser 0 (buida), 1 (bot) o 2 (usuari)
  var cache = CacheService.getScriptCache();
  cache.put(chatId, combi);  // Guardem la tirada a la memòria cau
  var resposta = "Comença la partida\n";
  resposta = resposta + "La meva fitxa és " + fitxes[1];
  resposta = resposta + " i la teva és " + fitxes[2] + "\n";
  resposta = resposta + "Pica el botó de la casella on vols posar la teva fitxa\n";
  resposta = resposta + "Sort!";
  sendText(chatId,resposta);
  tauler(chatId, combi);
}
function final(chatId){
  var cache = CacheService.getScriptCache();
  cache.remove(chatId);  // Esborrem les dades de la memòria cau
  var resposta = "S'ha esborrat la partida\n";
  sendText(chatId,resposta);
}
function tirada(chatId, txt){
  var cache = CacheService.getScriptCache();
  var combi = cache.get(chatId);  // Llegim les dades de la memòria cau
  if (!combi){
    var resposta = "No tens cap partida començada (o ha caducat)";
    sendText(chatId,resposta);
  } else {
    var pos = +txt.substring(2,3);  // El tercer caràcter és la posició
    var combi1 = combi;
    var carPos = combi.substring(pos,pos+1);  // Símbol que hi ha a la posició
    if(carPos == "0"){
      var esq = "";
      var dreta = "";
      if (pos > 0){
        esq = combi.substring(0,pos);
      }
      if (pos < 8){
        dreta = combi.substring(pos+1,9);
      }
      combi = esq + "2" + dreta;
      var guanya = guanyador(combi);
      if(guanya == "0"){
        combi = tirar(combi);
        guanya = guanyador(combi);
        if(guanya == "0"){
          var cache = CacheService.getScriptCache();
          cache.put(chatId, combi);  // Guardem la tirada a la memòria cau
          tauler(chatId, combi);
        }
      }
      if(guanya != "0"){
        tauler(chatId, combi);
        if(guanya == "1"){
          var resposta = "He guanyat!\nHo sento!";
        }
        if(guanya == "2"){
          var resposta = "Has guanyat!\nFelicitats!";
        }
        if(guanya == "3"){
          var resposta = "La partida ha acabat sense guanyador";
        }
        sendText(chatId,resposta);
        var cache = CacheService.getScriptCache();
        cache.remove(chatId);  // Esborrem les dades de la memòria cau
      }
    } else {
      var resposta = "Aquesta casella ja està ocupada";
      sendText(chatId,resposta);
    }
  }
}
function tirar(comb){
  // Fa una tirada aleatòria
  var lliures = new Array();  // Aquí guardarem les posicions lliures 
  for(var i=0; i<comb.length; i++){
    var car = comb.substring(i,i+1);  // Símbol que hi ha a la posició
    if(car == "0"){
      lliures.push(i);
    }
  }
  var ind = Math.floor(lliures.length * Math.random());
  var pos = lliures[ind];
  var esq = "";
  var dreta = "";
  if (pos > 0){
    esq = comb.substring(0,pos);
  }
  if (pos < 8){
    dreta = comb.substring(pos+1,9);
  }
  comb = esq + "1" + dreta;
  return comb;
}
function guanyador(comb){
  var guanya = "0";
  var v = comb.split("");  // Guardem les fitxes en un vector
  // Primer mirem les diagonals
  if((v[0] == v[4]) && (v[4] == v[8]) && (v[4] != "0")){
    guanya = v[4];
  }
  if((v[2] == v[4]) && (v[4] == v[6]) && (v[4] != "0")){
    guanya = v[4];
  }
  // Mirem les columnes
  for(var i=0; i<3; i++){
    if((v[i] == v[i+3]) && (v[i] == v[i+6]) && (v[i] != "0")){
      guanya = v[i];
    }
  }
  // Mirem les fileres
  for(var i=0; i<3; i++){
    var p = 3 * i;
    if((v[p] == v[p+1]) && (v[p] == v[p+2]) && (v[p] != "0")){
      guanya = v[p];
    }
  }
  // Si no hi ha guanyador, mirem si estan totes les caselles ocupades
  if((guanya == "0") && (compta("0",comb) == 0)){
    guanya = "3";  // Empat
  }
  return guanya;
}
function compta(fitxa,comb){
  var cnt = 0;
  for(var i=0; i<comb.length; i++){
    var car = comb.substring(i,i+1);  // Símbol que hi ha a la posició
    if(car == fitxa){
      cnt++;
    }
  }
  return cnt;
}
function tauler(ident,comb){
  var llista = new Array(3);  // La llista contindrà totes les tecles en tres fileres
  var cmpt = 0;  // Comptador de posicions
  for(var i=0; i<3; i++){  // Fileres
    var filera = new Array(3);  // Contindrà una filera de tres botons
    for(var j=0; j<3; j++){  // Fileres
      var estat = comb.substring(cmpt,cmpt+1);  // Estat de la casella
      var text_tecla = fitxes[estat];  // Fitxa actual de la casella
      var cmd_tecla = "/t" + cmpt;
      var tecla = {"text":text_tecla,"callback_data":cmd_tecla};
      filera[j] = tecla;  // Posem el botó a la filera
      cmpt++;
    }
    llista[i] = filera;
  }
  var tecles = {"inline_keyboard":llista};  // Creem el teclat
  var titol = "Et toca tirar";
  sendText(ident,titol,tecles); 
}
function sendText(chatId,text_env,keyBoard){  // Funció que prepara per enviar un text o un teclat a Telegram 
  keyBoard = keyBoard || 0;
  if(keyBoard.inline_keyboard || keyBoard.keyboard){
    var data = {
      method: "post",
      payload: {
        method: "sendMessage",
        chat_id: String(chatId),
        text: text_env,
        parse_mode: "HTML",
        reply_markup: JSON.stringify(keyBoard)
      }
    }
  } else {
    var data = {
      method: "post",
      payload: {
        method: "sendMessage",
        chat_id: String(chatId),
        text: text_env,
        parse_mode: "HTML"
      }
    }
  }
  UrlFetchApp.fetch( telegramUrl + '/', data);
}

Podem fer una segona versió del bot en la que la fitxa no es posi en una posició aleatòria sinó que s'intenti que el bot guanyi la partida. Això implica modificar la funció tirar que hi ha més amunt. Cada cop que s'executa la funció, o sigui, cada cop que li toca tirar al bot, es crida la funció proposta que proposa la casella en la que situar la fitxa; si no proposa cap casella (fet improbable) es situarà en una casella buida escollida aleatòriament.

La funció opcions mira, quan el paràmetre guany és cert, si en un grup de tres caselles el jugador indicat faria una línia. Quan el paràmetre és fals, busca una línia en la que el jugador indicat hi tingui una fitxa i les altres caselles estiguin lliures.

La funció proposta és la que indica a quina casella cal tirar la fitxa del bot. La decisió la pren seguint quatre criteris successius que, a la seva vegada, busquen primer una opció a les diagonals, després una a les columnes i, finalment, una a les fileres. Si en una d'aquestes anàlisis troba una proposta ja no seguirà mirant. Els quatre criteris es resumeixen a la taula següent:

Pas
( j )
Busca una línia amb Jugador Descripció
Fitxes iguals
a la del jugador
Caselles
en blanc
0 2 1 1 (🔴)
Bot
Mira si amb aquesta tirada el bot guanya
1 2 1 2 (🔶)
Usuari
Mira si amb aquesta tirada el jugador guanya
(per impedir-li ocupar la casella)
2 1 2 1 (🔴)
Bot
Mira si queda alguna línia amb una casella
del bot i dues en blanc
3 1 2 2 (🔶)
Usuari
Mira si queda alguna línia amb una casella
de l'usuari i dues en blanc

Fent servir aquesta versió és improbable que el jugador guanyi, o bé guanyarà el bot o s'omplirà el tauler abans que guanyi algú. És possible generar nivells de dificultat intermedis si es prescindeix d'algun dels darrers nivells de la taula, això es pot fer canviant la comparació del bucle (línia marcada en color) per un valor més petit.

Les funcions comentades són les següents:

function tirar(comb){
  // Fa una tirada intentant guanyar
  var pos = proposta(comb);
  if(pos == 9){  // Si no ha trobat cap proposta
    // Fa una tirada aleatòria
    var lliures = new Array();  // Aquí guardarem les posicions lliures 
    for(var i=0; i<comb.length; i++){
      var car = comb.substring(i,i+1);  // Símbol que hi ha a la posició
      if(car == "0"){
        lliures.push(i);
      }
    }
    var ind = Math.floor(lliures.length * Math.random());
    var pos = lliures[ind];
  }
  var esq = "";
  var dreta = "";
  if (pos > 0){
    esq = comb.substring(0,pos);
  }
  if (pos < 8){
    dreta = comb.substring(pos+1,9);
  }
  comb = esq + "1" + dreta;
  return comb;
}
function opcions(vec, a, b, c, fit, guany){
  var m = "0";
  if(guany){
    m = fit;
  }
  // Busca una línia en la que hi ha la fitxa indicada i dues caselles buides
  var p = 9;  // Valor que indica que no hem trobat una posició guanyadora
  if((vec[a] == fit) && (vec[b] == m) && (vec[c] == "0")){
    p = c;
  }
  if((vec[b] == fit) && (vec[c] == m) && (vec[a] == "0")){
    p = a;
  }
  if((vec[c] == fit) && (vec[a] == m) && (vec[b] == "0")){
    p = b;
  }
  return p;
}
function proposta(comb){
  var v = comb.split("");  // Guardem les fitxes en un vector
  var pos = 9;
  // Primer contemplarem quatre casos clars
  for(var j=0; j<4; j++){
    var gny = false;
    var cmp = "2";
    if(j<2){  // Primer mirem si un dels dos pot guanyar
      gny = true;
    }
    if((j==0) || (j==2)){  // Primer si pot guanyar el bot
      cmp = "1";
    }
    // Primer mirem les diagonals
    var pos = opcions(v, 0, 4, 8, cmp, gny);
    if(pos < 9){
      return pos;
    }
    pos = opcions(v, 2, 4, 6, cmp, gny);
    if(pos < 9){
      return pos;
    }
    // Mirem les columnes
    for(var i=0; i<3; i++){
      pos = opcions(v, i, i+3, i+6, cmp, gny);
      if(pos < 9){
        return pos;
      }
    }
    // Mirem les fileres
    for(var i=0; i<3; i++){
      var p = 3 * i;
      pos = opcions(v, p, p+1, p+2, cmp, gny);
      if(pos < 9){
        return pos;
      }
    }
  }
  return pos;  // Si arriba aquí, tornarà un 9
}

 

 

 

 

 

 

 

 

 

 

Llicència de Creative Commons
Aquesta obra d'Oriol Boix està llicenciada sota una llicència no importada Reconeixement-NoComercial-SenseObraDerivada 3.0.