Hello everyone! My name is Nikita, and I would like to share with you some of the practical aspects of developing my Ghost Letters board game (to be released this month by Economics). We tried to approach the development process as systematically as possible, so that our experience may be interesting for someone.
Ghost Letters is a detective board game with secret roles for deduction, bluff and associative thinking. If you like to play "Mafia" or "Imaginarium" - I'm sure you will like it too. From modern board games in genre it is closest to "Mysterium" and "Criminalist".
Assigned tasks
The basic mechanic of Ghost Letters is based on associations between cards with images of various objects (evidence cards). And at one of the early stages of development, we asked the question: "Is it possible to calculate and build a balance in the game on associations?" Actually, why not try it.
The task of balancing associations was something like this:
Minimize the number of strong βunambiguousβ associations. Each card should ideally associate with several others with approximately equal strength.
β β. , .
.
, , . 150 , β . , .
Google Docs
- Google , . - :
Google App Script. JS , -.
, . , .
, . - . .
Gephi
:
. Google Sheets, , .
4 :
0 β
1 β ,
2 β
3 β
, β , .
, 150 150 ( , ββ). , . , , , .
, , β , . , .
:
. id , . , id.
//
function RefreshPictures() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
//
var sheet_a = ss.getSheetByName("");
var range_a = sheet_a.getDataRange();
//
var sheet_p = ss.getSheetByName("");
var range_p = sheet_p.getDataRange();
//
var row = sheet_a.getActiveCell().getRow();
var col = sheet_a.getActiveCell().getColumn();
// id
var id1 = range_a.getCell(row, 1).getDisplayValue().toString();
var id2 = range_a.getCell(1, col).getDisplayValue().toString();
// id
var pos_pic1 = RowOfId(id1, range_p);
var pos_pic2 = RowOfId(id2, range_p);
// , id
if (pos_pic1 != -1) {
// ,
//
var pic1_f = range_p.getCell(pos_pic1, 2).getFormula();
range_a.getCell(2, 1).setFormula(pic1_f);
}
else
{
range_a.getCell(2, 1).setValue("X");
}
if (pos_pic2 != -1) {
var pic2_f = range_p.getCell(pos_pic2, 2).getFormula();
range_a.getCell(2, 2).setFormula(pic2_f);
}
else
{
range_a.getCell(2, 2).setValue("X");
}
}
// id
function RowOfId(id, rng) {
var height = rng.getHeight();
var data = rng.getValues();
for (var i = 1; i < height; i++) {
if (data[i][0].toString() == id) {
return i + 1;
}
}
return -1;
}
. 150 , Google Sheets ( ). -, Google App Script .
// Google Drive
function LoadPicturesFromDrive() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet_p = ss.getSheetByName("");
var range_p = sheet_p.getDataRange();
var art_folder = DriveApp.getRootFolder().getFoldersByName(" ").next()
var files = art_folder.getFiles();
//
var i = 1;
while (files.hasNext()) {
var file = files.next();
var file_name = file.getName();
// id
var id = file_name.slice(0, file_name.indexOf("."));
// id
sheet_p.getRange(i + 1, 1).setValue(id);
//
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
var file_id = file.getId();
// IMAGE
sheet_p.getRange(i + 1, 2).setFormula("=IMAGE(\"" + "https://drive.google.com/uc?export=download&id=" + file_id + "\")");
i = i + 1;
}
}
, Google Sheets Google Drive, - 10% . , , , . API Dropbox, . Dropbox , , .
// Dropbox
function LoadPicturesFromDropbox() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet_p = ss.getSheetByName("");
var range_p = sheet_p.getDataRange();
// POST-
var data = {
"path": "",
"recursive": false,
"include_media_info": false,
"include_deleted": false,
"include_has_explicit_shared_members": false,
"include_mounted_folders": true,
"include_non_downloadable_files": true
};
var payload = JSON.stringify(data);
var options = {
"method" : "POST",
"contentType" : "application/json",
"headers" : {
"Authorization" : "Bearer [ ]"
},
"payload" : payload,
muteHttpExceptions : true
};
// POST-
var url = "https://api.dropboxapi.com/2/files/list_folder";
var response = UrlFetchApp.fetch(url, options);
var json = JSON.parse(response.getContentText());
//
for (var i = 0; i < json.entries.length; i++) {
var name = json.entries[i].name;
//
CreateSharedLink(name);
var sh_link = GetSharedLink(name);
// id
id = name.slice(0, name.indexOf("."))
// IMAGE
sheet_p.getRange(i + 2, 1).setValue(id);
sheet_p.getRange(i + 2, 2).setFormula("=IMAGE(\"" + sh_link+"\")");
}
}
//
function CreateSharedLink(name) {
// POST-
var data = {
"path": ("/" + name),
"settings": {
"requested_visibility": "public",
"audience": "public",
"access": "viewer"
}
};
var payload = JSON.stringify(data);
var options = {
"method" : "POST",
"contentType" : "application/json",
"headers" : {
"Authorization" : "Bearer [ ]"
},
"payload" : payload,
muteHttpExceptions : true
};
// POST-
var url = "https://api.dropboxapi.com/2/sharing/create_shared_link_with_settings";
var response = UrlFetchApp.fetch(url, options);
}
//
function GetSharedLink(name) {
// POST-
var data = {
"path": ("/" + name)
};
var payload = JSON.stringify(data);
var options = {
"method" : "POST",
"contentType" : "application/json",
"headers" : {
"Authorization" : "Bearer [ ]"
},
"payload" : payload,
muteHttpExceptions : true
};
// POST-
var url = "https://api.dropboxapi.com/2/sharing/list_shared_links";
var response = UrlFetchApp.fetch(url, options);
var json = JSON.parse(response.getContentText());
//
var urlForDownload = json.links[0].url.slice(0, -1) + '1';
return urlForDownload;
}
Gephi ( ) CSV. : (: id, label) (: source, target, weight).
//
function CreateGraph() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet_a = ss.getSheetByName("");
var range_a = sheet_a.getDataRange();
var data = range_a.getValues();
var height = range_a.getHeight();
//
var sheet_lbl = ss.getSheetByName("Graph Labels");
//
var sheet_edg = ss.getSheetByName("Graph Edges");
//
var weights = new Array("1", "2", "3");
var edg_num = 0;
//
var lbl_header = ["Id", "Label"];
//
var edg_header = ["Source", "Target", "Weight"];
//
sheet_lbl.clear();
sheet_edg.clear();
//
sheet_lbl.appendRow(lbl_header);
sheet_edg.appendRow(edg_header);
// ,
var tmp_arr = [];
var tmp_arr_len = 0;
// ( )
for (var i = 2; i < height; i++) {
var id1 = data[i][0];
var name1 = data[i][1];
//
var lbl_row = [id1, name1];
sheet_lbl.appendRow(lbl_row);
for (var j = i + 1; j < height; j++) {
var wt = data[i][j].toString();
if (weights.includes(wt)) {
var id2 = data[0][j];
edg_num += 1;
var edg_row = [id1, id2, wt];
tmp_arr.push(edg_row);
tmp_arr_len += 1;
// 100 , .
// ,
// Google App Script
if (tmp_arr_len >= 100) {
sheet_edg.getRange(sheet_edg.getLastRow() + 1, 1, tmp_arr_len, 3).setValues(tmp_arr);
tmp_arr = [];
tmp_arr_len = 0;
}
}
}
}
//
if (tmp_arr_len > 0) {
sheet_edg.getRange(sheet_edg.getLastRow() + 1, 1, tmp_arr_len, 3).setValues(tmp_arr);
tmp_arr = [];
tmp_arr_len = 0;
}
}
, , . , β1β , . β2β β3β. .
, , , - . ββ, . ββ ββ Gephi. , 100 :
, , ββ . , . ββ. , .
Of course, in terms of visualization of the graph and methods of its analysis, there is still work to be done, but this approach has already shown itself well. If you also used graphs in the development of your games, it will be very interesting to know about your experience.
If you are interested in following the development of the project, subscribe to the VKontakte and Instagram game group . This is where I post development notes, plot snippets, and more.