| Bots de conversa | Exemples | Dades pràctiques | Recursos CITCEA | |
| Google Apps Script | Projectes | Interacció | Inici |
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.

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
}

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