1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2025-04-12 13:53:13 +00:00

Merge 388824e26e2e1d00084dd9b7624dca4827141ef0 into 961e74f73d230d0028efb586db07699120eac888

This commit is contained in:
Erwan 2025-04-04 15:00:28 +02:00 committed by GitHub
commit 4c4cac41b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 677 additions and 59 deletions

View File

@ -557,12 +557,15 @@ $tw.utils.checkVersions = function(versionStringA,versionStringB) {
return $tw.utils.compareVersions(versionStringA,versionStringB) !== -1;
};
/*
Register file type information
options: {flags: flags,deserializerType: deserializerType}
flags:"image" for image types
deserializerType: defaults to type if not specified
*/
/**
* Register file type information in `$tw.config.contentTypeInfo[type]`
* @param {string} type - MIME-style content type string
* @param {string} encoding - Valid NodeJS-style file encoding
* @param {string[]} extension - File extensions that should match this filetype
* @param {Object=} options - Optional object to give some more info
* @param {Array=} options.flags - Useful flags to be used, "image" for image types
* @param {string} [options.deserializerType=type] - key to a deserializer to be used with this filetype, defaults to "type"
*/
$tw.utils.registerFileType = function(type,encoding,extension,options) {
options = options || {};
if($tw.utils.isArray(extension)) {
@ -576,10 +579,27 @@ $tw.utils.registerFileType = function(type,encoding,extension,options) {
$tw.config.contentTypeInfo[type] = {encoding: encoding, extension: extension, flags: options.flags || [], deserializerType: options.deserializerType || type};
};
/*
Given an extension, always access the $tw.config.fileExtensionInfo
using a lowercase extension only.
*/
/**
* @typedef ContentTypeInfo
* @type {object}
* @property {string} encoding
* @property {string} extension
* @property {Array=} flags
* @property {string} deserializerType
*/
/**
* @typedef FileExtensionInfo
* @type {object}
* @property {string} type
*/
/**
* Given an extension, always access the $tw.config.fileExtensionInfo
* using a lowercase extension only.
* @param {string} ext - Extension to find a type for
* @returns {FileExtensionInfo | null | undefined}
*/
$tw.utils.getFileExtensionInfo = function(ext) {
return ext ? $tw.config.fileExtensionInfo[ext.toLowerCase()] : null;
}
@ -1594,9 +1614,15 @@ $tw.Wiki.prototype.processSafeMode = function() {
this.addTiddler(new $tw.Tiddler({title: "$:/DefaultTiddlers", text: "[[" + titleReportTiddler + "]]"}));
};
/*
Extracts tiddlers from a typed block of text, specifying default field values
*/
/**
* Extracts tiddlers from a typed block of text, specifying default field values
* @param {str} type - MIME-style content type string or file extension
* @param {any} text - content to call the deserializer with
* @param {Array=} srcFields - a list of already-defined fields to add to the tiddler
* @param {Object=} options - Option object
* @param {string} options.deserializer - key of a deserializer registered in `$tw.Wiki.tiddlerDeserializerModules`
* @returns {Object[]}
*/
$tw.Wiki.prototype.deserializeTiddlers = function(type,text,srcFields,options) {
srcFields = srcFields || Object.create(null);
options = options || {};
@ -1969,14 +1995,77 @@ $tw.loadTiddlersFromPath = function(filepath,excludeRegExp) {
return tiddlers;
};
$tw.deferredDirSpecs = [];
/*
Load all the tiddlers defined by a `tiddlywiki.files` specification file
filepath: pathname of the directory containing the specification file
options:
loadDeferred {bool|undefined}: wheter or not to load the tiddlers marked as "deferred" in the specification.
*/
$tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
$tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp,options) {
options = options || {};
var loadDeferred = options.loadDeferred || false;
var tiddlers = [];
// Read the specification
var filesInfo = $tw.utils.parseJSONSafe(fs.readFileSync(filepath + path.sep + "tiddlywiki.files","utf8"));
/**
* Helper to fill the tiddler fields with info given in combined fields.
* The combined fields can derive info from file info.
* @param {Object} tiddler tiddle-like object
* @param {Object} combinedFields fields object
* @param {str} filepath canonical path of the file
* @param {str} filename canonical filename
* @param {str} rootPath directory where the tiddler file is
*/
var fillCombinedFields = function (tiddler, combinedFields, filepath ,filename, rootPath) {
var pathname = path.resolve(filepath,filename) // Resolve canonical path of file
$tw.utils.each(combinedFields,function(fieldInfo,name) {
if(typeof fieldInfo === "string" || $tw.utils.isArray(fieldInfo)) {
tiddler[name] = fieldInfo;
} else {
var value = tiddler[name];
switch(fieldInfo.source) {
case "subdirectories":
value = path.relative(rootPath, filename).split(path.sep).slice(0, -1);
break;
case "filepath":
value = path.relative(rootPath, filename).split(path.sep).join('/');
break;
case "filename":
value = path.basename(filename);
break;
case "filename-uri-decoded":
value = $tw.utils.decodeURIComponentSafe(path.basename(filename));
break;
case "basename":
value = path.basename(filename,path.extname(filename));
break;
case "basename-uri-decoded":
value = $tw.utils.decodeURIComponentSafe(path.basename(filename,path.extname(filename)));
break;
case "extname":
value = path.extname(filename);
break;
case "created":
value = new Date(fs.statSync(pathname).birthtime);
break;
case "modified":
value = new Date(fs.statSync(pathname).mtime);
break;
}
if(fieldInfo.prefix) {
value = fieldInfo.prefix + value;
}
if(fieldInfo.suffix) {
value = value + fieldInfo.suffix;
}
tiddler[name] = value;
}
});
}
// Helper to process a file
var processFile = function(filename,isTiddlerFile,fields,isEditableFile,rootPath) {
var extInfo = $tw.config.fileExtensionInfo[path.extname(filename)],
@ -1993,49 +2082,7 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
}
var combinedFields = $tw.utils.extend({},fields,metadata);
$tw.utils.each(fileTiddlers,function(tiddler) {
$tw.utils.each(combinedFields,function(fieldInfo,name) {
if(typeof fieldInfo === "string" || $tw.utils.isArray(fieldInfo)) {
tiddler[name] = fieldInfo;
} else {
var value = tiddler[name];
switch(fieldInfo.source) {
case "subdirectories":
value = path.relative(rootPath, filename).split(path.sep).slice(0, -1);
break;
case "filepath":
value = path.relative(rootPath, filename).split(path.sep).join('/');
break;
case "filename":
value = path.basename(filename);
break;
case "filename-uri-decoded":
value = $tw.utils.decodeURIComponentSafe(path.basename(filename));
break;
case "basename":
value = path.basename(filename,path.extname(filename));
break;
case "basename-uri-decoded":
value = $tw.utils.decodeURIComponentSafe(path.basename(filename,path.extname(filename)));
break;
case "extname":
value = path.extname(filename);
break;
case "created":
value = new Date(fs.statSync(pathname).birthtime);
break;
case "modified":
value = new Date(fs.statSync(pathname).mtime);
break;
}
if(fieldInfo.prefix) {
value = fieldInfo.prefix + value;
}
if(fieldInfo.suffix) {
value = value + fieldInfo.suffix;
}
tiddler[name] = value;
}
});
fillCombinedFields(tiddler, combinedFields, filepath, filename, rootPath)
});
if(isEditableFile) {
tiddlers.push({filepath: pathname, hasMetaFile: !!metadata && !isTiddlerFile, isEditableFile: true, tiddlers: fileTiddlers});
@ -2058,6 +2105,7 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
return arrayOfFiles;
}
// Process the listed tiddlers
// TODO: Patch for deferred loading
$tw.utils.each(filesInfo.tiddlers,function(tidInfo) {
if(tidInfo.prefix && tidInfo.suffix) {
tidInfo.fields.text = {prefix: tidInfo.prefix,suffix: tidInfo.suffix};
@ -2079,6 +2127,11 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
} else {
// Process directory specifier
var dirPath = path.resolve(filepath,dirSpec.path);
if(dirSpec.isDeferred && !loadDeferred){
$tw.deferredDirSpecs.push({filepath, dirSpec})
console.log("Found defer dir:", filepath)
return; //Do not process deferred dirs yet.
}
if(fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
var files = getAllFiles(dirPath, dirSpec.searchSubdirectories),
fileRegExp = new RegExp(dirSpec.filesRegExp || "^.*$"),
@ -2087,7 +2140,25 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
var thisPath = path.relative(filepath, files[t]),
filename = path.basename(thisPath);
if(filename !== "tiddlywiki.files" && !metaRegExp.test(filename) && fileRegExp.test(filename)) {
processFile(thisPath,dirSpec.isTiddlerFile,dirSpec.fields,dirSpec.isEditableFile,dirSpec.path);
if(dirSpec.isDeferred && loadDeferred) {
// resolve file using parser...
// Bypasses processFile
var parser = new $tw.deserializerParsers[dirSpec.deferredType]();
pathname = path.resolve(filepath,thisPath)
var loadedTiddlers = parser.load(thisPath, fs.readFileSync(pathname, null), dirSpec)
var combinedFields = $tw.utils.extend({},dirSpec.fields)
$tw.utils.each(loadedTiddlers,
function(tiddler) {
fillCombinedFields(tiddler, combinedFields, filepath, filename, dirSpec.path)
});
if(dirSpec.isEditableFile) {
tiddlers.push({filepath: pathname, hasMetaFile: !dirSpec.isTiddlerFile, isEditableFile: true, tiddlers: loadedTiddlers});
} else {
tiddlers.push({tiddlers: loadedTiddlers});
}
} else {
processFile(thisPath,dirSpec.isTiddlerFile,dirSpec.fields,dirSpec.isEditableFile,dirSpec.path);
}
}
}
} else {
@ -2617,6 +2688,7 @@ $tw.boot.executeNextStartupTask = function(callback) {
s.push("before:",task.before.join(","));
}
$tw.boot.log(s.join(" "));
console.log(s.join(" "))
// Execute task
if(!$tw.utils.hop(task,"synchronous") || task.synchronous) {
task.startup();

View File

@ -0,0 +1,53 @@
/*\
title: $:/core/modules/startup/load-defer.js
type: application/javascript
module-type: startup
Register tiddlerloader plugins and load deferred tiddlers.
\*/
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
// Export name and synchronous status
exports.name = "load-defer";
exports.platforms = ["node"];
exports.after = ["plugins"];
exports.synchronous = true;
var parsers = {};
$tw.deserializerParsers = parsers;
exports.startup = function(callback) {
var path = require("path");
// First, exec all tiddlerloaders
$tw.modules.forEachModuleOfType("tiddlerLoader",function(title,module) {
for(var f in module) {
if($tw.utils.hop(module,f)) {
parsers[f] = module[f]; // Store the parser class
}
}
});
var specs = $tw.deferredDirSpecs;
$tw.utils.each(specs, function(spec){
var fpath = spec.filepath;
var tiddlers = $tw.loadTiddlersFromSpecification(fpath, undefined, {loadDeferred: true})
$tw.utils.each(tiddlers,function(tiddlerFile) {
$tw.utils.each(tiddlerFile.tiddlers,function(tiddler) {
var relativePath = path.relative($tw.boot.wikiTiddlersPath,tiddlerFile.filepath);
// Keep track of our file tiddlers, so add them to boot.files
$tw.boot.files[tiddler.title] = {
filepath: tiddlerFile.filepath,
type: tiddlerFile.type,
hasMetaFile: tiddlerFile.hasMetaFile,
isEditableFile: tiddlerFile.isEditableFile || tiddlerFile.filepath.indexOf($tw.boot.wikiTiddlersPath) !== 0,
originalpath: relativePath
};
});
$tw.wiki.addTiddlers(tiddlerFile.tiddlers);
});
});
};

View File

@ -269,6 +269,19 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
}
}
}
// Checks for deferred loading/saving field
var deferredFiletype = tiddler.fields.deferredFiletype;
if(deferredFiletype) {
// Check if a parser/loader exists for that filetype
var loader = $tw.deserializerParsers[deferredFiletype];
if(loader) {
// Mark the file as serializable and pass on the info.
fileInfo.deferredFiletype = deferredFiletype;
options.fileInfo.deferredFiletype = deferredFiletype;
fileInfo.hasMetaFile = false;
}
}
// Take the file extension from the tiddler content type or metaExt
var contentTypeInfo = $tw.config.contentTypeInfo[fileInfo.type] || {extension: ""};
// Generate the filepath
@ -380,6 +393,12 @@ exports.generateTiddlerFilepath = function(title,options) {
filepath += char.charCodeAt(0).toString();
});
}
// If we are handling deferred files, ignore normal file extension.
if ((options.fileInfo && options.fileInfo.deferredFiletype)) {
extension = "";
}
// Add a uniquifier if the file already exists (default)
var fullPath = path.resolve(directory, filepath + extension);
if (!overwrite) {
@ -441,7 +460,21 @@ exports.saveTiddlerToFile = function(tiddler,fileInfo,callback) {
return callback(null,fileInfo);
});
} else {
fs.writeFile(fileInfo.filepath,JSON.stringify([tiddler.getFieldStrings({exclude: ["bag"]})],null,$tw.config.preferences.jsonSpaces),"utf8",function(err) {
// If we have a deferred filetype, call parser first and then save.
var content,
fileOptions = "utf8",
filepath = fileInfo.filepath;
if(fileInfo.deferredFiletype) {
var loader = new $tw.deserializerParsers[fileInfo.deferredFiletype]();
var binContent = loader.save(fileInfo.filepath, tiddler);
filepath = binContent.filePath;
content = binContent.buffer;
fileOptions = binContent.fileOptions;
} else {
content = JSON.stringify([tiddler.getFieldStrings({exclude: ["bag"]})],null,$tw.config.preferences.jsonSpaces)
}
fs.writeFile(filepath,content,fileOptions,function(err) {
if(err) {
return callback(err);
}
@ -456,6 +489,7 @@ Save a tiddler to a file described by the fileInfo:
filepath: the absolute path to the file containing the tiddler
type: the type of the tiddler file (NOT the type of the tiddler)
hasMetaFile: true if the file also has a companion .meta file
TODO: Add the deferredFiletype saver here too.
*/
exports.saveTiddlerToFileSync = function(tiddler,fileInfo) {
$tw.utils.createDirectory(path.dirname(fileInfo.filepath));

View File

@ -0,0 +1,159 @@
---
created: 20240523153216127
deferredFiletype: bin/test-tiddler
desc: ''
id: vouch037emrmt5a3eugh7jm
modified: 20240523153420180
tags: test-defer
title: misc.card-and-board-games.gridcannon
type: text/markdown
updated: 1648720082424
---
# An example markdown file
Below is an example taken from my (CyberFoxar's) notes. It's the rules from a card game called gridcannon, feel free to read it if you want, here it's merely placeholder text.
## [Gridcannon: A Single Player Game With Regular Playing Cards](https://www.pentadact.com/2019-08-20-gridcannon-a-single-player-game-with-regular-playing-cards/)
I thought it would be an interesting game design challenge to come up with a single player game you can play with a regular deck of playing cards. My first try, about a month ago, didnt work. But on Sunday I had a new idea, and with one tweak from me and another from my friend [Chris Thursten](https://twitter.com/CThursten), its playing pretty well now! In the video I both explain it and play a full game. Ill write the rules here, but theyll make more sense when you see it played:
<iframe src="https://www.youtube.com/embed/gqmUpQjFHrA" allowfullscreen="allowfullscreen" width="560" height="315" frameborder="0"></iframe>
If youre following closely, you might notice I slip up and fail to kill the king of clubs when he should have died, but I re-kill him with the next play so its fine. I was tired.
**Update:** Since making the video Ive tweaked the rules a bit, so Ill lay out the rules for the revised version here. If youre curious about the evolution, Ill also include the old post and its update below that.
#### Version 2
##### Setup
1. Start with a shuffled deck, including jokers.
2. With the deck face-down, draw cards from the top and lay them out face-up in a 3×3 grid. If you draw any royals, aces or jokers during this, put them on a separate pile and keep drawing til youve made the grid of just number cards.
3. If you did draw some royals, you now place them the same way we will when playing: put it _outside_ the grid, adjacent to the grid card its most similar to. Most similar means:
1. Highest value card of the same suit
2. If none, highest value card of the same colour
3. If none, highest value card
4. If theres a tie, or most similar card is on a corner, you can choose between the equally valid positions
4. Any aces and jokers you drew during set up, keep them face-up to one side. These are Ploys you can play whenever you like, rules below.
5. Once you have a 3×3 grid of number cards, you may choose one to replace if you like: put it on the bottom of the draw pile and draw a new card to replace it.
##### The Goal
Find and kill all the royals.
##### Play
Draw the top card from the deck.
- **If its a royal:** use placement rule above.
- **If it has value 2-10:** you must place it on the grid. It can go on any card with the same or lower value, regardless of suit.
- **If its an ace or joker:** keep it to one side, see **Ploys**.
**Killing royals:** if youre able to place a card on the grid opposite a royal so there are two cards between those two cards **Attack** the royal. The sum of their values must be at least as much as health of the royal to kill them: if its not, you can still place the card, but the royal is unaffected. The value of the card you just placed is not part of the Attack, only the two between.
- **Jacks:** 11 health. The cards Attacking can be any suit.
- **Queens:** 12 health. The cards Attacking must match the colour of the queen to count.
- **Kings:** 13 health. The cards Attacking must match the suit of the king to count.
If you killed the royal, turn it face down but dont remove it new royals you draw still cant be placed in that spot. Once every spot around the grid has a dead royal in it (12 total) youve won.
**Ploys**:
- **Aces are Extractions:** at any time you can use up one of the aces youve drawn to pick up one stack of cards from the grid and put them face-down at the bottom of your draw pile. You can do this even after drawing a card and before placing it. Turn the ace face-down to remember youve used it.
- **Jokers are Reassignments:** at any time you can use up one youve drawn to move the top card of one stack on the grid to another position. The place you move it to must be a valid spot to play the card, and placing it can trigger an Attack the same way a normal play can. Turn the joker face-down to remember youve used it.
**If you cannot place a card:** and you have no Ploys to use, you must add the card as **Armour** to the royal its most similar to (lowest value royal of same suit, failing that lowest of same colour, etc). It increases their health by the value of the card. So a King with a 3 as armour now has 13 + 3 = 16 health. You can add armour to a royal that already has armour it stacks. If a royal ends up with 20+ health (or 19+ for a King), thats a natural loss as theres no longer any way to kill them. (Credit to Chris Thursten for the armour idea!)
**If there are no living royals on the table:** if every spot around the grid has a dead royal on it all 12 youve won! If not, just keep drawing cards until you find a royal, placing the cards in a face-up pile as you go. Once you find a royal, place it, then add the cards you cycled through to the bottom of your deck.
**If the draw pile runs out:** and you havent killed all the royals, use any ploys you have left to fix the situation if you can. If youre out of both cards and ploys and not all royals are dead, youve lost.
##### Scoring
If youve killed all the royals without running out of cards, your score is how many Ploys you have left unspent. So the maximum score is 6.
If you play it, let me know how it goes in the replies to [this tweet](https://twitter.com/Pentadact/status/1163905286375170048)!
##### If youd like to make/release/sell a game based on this
Please do! Id suggest saying “Based on Gridcannon by Tom Francis” somewhere in the credits a link to this post would be cool if possible.
Id also suggest not calling it just Gridcannon, but its fine to use that word in the title.
If youre going to charge for it, maybe think about if theres something youd like to add to the game. Could just be theme/art/flash, or perhaps a mechanics change? Do you have a better idea for scoring it? Should Jokers do something different? This is just a quick prototype, it has lots room for improvement. And digital versions let you do things I couldnt with cards prevent bad deals, know which stacks have resets, start with a more specific grid setup, reward achievements…
Also a heads up that a nearly complete digital version is up, free, and playable in browser [here](https://herebemike.github.io/Gridcannon/site/) by [@HereBeMike](https://twitter.com/HereBeMike).
#### Old Version
##### Setup
1. Start with a shuffled deck, including jokers.
2. With the deck face-down, draw cards and lay out a 3×3 grid, skipping the center position. If you draw any royals during this, put them on a separate pile instead and keep drawing til youve made the grid without royals
3. If you did draw some royals, you now place them the same way we will when playing: put it _outside_ the grid, adjacent to the grid card its most similar to. That means highest value card of the same suit. If none match suit, highest of same colour. If none match colour, highest value. If still tied, you can choose. If the card most like the royal is on a corner, you can choose which side to put it.
**Goal: Find and kill all royals**
##### Play
Draw a card from the deck.
- **If its a royal:** use placement rule above.
- **If it has value 2-10:** you must place it on the grid. It can go on any card with the same or lower value. Empty spots and jokers have value zero, aces have value 1.
- **If its an ace or joker:** these can be played on top of anything, and doing so Resets that stack: pick up the stack and add it to the bottom of the deck. Now place your ace or joker where it was.
**Killing royals:** if youre able to place a card on the grid opposite a royal so there are two cards between those two cards become a payload that you are firing at the royal. The sum of their values is the power of the shot. The power of the shot must be as much or greater than the health of the royal to kill it if its not, it does nothing.
- **Jacks:** 11 health. The cards in the payload can be any suit.
- **Queens:** 12 health. The cards in the payload must match the colour of the queen to count.
- **Kings:** 13 health. The cards in the payload must match the suit of the king to count.
**If you cannot place a card:** you have two options:
- **Hard Reset:** put the card in your shame pile, and Reset any stack of your choice (pick it up, the space becomes blank). Your shame pile is a negative score your goal is to keep it as small as possible.
- **Add Armour:** the card you cant play is added to the royal its most similar to (lowest value royal of same suit) and increases their health by that much. So a King with a 3 as armour now has 13 + 3 = 16 health.
In both cases, the card you cant play never returns to the deck or the grid.
**If you run out of cards in your deck:** choose a stack. Put its top card on your Shame Pile, and take the rest as your new deck.
**If there are no living royals in play:** if all 12 are dead, youve won. If not, draw cards until you find a royal, placing these in a face-up pile as you go. Once you find a royal, place it, then add the cards you cycled through to the bottom of your deck.
The Add Armour option was Chriss idea, and if youre curious, leaving the middle space blank is the one I added after the initial design without that, you can get really screwed by unlucky deals.
Personally, Im still tinkering with some of the rules for a possible next version, and my friend Mike Cook (different Mike) had a great idea for a possible different theme.
##### Revisions To Old Version
Im testing out a new version of the rules to fix a few problems. Give it a try and tell me what you think!
**Problem 1: Remembering resets.** If you remember where you put your resets, you can get them back with your next reset and the deck lasts forever. If you cant, it runs out, to great cost. This is fiddly and puts too much emphasis on memory I dont enjoy that kind of challenge and I dont want the game to be inaccessible to those who cant remember that stuff.
**Problem 2: The Shame Pile.** A lot of people have trouble understanding this concept or just dont like it. A negative score system is unusual, and it also doesnt feel great winning with shame is a bit of a mixed feeling.
**Problem 3: Too easy.** Some are finding it too easy to finish with no shame.
So the changes Im leaning towards are:
**Storing Aces:** When you draw an ace, put it face-up to one side. At any time, you can spend any of your aces from this Stash to pick up any stack including after you draw a card but before you place it. When you use an ace, just turn it face down to remember its used it doesnt go in your deck.
**Jokers:** When you draw a joker, also add it to your Stash. At any time, you can spend it to move a card thats already on the grid. You can only move it to a valid place to play it. You only move the top card, not the whole stack. As with aces, you just turn the joker over, it stays in your stash.
**Scoring:** If you win, your unspent Jokers and Aces are your score. So a perfect game is 6, if you won without using any.
**Failure:** When the deck runs out, if you havent won and you dont have any unspent aces to get more cards with, the game is over. If you draw a card you cant play, and you cant add it as armour without making a royal invincible, and you dont have an ace or joker to get out of it, thats also game over. Ive never had that scenario yet.
**Setup:** You now lay out a full 3×3 grid, dont skip the middle space, but after placing royals, you can take any 1 grid card, add it to the bottom of your deck, and draw a new card to replace it.
In my playtests this version feels like it gives you a lot more to think about, and more control over your success, which is why Im ok with allowing a hard failure state. Im not sure about overall difficulty yet so far Ive never come close to a perfect game, usually scraping through with 2, 1, or 0 special cards unspent. For me its been rare to fail, and always felt like my fault.
That setup change is a bit of a tangent, I just found it aesthetically messy that you start with this blank spot and I had to explain to people that it was a valid spot and what you could play there, etc. I think the new way still gives you decent bad-deal mitigation, and it gives you 2 more chances to draw an ace, joker, or royal early, all of which are advantageous in this version, but probably makes the game harder than the blank-spot system.
If you play, please let me know how you find it! Did you ever have a failure that didnt feel like you could have avoided it? Are you getting perfect games too easily? Reply to [this tweet](https://twitter.com/Pentadact/status/1173287612985032711) or e-mail [tom@pentadact.com](mailto:tom@pentadact.com).

View File

@ -0,0 +1,159 @@
---
created: 20240523074244753
deferredFiletype: bin/test-tiddler
desc: ''
id: vouch037emrmt5a3eugh7jm
modified: 20240523152505267
tags: test-defer
title: misc.card-and-board-games.gridcannon
type: text/markdown
updated: 1648720082424
---
# An example markdown file
Below is an example taken from my (CyberFoxar's) notes. It's the rules from a card game called gridcannon, feel free to read it if you want, here it's merely placeholder text.
## [Gridcannon: A Single Player Game With Regular Playing Cards](https://www.pentadact.com/2019-08-20-gridcannon-a-single-player-game-with-regular-playing-cards/)
I thought it would be an interesting game design challenge to come up with a single player game you can play with a regular deck of playing cards. My first try, about a month ago, didnt work. But on Sunday I had a new idea, and with one tweak from me and another from my friend [Chris Thursten](https://twitter.com/CThursten), its playing pretty well now! In the video I both explain it and play a full game. Ill write the rules here, but theyll make more sense when you see it played:
<iframe src="https://www.youtube.com/embed/gqmUpQjFHrA" allowfullscreen="allowfullscreen" width="560" height="315" frameborder="0"></iframe>
If youre following closely, you might notice I slip up and fail to kill the king of clubs when he should have died, but I re-kill him with the next play so its fine. I was tired.
**Update:** Since making the video Ive tweaked the rules a bit, so Ill lay out the rules for the revised version here. If youre curious about the evolution, Ill also include the old post and its update below that.
#### Version 2
##### Setup
1. Start with a shuffled deck, including jokers.
2. With the deck face-down, draw cards from the top and lay them out face-up in a 3×3 grid. If you draw any royals, aces or jokers during this, put them on a separate pile and keep drawing til youve made the grid of just number cards.
3. If you did draw some royals, you now place them the same way we will when playing: put it _outside_ the grid, adjacent to the grid card its most similar to. Most similar means:
1. Highest value card of the same suit
2. If none, highest value card of the same colour
3. If none, highest value card
4. If theres a tie, or most similar card is on a corner, you can choose between the equally valid positions
4. Any aces and jokers you drew during set up, keep them face-up to one side. These are Ploys you can play whenever you like, rules below.
5. Once you have a 3×3 grid of number cards, you may choose one to replace if you like: put it on the bottom of the draw pile and draw a new card to replace it.
##### The Goal
Find and kill all the royals.
##### Play
Draw the top card from the deck.
- **If its a royal:** use placement rule above.
- **If it has value 2-10:** you must place it on the grid. It can go on any card with the same or lower value, regardless of suit.
- **If its an ace or joker:** keep it to one side, see **Ploys**.
**Killing royals:** if youre able to place a card on the grid opposite a royal so there are two cards between those two cards **Attack** the royal. The sum of their values must be at least as much as health of the royal to kill them: if its not, you can still place the card, but the royal is unaffected. The value of the card you just placed is not part of the Attack, only the two between.
- **Jacks:** 11 health. The cards Attacking can be any suit.
- **Queens:** 12 health. The cards Attacking must match the colour of the queen to count.
- **Kings:** 13 health. The cards Attacking must match the suit of the king to count.
If you killed the royal, turn it face down but dont remove it new royals you draw still cant be placed in that spot. Once every spot around the grid has a dead royal in it (12 total) youve won.
**Ploys**:
- **Aces are Extractions:** at any time you can use up one of the aces youve drawn to pick up one stack of cards from the grid and put them face-down at the bottom of your draw pile. You can do this even after drawing a card and before placing it. Turn the ace face-down to remember youve used it.
- **Jokers are Reassignments:** at any time you can use up one youve drawn to move the top card of one stack on the grid to another position. The place you move it to must be a valid spot to play the card, and placing it can trigger an Attack the same way a normal play can. Turn the joker face-down to remember youve used it.
**If you cannot place a card:** and you have no Ploys to use, you must add the card as **Armour** to the royal its most similar to (lowest value royal of same suit, failing that lowest of same colour, etc). It increases their health by the value of the card. So a King with a 3 as armour now has 13 + 3 = 16 health. You can add armour to a royal that already has armour it stacks. If a royal ends up with 20+ health (or 19+ for a King), thats a natural loss as theres no longer any way to kill them. (Credit to Chris Thursten for the armour idea!)
**If there are no living royals on the table:** if every spot around the grid has a dead royal on it all 12 youve won! If not, just keep drawing cards until you find a royal, placing the cards in a face-up pile as you go. Once you find a royal, place it, then add the cards you cycled through to the bottom of your deck.
**If the draw pile runs out:** and you havent killed all the royals, use any ploys you have left to fix the situation if you can. If youre out of both cards and ploys and not all royals are dead, youve lost.
##### Scoring
If youve killed all the royals without running out of cards, your score is how many Ploys you have left unspent. So the maximum score is 6.
If you play it, let me know how it goes in the replies to [this tweet](https://twitter.com/Pentadact/status/1163905286375170048)!
##### If youd like to make/release/sell a game based on this
Please do! Id suggest saying “Based on Gridcannon by Tom Francis” somewhere in the credits a link to this post would be cool if possible.
Id also suggest not calling it just Gridcannon, but its fine to use that word in the title.
If youre going to charge for it, maybe think about if theres something youd like to add to the game. Could just be theme/art/flash, or perhaps a mechanics change? Do you have a better idea for scoring it? Should Jokers do something different? This is just a quick prototype, it has lots room for improvement. And digital versions let you do things I couldnt with cards prevent bad deals, know which stacks have resets, start with a more specific grid setup, reward achievements…
Also a heads up that a nearly complete digital version is up, free, and playable in browser [here](https://herebemike.github.io/Gridcannon/site/) by [@HereBeMike](https://twitter.com/HereBeMike).
#### Old Version
##### Setup
1. Start with a shuffled deck, including jokers.
2. With the deck face-down, draw cards and lay out a 3×3 grid, skipping the center position. If you draw any royals during this, put them on a separate pile instead and keep drawing til youve made the grid without royals
3. If you did draw some royals, you now place them the same way we will when playing: put it _outside_ the grid, adjacent to the grid card its most similar to. That means highest value card of the same suit. If none match suit, highest of same colour. If none match colour, highest value. If still tied, you can choose. If the card most like the royal is on a corner, you can choose which side to put it.
**Goal: Find and kill all royals**
##### Play
Draw a card from the deck.
- **If its a royal:** use placement rule above.
- **If it has value 2-10:** you must place it on the grid. It can go on any card with the same or lower value. Empty spots and jokers have value zero, aces have value 1.
- **If its an ace or joker:** these can be played on top of anything, and doing so Resets that stack: pick up the stack and add it to the bottom of the deck. Now place your ace or joker where it was.
**Killing royals:** if youre able to place a card on the grid opposite a royal so there are two cards between those two cards become a payload that you are firing at the royal. The sum of their values is the power of the shot. The power of the shot must be as much or greater than the health of the royal to kill it if its not, it does nothing.
- **Jacks:** 11 health. The cards in the payload can be any suit.
- **Queens:** 12 health. The cards in the payload must match the colour of the queen to count.
- **Kings:** 13 health. The cards in the payload must match the suit of the king to count.
**If you cannot place a card:** you have two options:
- **Hard Reset:** put the card in your shame pile, and Reset any stack of your choice (pick it up, the space becomes blank). Your shame pile is a negative score your goal is to keep it as small as possible.
- **Add Armour:** the card you cant play is added to the royal its most similar to (lowest value royal of same suit) and increases their health by that much. So a King with a 3 as armour now has 13 + 3 = 16 health.
In both cases, the card you cant play never returns to the deck or the grid.
**If you run out of cards in your deck:** choose a stack. Put its top card on your Shame Pile, and take the rest as your new deck.
**If there are no living royals in play:** if all 12 are dead, youve won. If not, draw cards until you find a royal, placing these in a face-up pile as you go. Once you find a royal, place it, then add the cards you cycled through to the bottom of your deck.
The Add Armour option was Chriss idea, and if youre curious, leaving the middle space blank is the one I added after the initial design without that, you can get really screwed by unlucky deals.
Personally, Im still tinkering with some of the rules for a possible next version, and my friend Mike Cook (different Mike) had a great idea for a possible different theme.
##### Revisions To Old Version
Im testing out a new version of the rules to fix a few problems. Give it a try and tell me what you think!
**Problem 1: Remembering resets.** If you remember where you put your resets, you can get them back with your next reset and the deck lasts forever. If you cant, it runs out, to great cost. This is fiddly and puts too much emphasis on memory I dont enjoy that kind of challenge and I dont want the game to be inaccessible to those who cant remember that stuff.
**Problem 2: The Shame Pile.** A lot of people have trouble understanding this concept or just dont like it. A negative score system is unusual, and it also doesnt feel great winning with shame is a bit of a mixed feeling.
**Problem 3: Too easy.** Some are finding it too easy to finish with no shame.
So the changes Im leaning towards are:
**Storing Aces:** When you draw an ace, put it face-up to one side. At any time, you can spend any of your aces from this Stash to pick up any stack including after you draw a card but before you place it. When you use an ace, just turn it face down to remember its used it doesnt go in your deck.
**Jokers:** When you draw a joker, also add it to your Stash. At any time, you can spend it to move a card thats already on the grid. You can only move it to a valid place to play it. You only move the top card, not the whole stack. As with aces, you just turn the joker over, it stays in your stash.
**Scoring:** If you win, your unspent Jokers and Aces are your score. So a perfect game is 6, if you won without using any.
**Failure:** When the deck runs out, if you havent won and you dont have any unspent aces to get more cards with, the game is over. If you draw a card you cant play, and you cant add it as armour without making a royal invincible, and you dont have an ace or joker to get out of it, thats also game over. Ive never had that scenario yet.
**Setup:** You now lay out a full 3×3 grid, dont skip the middle space, but after placing royals, you can take any 1 grid card, add it to the bottom of your deck, and draw a new card to replace it.
In my playtests this version feels like it gives you a lot more to think about, and more control over your success, which is why Im ok with allowing a hard failure state. Im not sure about overall difficulty yet so far Ive never come close to a perfect game, usually scraping through with 2, 1, or 0 special cards unspent. For me its been rare to fail, and always felt like my fault.
That setup change is a bit of a tangent, I just found it aesthetically messy that you start with this blank spot and I had to explain to people that it was a valid spot and what you could play there, etc. I think the new way still gives you decent bad-deal mitigation, and it gives you 2 more chances to draw an ace, joker, or royal early, all of which are advantageous in this version, but probably makes the game harder than the blank-spot system.
If you play, please let me know how you find it! Did you ever have a failure that didnt feel like you could have avoided it? Are you getting perfect games too easily? Reply to [this tweet](https://twitter.com/Pentadact/status/1173287612985032711) or e-mail [tom@pentadact.com](mailto:tom@pentadact.com).

View File

@ -0,0 +1,19 @@
{
"directories": [
{
"path": ".",
"filesRegExp": "^.*\\.fmd$",
"isTiddlerFile": true,
"isEditableFile": true,
"fields": {
"title": {"source": "basename-uri-decoded"},
"created": {"source": "created"},
"modified": {"source": "modified"},
"type": "text/markdown",
"tags": ["test-defer"]
},
"isDeferred": true,
"deferredType": "bin/test-tiddler"
}
]
}

View File

@ -0,0 +1,23 @@
{
"description": "Basic client-server edition, showing off deferred loading",
"plugins": [
"tiddlywiki/tiddlyweb",
"tiddlywiki/filesystem",
"tiddlywiki/highlight",
"tiddlywiki/markdown",
"cyberfoxar/frontmatter-tiddler"
],
"themes": [
"tiddlywiki/vanilla",
"tiddlywiki/snowwhite"
],
"build": {
"index": [
"--render","$:/plugins/tiddlywiki/tiddlyweb/save/offline","index.html","text/plain"],
"static": [
"--render","$:/core/templates/static.template.html","static.html","text/plain",
"--render","$:/core/templates/alltiddlers.template.html","alltiddlers.html","text/plain",
"--render","[!is[system]]","[encodeuricomponent[]addprefix[static/]addsuffix[.html]]","text/plain","$:/core/templates/static.tiddler.html",
"--render","$:/core/templates/static.template.css","static/static.css","text/plain"]
}
}

View File

@ -0,0 +1,89 @@
/*\
title: $:/plugins/cyberfoxar/frontmatter-tiddler/fmloader.js
type: application/javascript
module-type: tiddlerLoader
Proof-of-concept plugin for a tiddlerloader.
Tries to find a fenced frontmatter block and parse the content like a tiddler metafile.
\*/
(function(){
/**
* When given binary data, tries to deserialize it into a tiddler.
*
* @param {string} filename - original filename, does not include path
* @param {Buffer} fileBuffer - file as read by NodeJS
* @param {Object} fileSpec - Context/Directory specification-like object
* @returns {Array[Object]} - decoded Tiddler fields to be added to the wiki
*/
function loadTiddlerFromBinary(filename, fileBuffer, fileSpec){
var lines = fileBuffer.toString().split(/(?:\r\n|\r|\n)/g)
var fm, text
for (let index = 0; index < lines.length; index++) {
const line = lines[index];
console.log('read line:', line)
if (line.startsWith('---') && line.trim().length < 4) {
if (fm === undefined){
fm = []
// start fm
} else {
// end fm
text = lines.slice(index+1).join('\n')
break
}
} else if (fm !== undefined) {
// fm has started
fm = fm.concat(line)
}
}
fm = fm ? fm.join('\n') : ""
var myfields = $tw.utils.parseFields(fm)
myfields.text = text
myfields.type = 'text/markdown'
myfields.deferredFiletype = 'bin/test-tiddler'
return [myfields]
}
/**
* When given a Tiddler, binarize it however we like and gives
* back a temporary object holding the data, as well as the path
* where to save it.
* **This must include a file extension.**
*
* @param {string} filePath
* @param {$tw.Tiddler} tiddler - tiddler to be binarized
* @returns {
* {
* filePath: string
* buffer: Buffer,
* fileOptions: {fs.WriteFileOptions | undefined}
* }
* }
*/
function makeBinaryFromTiddler(filePath, tiddler){
// This is a very naive implementation of fences-in-text style.
// It works, though.
var fm = tiddler.getFieldStringBlock({exclude: ["text","bag"]});
var content = "---\n" + (fm) + "\n---\n" + (!!tiddler.fields.text ? tiddler.fields.text : "")
var fpath = filePath.concat(".fmd")
return {
filePath: fpath,
buffer: content,
fileOptions: {
encoding: "utf8"
}
}
}
TiddlerLoaderPlugin.prototype.load = loadTiddlerFromBinary
TiddlerLoaderPlugin.prototype.save = makeBinaryFromTiddler
function TiddlerLoaderPlugin(){
// console.log("TiddlerLoaderPlugin init called")
}
exports["bin/test-tiddler"] = TiddlerLoaderPlugin
})()

View File

@ -0,0 +1,10 @@
{
"title": "$:/plugins/cyberfoxar/frontmatter-tiddler",
"description": "A PoC plugin for loading and saving a text-based tiddlers as a unified file with frontmatter meta and showing off deferred loading",
"author": "CyberFoxar",
"version": "0.0.1",
"name": "Frontmatter loader/parser",
"list": "n/a",
"plugin-type": "plugin",
"stability": "STABILITY_1_EXPERIMENTAL"
}