沙漠掘金
沙漠掘金是一个企业培训课程游戏,规则大致是:
- 玩家从大本营出发,到达矿山掘金后返回,如果规定的天数未回来,则失败,如果回来,则算成功,按照挖取到的金子排名。
- 玩家携带一定量的食物、水,具有一定的负重。
- 地图分为:村庄、大本营、沙漠、王陵、大山。玩家可以在村庄和大本营补给,大本营、王陵会有特殊事件,大山可以挖到金子,沙漠地区则会遭遇高温和沙尘暴。
- 不同的地区和日期会有不同的天气:晴天、高温、沙尘暴。不同的天气消耗的水和食物不同。
- 出发可以携带道具:指南针和帐篷。但是会占用负重。
这个游戏有多个版本,大致的游戏内容都一样。
生成一个地图
从网上能找到好多地图,奈何不会P图,还想地图能动态生成,就写一个吧。
效果:
这里使用js的canvas生成。
具体生成逻辑:把一个画布分成一定数量的小格子,然后随机出固定个数的地图原点,遍历所有小格子,一遍一遍往原点上堆积,形成固定数量的区域。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas</title>
<script src="./js/aa.js"></script>
<style>
canvas{
position: absolute;
top: 100;
left: 100;
}
#msg{
display: inline-block;
width: 200px;
}
</style>
</head>
<body>
<span>地图块数:</span><input type="number" id="nums"/><button onclick="generate()">开始生成</button>
<div id="msg"></div>
<div class="container">
<canvas id="canvas" width="100" height="100" style="background-color: bisque;"></canvas>
</div>
</body>
</html>
js
let windowWidth;
let windowHeight;
let canvas;
// 数量dom
let nd;
let nn;
let ss = 5;
// 消息dom
let md;
let ctx;
// 画布宽带
let width;
// 画布高度
let height;
// 地图块之间的间隔
let gap = 1;
// 地图块大小
let tw = 5;
// 地图区域数量
let nums;
// 行数
let rn;
// 列数
let cn;
// 地图左右边距
let pw;
// 地图上下边距
let ph;
// 地图
let maps;
async function init() {
windowWidth = window.innerWidth;
windowHeight = window.innerHeight;
width = windowWidth - 100 * 2;
height = windowHeight - 100 * 2;
cn = Math.floor((width + gap)/(tw + gap));
rn = Math.floor((height + gap)/(tw + gap));
width = cn * (tw + gap) - gap;
height = rn * (tw + gap) - gap;
pw = (windowWidth - width)/2;
ph = (windowHeight - height)/2;
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
canvas.style.top = ph + 'px';
canvas.style.left = pw + 'px';
canvas.width = width;
canvas.height = height;
let start = Date.now();
await createMap();
let end = Date.now();
console.log(`计算图形耗时:${(end - start - nn*ss)/1000}s`);
render(maps);
}
/**
* 随机一个数
* @param {Number} min 下限
* @param {Number} max 上限
* @returns
*/
function random(min, max) {
if (max == min) {
return min;
}
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* 随机颜色
*/
function getRandomHexColor() {
function getRandomHexDigit() {
return Math.floor(Math.random() * 16).toString(16);
}
return `#${getRandomHexDigit()}${getRandomHexDigit()}${getRandomHexDigit()}${getRandomHexDigit()}${getRandomHexDigit()}${getRandomHexDigit()}`;
}
/**
* 创建地图
* @returns 地图
*/
async function createMap() {
maps = [];
nn = 0;
console.log(`地图数量:${nums}`);
let all = [];
for (let i = 0; i < rn; i++) {
for (let j = 0; j < cn; j++) {
all.push({
i: i,
j: j
});
}
}
// 先随机出减去首尾的剩余节点
let indexArr = new Array(all.length - 2).fill(0).map((item, index) => index + 1);
for(let i = 0; i < nums; i++) {
let index = random(0, indexArr.length - 1);
let item = indexArr[index];
indexArr.splice(index, 1);
maps.push({
i: all[item].i,
j: all[item].j,
links: [],
start: false,
end: false,
color: getRandomHexColor(),
type: 'shamo'
});
}
// 标记区域类型,c村庄、皇陵、绿洲
// 最靠近中间的两个点,标记为皇陵和绿洲
// 远离中间的点,靠近大本营横坐标的一侧,随机出1/5,最小为1,也可以没有,的区域为村庄
let center = { i: Math.floor(rn / 2), j: Math.floor(cn / 2) };
let juliOrder = maps.filter(it => it.type == 'shamo');
function getDir(p1, p2) {
return Math.sqrt(Math.pow(Math.abs(p1.i - p2.i), 2) + Math.pow(Math.abs(p1.j - p2.j), 2));
}
juliOrder.sort((a, b) => {
return getDir(a, center) - getDir(b, center);
});
//
if (juliOrder.length > 2) {
juliOrder[0].type = 'huangling';
juliOrder[1].type = 'lvzhou';
}
// 随机出村庄
juliOrder = juliOrder.slice(2);
if (juliOrder.length > 0) {
if (juliOrder.length < nums/5) {
juliOrder.forEach(it => it.type = 'cunzhuang');
} else {
let nn = Math.floor(nums/5);
for(let i = 0; i < nn; i++) {
let item = juliOrder[juliOrder.length - i - 1];
item.type = 'cunzhuang';
}
}
}
// 尝试扩充
let copyAll = [...all].filter(item => !maps.find(map => map.i === item.i && map.j === item.j));
let ijMap = copyAll.reduce((x,y) => {
x[y.i + '_' + y.j] = y;
return x;
}, {});
let msg = '';
while (Object.keys(ijMap).length) {
msg = `剩余节点数量:${Object.keys(ijMap).length}`;
console.log(msg);
renderMsg(msg);
nn++;
await sleep(ss);
for (let i = 0; i < maps.length; i++) {
let map = maps[i];
let ps = [{i:map.i,j:map.j}, ...map.links];
// 找到一个边界点
let ks = ps;
let kss = [];
for(let k = 0; k < ks.length; k++) {
// 四个位置,上下左右
let dirs = [
{i: 1, j: 0},
{i: -1, j: 0},
{i: 0, j: -1},
{i: 0, j: 1}
];
kss.push(...dirs.map(dir => ({i: ks[k].i+dir.i, j: ks[k].j+dir.j})));
}
kss = kss.filter(p => p.i >= 0 && p.i < rn && p.j >= 0 && p.j < cn && ijMap[p.i + '_' + p.j]);
if(!kss.length) {
continue;
}
let dir = kss[random(0, kss.length - 1)];
let key = dir.i + '_' + dir.j;
let p = ijMap[key];
if (p) {
map.links.push(p);
delete ijMap[key];
}
}
}
return Promise.resolve();
}
/**
* 渲染出地图
* @param {Object} maps 地图数据
*/
function render(maps) {
console.log('渲染地图', maps);
let colorMap = maps.reduce((x,y) => {
x[y.i + '_' + y.j] = y.color;
y.links.forEach(p => {
x[p.i + '_' + p.j] = y.color;
});
return x;
}, {});
ctx.clearRect(0, 0, width, height);
for (let i = 0; i < rn; i++) {
for (let j = 0; j < cn; j++) {
ctx.fillStyle = colorMap[i + '_' + j] || '#fff';
ctx.fillRect(j * (tw + gap), i * (tw + gap), tw, tw);
}
}
}
/**
* 生成地图
*/
function generate() {
init();
}
function renderMsg(msg) {
md.textContent = msg;
}
function sleep(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
document.addEventListener('DOMContentLoaded', function() {
nd = document.getElementById('nums');
md = document.getElementById('msg');
nd.value = nums = random(15, 50);
nd.addEventListener('change', function() {
let value = this.value;
if (this.value <= 0) {
value = 1;
} else if (this.value > 100) {
value = 100;
}
nd.value = nums = parseInt(value);
});
});