/* This file is part of Bolixo. Bolixo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Bolixo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Bolixo. If not, see . */ #include #include #include #include #include #include #include #include "bolixo.h" #include "proto/bod_client.protodef" #include "documentd.h" #include "bolixo.m" using namespace std; #include "proto/documentd_sudoku.protoh" #define documentd_sudoku_cell_NOTNEED #define documentd_sudoku_cell2_NOTNEED #define documentd_sudoku_header_NOTNEED #define documentd_sudoku_chat_NOTNEED #include "proto/documentd_sudoku.protoch" static DEBUG_KEY D_SUDOKU("sudoku","sudoku game"); struct SUDOKU_CELL{ unsigned char visible; // This cell is visible unsigned char value; // Value of the cell. If visible, this value is shown in gray unsigned char user_value; // Value entered by the user, if user_value == value, user is right. std::string username; // User who solved this cell. unsigned char user_guess; // When trying to solve complex puzzle, a user // may enter a guess in a cell to help his memory unsigned char guess_color; void reset(){ visible = value = user_value = 0; username.clear(); user_guess = guess_color = 0; } SUDOKU_CELL(){ reset(); } }; struct SUDO_USERPREF{ unsigned color = 0; unsigned last_column = 10; // Last solve cell coordinate unsigned last_line = 10; }; class SUDOKU: public GAME{ SUDOKU_CELL grid[9][9]; std::map seldigs; // Selected digit used when setting a value std::map prefs; // Preferences associated with a user unsigned difficulty=0; // What difficulty was used to initialize the game bool grid_full[9]; // The grid is now complete for a given digit. void compute_grid_full(); void redraw_notify(std::vector &res); void update_msg(bool to_all, PARAM_STRING msg, const char *color, std::vector &res); public: const char *getclass() const{ return "SUDO"; } SUDOKU(){ resetgame(); } void save(DOC_WRITER &fout, bool save_session_info); void load(DOC_READER &r, std::string &msg); void resetgame(); void testwin(std::vector &res); void exec (const char *var, const char *val, const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, std::vector &res, std::vector &unotifies); }; GAME_P make_SUDOKU() { return make_shared(); } void SUDOKU::save (DOC_WRITER &w, bool save_session_info) { documentd_sudoku_header2(&w,revision,difficulty); if (save_session_info){ for (auto &s:seldigs){ documentd_sudoku_select(&w,s.first,s.second); } } vector users; for (auto &a:prefs){ USERAVATAR u; u.name = a.first; u.avatar = a.second.color; u.last_column = a.second.last_column; u.last_line = a.second.last_line; users.emplace_back(u); } documentd_sudoku_userpref (&w,users); vector cells; for (auto &g:grid){ for (auto &gg:g){ CELL3 c; c.visible = gg.visible; c.value = gg.value; c.user_value = gg.user_value; c.username = gg.username; c.user_guess = gg.user_guess; c.guess_color = gg.guess_color; cells.emplace_back(c); } } documentd_sudoku_cell3(&w,cells); vector schat; documentd_copychat (schat,chat); documentd_sudoku_chat2(&w,schat); } void SUDOKU::load (DOC_READER &r, string &msg) { glocal grid; glocal msg; glocal unsigned x=0; glocal unsigned y=0; glocal seldigs; glocal revision; glocal difficulty; glocal prefs; glocal chat; chat.clear(); resetgame(); (&r); glocal.revision = revision; glocal.revision = revision; glocal.difficulty = difficulty; if (glocal.x > 8){ glocal.msg = "Too many cells in the sodoku files"; end = true; }else{ SUDOKU_CELL &gg = glocal.grid[glocal.x][glocal.y]; gg.visible = visible; gg.value = value; gg.user_value = user_value; glocal.y = (glocal.y+1)%9; if (glocal.y == 0) glocal.x++; } if (glocal.x > 8){ glocal.msg = "Too many cells in the sodoku file"; end = true; }else{ SUDOKU_CELL &gg = glocal.grid[glocal.x][glocal.y]; gg.visible = visible; gg.value = value; gg.user_value = user_value; gg.username = username; glocal.y = (glocal.y+1)%9; if (glocal.y == 0) glocal.x++; } if (cells.size() != 81){ glocal.msg = "Invalid number of cells in the sodoku file"; end = true; }else{ unsigned line=0; unsigned col=0; for (auto &c:cells){ SUDOKU_CELL &gg = glocal.grid[line][col]; gg.visible = c.visible; gg.value = c.value; gg.user_value = c.user_value; gg.username = c.username; gg.user_guess = c.user_guess; gg.guess_color = c.guess_color; col++; if (col==9){ col = 0; line++; } } } glocal.seldigs[session] = value; for (auto &u:prefs){ auto &p = glocal.prefs[u.name]; p.color = u.avatar; p.last_column = u.last_column; p.last_line = u.last_line; } for (auto l:lines) glocal.chat.emplace_back(0,l); for (auto l:lines) glocal.chat.emplace_back(l.time,l.line); glocal.msg = "Invalid format for sudoku file"; compute_grid_full(); } void SUDOKU::compute_grid_full() { unsigned stats[9]; for (auto &s:stats) s = 0; for (auto &g:grid) for (auto &gg:g){ if (gg.visible || gg.value == gg.user_value){ stats[gg.value-1]++; } } for (unsigned i=0; i<9; i++) grid_full[i] = stats[i] == 9; } void SUDOKU::resetgame() { //line = column = 0; seldigs.clear(); prefs.clear(); for (auto &g:grid){ for (auto &gg:g){ gg.reset(); } } for (auto &g:grid_full) g=false; } void SUDOKU::update_msg( bool to_all, // The message will be shown to all player or not PARAM_STRING msg, const char *color, vector &res) { VARVAL mvar; mvar.var = to_all ? VAR_NOTIFY : VAR_SCRIPT; js_find_set (mvar.val,"msg","style.color",color,"innerHTML",documentd_escape(msg).c_str()); res.emplace_back(mvar); } void SUDOKU::testwin(vector &res) { unsigned nbok = 0; for (auto &g:grid){ for (auto &gg:g){ if (gg.visible || (gg.value != 0 && gg.value == gg.user_value)) nbok++; } } if (nbok == 9*9){ update_msg (true,MSG_U(I_COMPLETE,"Congratulation! You completed the puzzle"),"blue",res); } } const unsigned nbcolor=6; static const char *colors[nbcolor]={"black","green","blue","orange","lightblue","lightgreen"}; /* Redraw the grid content using javascript. */ void SUDOKU::redraw_notify(vector &res) { VARVAL var; var.var = VAR_NOTIFY; var.val = string_f ("var frm = document.getElementById('buttons-%s');\n",gameid.c_str()); var.val += "if (frm != null){\n"; var.val += "\tvar buttons = frm.getElementsByClassName('button');\n"; for (unsigned i=0; i<4; i++){ var.val += string_f("\tbuttons[%i].style.background=%s;\n",i,i==difficulty ? "'lightblue'" : "null"); } var.val += "}\n"; var.val += string_f ("var doc = document.getElementById('grid-%s');\n",gameid.c_str()); var.val += "if (doc != null){\n"; var.val += "\tvar circles = doc.getElementsByTagName('circle');\n"; var.val += "\tvar txts = doc.getElementsByTagName('text');\n"; // We have to skip the a,b,c...i and then the A unsigned txt_item = 9+1; unsigned circle_item = 0; unsigned line=0; for (auto &g:grid){ unsigned col = 0; for (auto &&gg:g){ const char *color = "none"; const char *circle_color = "none"; const char *guess_color = "none"; unsigned value = gg.value; if (gg.visible){ color = "gray"; }else if (gg.user_value != 0){ value = gg.user_value; if (gg.user_value != gg.value){ color = "red"; }else{ color = "black"; auto a = prefs.find(gg.username); if (a != prefs.end()){ color = colors[a->second.color]; if (a->second.last_column == col && a->second.last_line == line){ circle_color = color; } } } }else if (gg.user_guess != 0){ guess_color = colors[gg.guess_color]; } // We draw the guess var.val += string_f("\tvar e = txts[%u];\n",txt_item); var.val += string_f("\te.style.fill = '%s';\n",guess_color); var.val += string_f("\te.style.stroke = '%s';\n",guess_color); var.val += string_f("\te.textContent = '%u';\n",gg.user_guess); txt_item++; // We draw the value var.val += string_f ("\tvar e = txts[%u];\n",txt_item); var.val += string_f("\te.style.fill = '%s';\n",color); var.val += string_f("\te.style.stroke = '%s';\n",color); var.val += string_f("\te.textContent = '%u';\n",value); txt_item++; var.val += string_f ("\tvar e = circles[%u];\n",circle_item); var.val += string_f("\t\t\te.style.stroke = '%s';\n",circle_color); circle_item++; col++; } // We skip the letter on the side txt_item++; line++; } var.val += "}\n"; // Draw the user ids under their color var.val += string_f ("var doc = document.getElementById('avatar-%s');\n",gameid.c_str()); var.val += "if (doc != null){\n"; var.val += "\tvar txts = doc.getElementsByTagName('text');\n"; const char *tbusers[nbcolor]={nullptr,nullptr,nullptr,nullptr,nullptr,nullptr}; for (auto &a:prefs) tbusers[a.second.color] = a.first.c_str(); for (unsigned i=0; i void SUDOKU::exec ( const char *var, const char *val, const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, vector &res, vector &unotifies) { string error; setactivity(); //tlmp_error ("SUDOKU var %s val%s username %s maywrite %d",var,val,username,maywrite); /* We check if the user has selected a color. If not, we assign the first available */ { auto a = prefs.find(ctx.username); if (a == prefs.end()){ bool takens[nbcolor] = {false,false,false,false,false,false}; for (auto &a:prefs) takens[a.second.color] = true; for (unsigned i=0; i\n",gameid.c_str()); documentd_button_start(lines,gameid); documentd_button_label (lines,MSG_U(I_NEWGAME,"New game")); documentd_button (lines,0,MSG_U(I_SIMPLE,"Simple"),difficulty==0); documentd_button (lines,1,MSG_U(I_EASY,"Easy"),difficulty==1); documentd_button (lines,2,MSG_U(I_INTERMEDIATE,"Intermediate"),difficulty==2); documentd_button (lines,3,MSG_U(I_EXPERT,"Expert"),difficulty==3); documentd_button_end(lines); //lines += "\n"; lines += "\n"; lines += string_f("
\n",val); lines += "
\n"; lines += string_f("\n" ,gameid.c_str(),w9,grid_height,w9,grid_height); unsigned font_base = h9*7/10; unsigned font_base_guess = h9*9/10; unsigned font_size = h9*6/10; unsigned w9_3 = w9/3; unsigned seldig = seldigs[ctx.session]; for (unsigned i=0; i<9; i++){ unsigned offy = g_offy + i*h9; lines += string_f("\n",i==seldig ? "pink" : "white" ,offy,w9,h9); const char *color = grid_full[i] ? "lightgray" : "black"; lines += string_f("%u\n" ,w9_3,offy+font_base,color,color,font_size,i+1); } lines += "\n"; lines += "
\n"; lines += "
\n"; lines += string_f("\n" ,gameid.c_str(),grid_width,grid_height,grid_width,grid_height); unsigned line = 0; unsigned w9_2 = w9/2; unsigned h9_2 = h9/2; unsigned circle_radius = h9_2 - h9/10; lines += string_f("\n" ,g_offx,g_offy,grid_width-1,grid_height-1,g_offx); for (unsigned i=0; i<9; i++){ lines += string_f("%c\n" ,w9*i+w9_2+g_offx,'a'+i); } for (auto &g:grid){ lines += string_f("%c\n" ,g_offy+h9*line+h9_2,'A'+line); lines += string_f("\n",line%3==0 ? "green" : "lightgray" ,g_offx,g_offy+h9*line,grid_width); unsigned col = 0; for (auto &&gg:g){ lines += string_f("\n",col%3==0 ? "green" : "lightgray" ,g_offx+w9*col,g_offy,grid_height); const char *color = "white"; const char *circle_color = "none"; unsigned value = gg.value; if (gg.visible){ color = "gray"; }else if(gg.user_value != 0){ color = "black"; value = gg.user_value; auto a = prefs.find(gg.username); if (a != prefs.end()){ color = colors[a->second.color]; if (a->second.last_column == col && a->second.last_line == line){ circle_color = color; } } if (gg.user_value != gg.value) color = "red"; } unsigned w9_col = w9*col+g_offx; unsigned h9_line = h9*line+g_offy; // We always display the guess, so it can be manipulated later using notifications const char *guess_color = gg.user_guess == 0 ? "none" : colors[gg.guess_color]; lines += string_f("%u\n" ,line,col ,w9_col+w9_3,h9_line+font_base_guess ,guess_color,guess_color,font_size/2,gg.user_guess); // We always draw a circle around the number so it is easy to change its attribute later // By default, the circle is white. lines += string_f("\n" ,line,col,w9_col+w9_2,h9_line+h9_2,circle_radius,circle_color); lines += string_f("%u\n" ,line,col ,w9_col+w9_3,h9_line+font_base ,color,color,font_size,value); col++; } line++; } lines += "\n"; lines += "
\n"; draw_waiting_users(lines,waiting_users_width,grid_height,"flex:0 0 auto;"); lines += "
\n"; /* Colored rectangle. Each user can pick one. His name will show under the selected rectangle. When the user place a digit on the grid, the selected rectangle color will be used. */ //lines += "
\n"; lines += "
\n"; unsigned av_height=w9/2; unsigned av_vheight=30; unsigned av_fontheight=17; if (sp.mobile){ av_height = w9; av_vheight = 60; av_fontheight=34; } lines += string_f("\n" ,gameid.c_str(),avatarw,av_height,avatarw,av_vheight); const char *tbusers[nbcolor]={"","","","","",""}; for (auto &a:prefs) tbusers[a.second.color] = a.first.c_str(); for (unsigned i=0; i\n",colors[i] ,x,avatarcw-5,av_vheight/2,x); lines += string_f("\n",i,x,avatarcw-5,av_height+5); lines += string_f("%s\n" ,x+5,av_vheight,i,"black","black",av_fontheight,tbusers[i]); } lines += "\n"; lines += string_f("
This is a messageCeci est un message
\n",gameid.c_str()); documentd_chat (lines,ctx.username,sp.mobile,chat,sp.width-20,sp.mobile ? 200 : 100); lines += "
\n"; } VARVAL v; v.var = VAR_CONTENT; v.val = lines; res.push_back(v); }else if (strcmp(var,REQ_FOCUS)==0){ // Does not work with the keyboard at all. }else if (strcmp(var,REQ_CHAT)==0){ VARVAL notify_var; notify_var.var = VAR_NOTIFY; appendchat(val,notify_var.val,res,ctx); res.emplace_back(move(notify_var)); }else if (ctx.maywrite){ if (strcmp(var,"place")==0){ unsigned lo,co,guess; if (splitline(val,',',limits(lo,0u,8u),limits(co,0u,8u),enums(guess,{1u,2u}))){ auto &gg = grid[lo][co]; if (gg.visible){ error = MSG_U(E_CANTSELTHISCELL,"You can't set this cell"); }else{ // We can set a value. If it is wrong, it will show in red. // If you click over again, it will erase itself. unsigned newval = seldigs[ctx.session]+1; const char *color = "black"; const char *gcolor = "none"; unsigned gnewval = 0; VARVAL var; var.var = VAR_NOTIFY; var.val = string_f ("var doc = document.getElementById('grid-%s');\n",gameid.c_str()); var.val += "if (doc != null){\n"; if (gg.user_value == 0){ auto a = prefs.find(ctx.username); if (guess == 2){ if (a != prefs.end()){ if (gg.user_guess == newval && gg.guess_color == a->second.color){ // The user erased is guess gg.user_guess = 0; gg.guess_color = 0; gcolor = "none"; gnewval = 0; }else{ gg.user_guess = newval; gg.guess_color = a->second.color; gcolor = colors[a->second.color]; gnewval = newval; } color="none"; } }else{ gg.user_value = newval; gg.username = ctx.username; gg.user_guess = 0; gg.guess_color = 0; if (gg.user_value != gg.value){ color = "red"; }else{ auto a = prefs.find(ctx.username); if (a != prefs.end()){ color = colors[a->second.color]; var.val += "\tvar circles = doc.getElementsByTagName('circle');\n"; var.val += "\tfor(var i = 0; i< circles.length;i++){\n"; var.val += "\t\tvar e = circles[i];\n"; if (a->second.last_line != lo || a->second.last_column != co){ // Erase the last solved cell circle var.val += string_f("\t\tif (e.getAttribute('id') == 'c%u,%u'){\n" ,a->second.last_line,a->second.last_column); var.val += string_f("\t\t\te.style.stroke = 'none';\n"); var.val += "\t\t}else"; } // Draw the new solved cell circle var.val += string_f(" if (e.getAttribute('id') == 'c%u,%u'){\n",lo,co); var.val += string_f("\t\t\te.style.stroke = '%s';\n",color); var.val += "\t\t}\n"; var.val += "\t}\n"; a->second.last_column = co; a->second.last_line = lo; } } } }else if (gg.user_value == newval){ gg.user_value = 0; color = "white"; } setmodified(ctx.username); var.val += "\tvar txts = doc.getElementsByTagName('text');\n"; var.val += "\tfor(var i = 0; i< txts.length;i++){\n"; var.val += "\t\tvar e = txts[i];\n"; var.val += string_f("\t\tif (e.getAttribute('id') == 'g%u,%u'){\n",lo,co); var.val += string_f("\t\t\te.style.fill = '%s';\n",gcolor); var.val += string_f("\t\t\te.style.stroke = '%s';\n",gcolor); var.val += string_f("\t\t\te.textContent = '%u';\n",gnewval); var.val += string_f("\t\t}else if (e.getAttribute('id') == '%u,%u'){\n",lo,co); var.val += string_f("\t\t\te.style.fill = '%s';\n",color); var.val += string_f("\t\t\te.style.stroke = '%s';\n",color); var.val += string_f("\t\t\te.textContent = '%u';\n",newval); var.val += "\t\t\tbreak;\n"; var.val += "\t\t}\n"; var.val += "\t}\n"; var.val += "}\n"; // Update the digit selector on the side to tell which digits have been solved (all 9 has been found) compute_grid_full(); var.val += string_f ("var doc = document.getElementById('sudosel-%s');\n",gameid.c_str()); var.val += "if (doc != null){\n"; var.val += "\tvar txts = doc.getElementsByTagName('text');\n"; for (unsigned i=0; i<9; i++){ if (grid_full[i]){ var.val += string_f("\ttxts[%u].style.stroke='lightgray';\n",i); var.val += string_f("\ttxts[%u].style.fill='lightgray';\n",i); } } var.val += "}\n"; res.push_back(var); documentd_setchanges (res); } }else{ error = MSG_U(E_IVLDPLACESUD,"Invalid place command (need 2 value)"); } }else if (strcmp(var,"select")==0){ unsigned sel = atoi(val); if (sel > 0 && sel < 10){ sel--; if (grid_full[sel]){ error = MSG_U(E_ALLDIGITFOUND,"The nine positions are known for this digit"); }else{ seldigs[ctx.session] = sel; setmodified(ctx.username); VARVAL var; var.var = VAR_SCRIPT; var.val = string_f ("var doc = document.getElementById('sudosel-%s');\n",gameid.c_str()); var.val += "if (doc != null){\n"; var.val += "\tvar paths = doc.getElementsByTagName('path');\n"; for (unsigned i=0; i<9; i++){ var.val += string_f("\tpaths[%u].style.fill='%s';\n",i,i==sel ? "pink" : "white"); } var.val += "}\n"; res.push_back(var); } } }else if (strcmp(var,"avatar")==0){ unsigned avatar = atoi(val); if (avatar < nbcolor){ // User is not allowed to pick an avatar used by another user bool fail = false; for (auto &a:prefs){ if (a.second.color == avatar && a.first != ctx.username){ error = MSG_U(E_USEDAVATAR,"This color is already used!"); fail = true; break; } } if (!fail){ auto &u = prefs[ctx.username]; unsigned old_avatar = u.color; u.color = avatar; // Change all the guess color for (auto &g:grid){ for (auto &gg:g){ if (gg.user_guess != 0 && gg.guess_color == old_avatar){ gg.guess_color = avatar; } } } redraw_notify(res); } }else{ documentd_error (res,"Color selection from 0 to 5"); } }else if (strcmp(var,"newgame")==0){ glocal grid; unsigned uval = atoi(val); if (uval > 3){ documentd_error (res,"Difficulty from 0 to 3"); }else{ difficulty = uval; static const char *tbdiff[]={"simple", "easy", "intermediate", "expert"}; (string_f("qqwing --generate 1 --compact --solution --difficulty %s",tbdiff[difficulty]),10); debug_printf (D_SUDOKU,"read qqwing %s\n",line); if (noline < 9){ auto &g = glocal.grid[noline]; for (unsigned i=0; i<9; i++){ char car = line[i]; auto &gg = g[i]; gg.reset(); if (car != '.'){ gg.visible = 1; gg.value = car - '0'; } } } if (noline >= 10 && noline < 19){ auto &g = glocal.grid[noline-10]; for (unsigned i=0; i<9; i++){ g[i].value = line[i]-'0'; } } return 0; tlmp_error ("sudoku command=%s line=%s\n",command,line); return 0; setmodified(ctx.username); compute_grid_full(); redraw_notify(res); // Make all nine digits available VARVAL var; var.var = VAR_NOTIFY; var.val = string_f ("var doc = document.getElementById('sudosel-%s');\n",gameid.c_str()); var.val += "if (doc != null){\n"; var.val += "\tvar txts = doc.getElementsByTagName('text');\n"; for (unsigned i=0; i<9; i++){ var.val += string_f("\ttxts[%u].style.stroke='black';\n",i); var.val += string_f("\ttxts[%u].style.fill='black';\n",i); } var.val += "}\n"; res.push_back(var); } }else{ tlmp_error ("sudoku invalid command %s\n",var); } }else{ error = MSG_U(E_READONLY,"You do not have write access to this game"); } if (error.size() > 0){ update_msg(false,error,"red",res); }else{ update_msg(false," ","white",res); } }