我建造的
Goldroad是浏览器中的每日硬币益智游戏。您的黄金是找到两个给定硬币之间的最佳路径。最好的道路是给您最大黄金的道路。
类别提交:
选择自己的冒险
尽管该应用程序将使用Atlas更改流的实时方面,但这仍在进行中。它还使用Google Cloud Run进行托管,但没有使用其他Google API。因此,选择自己的冒险似乎是正确的选择。
应用链接
可以尝试使用here
屏幕截图
游戏页面
关于页面
描述
Goldroad是浏览器中的每日硬币益智游戏。游戏的目的是找到两个给定硬币之间的最佳路径。最好的路径是允许用户收集最大金币的路径。
当前功能
- 每日游戏的无限剧
- Auto Game在UTC上午12:00进行新游戏刷新
- 自动从后端创建新游戏
链接到源代码
goldroad
The repo for the goldroad puzzle game. This game was created as part of the MongoDb Atlas Hackathon with Dev.to.
Frontend
Frontend is using ReactJs with Vanilla CSS.
Backkend
Backend is Mongo Atlas App Services with MongoDB.
允许许可证
来源是根据麻省理工学院许可证的许可。
背景
几个月前,我遇到了one twitter post宣布新的每日益智游戏。我尝试了一下,非常喜欢游戏,以至于我使用Python和published blogs记录了该过程。
从那时起,我想创建一个类似但不同的游戏。最近,我正在观看一些视频,解释了贪婪的算法,以找到最佳的道路,这立刻凝结着瓷砖游戏。因此,在修补了不同的方法和自定义之后,这里就是这样。
我如何建造它
我很晚才知道黑客马拉松。因此,坚持我的舒适区并将Vuejs与Firebase一起使用(当然,考虑到您应该使用MongoDB),这将变得更容易。但是这样做的意义何在?黑客马拉松的精神是尽可能多地使用给定的技术(至少这就是我所相信的)。
所以,我使用mongodb创建了一个帐户(第一次: - )。
创建一个帐户后,它要求您创建一个集群,我使用以下设置(也可以免费提供共享群集的终止保护)。
在此过程中,您还可以创建数据库和项目。由于我需要在本地与该项目互动,因此我通过访问管理器创建了一个API键。
本地设置
首先,我们需要使用
全局安装realm-cli
npm install -g mongodb-realm-cli
然后使用
进行身份验证
realm-cli login --api-key="<my api key>" --private-api-key="<my private api key>"
现在我们准备使用CLI与后端进行交互。
使用
创建本地React项目
yarn create react-app my-app
在我的情况下,我将所有文件移至一个名为Frontend的内部文件夹。然后,我在根目录内创建了一个后端文件夹,并使用
创建了一个新的后端应用程序
realm-cli app create --environment development --cluster <my_cluster_name>
在此时间点,项目文件夹看起来如下:
App Services HTTPS端点
本地项目创建后,手头的第一个任务是能够创建新游戏,并将其存储在DB中。我需要一个后端工人来执行我已经测试过的本地游戏生成代码。这个后端工人需要支持两种方法
- 能够被称为临时的能力(以便在紧急情况下我可以很容易地创建新游戏)
- 能够通过某个触发器自动调用的能力(因此我不需要称其为临时: - ) )
https端点由功能后端完美地符合帐单。找不到使用Realm-CLI交互方式创建端点的方法,因此请使用App Services UI(在App Services Services选项卡中使用应用程序中的应用程序,然后从左侧侧栏菜单中选择HTTPS端点)。 p>
创建端点时,您可以为备份此端点的函数配置身份验证(默认情况下是应用程序auth)。由于我将调用此端点临时(使用Postman等),因此我必须将身份验证更改为System
。在创建端点时,这是不可能的。之后,我们可以通过转到备份功能设置来做到这一点,并将身份验证更改为System
。
为了给我们的端点一些保护,我将端点授权更改为Verify Payload Signature
。通过这样做,要成功打电话给我们的端点,我们需要用一个秘密签署有效载荷(我们在应用程序UI本身中创建的有效负载)。
由于到目前为止我们没有任何功能,所以我从UI本身创建了一个新功能,并接受了默认生成的代码。
现在我们准备好测试此端点了。启动的邮递员配置了端点URL,选择了正确的方法(在我的情况下帖子),添加一些虚拟主体和BAM! Send
。正如预期的那样,我们会有一个错误
{
"error": "expected to find Endpoint-Signature in header",
"error_code": "InvalidParameter",
"link": "https://realm.mongodb.com/groups/6385ddf6b41c43346bccf9ff/..."
}
我们需要使用秘密密钥签署有效载荷。为此,使用Postman,我们可以在URL地址栏下的Pre-request Script
选项卡中添加以下代码
const signBytes = CryptoJS.HmacSHA256(pm.request.body.raw, '<my_secret_code>');
const signHex = CryptoJS.enc.Hex.stringify(signBytes);
pm.request.headers.add({
key: "Endpoint-Signature",
value: "sha256="+signHex
});
我为此挣扎了一段时间,原因是端点创建UI本身。它显示以下示例
curl \
-H "Content-Type: application/json" \
-d '{"foo":"bar"}' \
-H 'X-Hook-Signature:sha256=<hex-encoded-hash>' \
https://data.mongodb-api.com/app/<my_app_id>/endpoint/<endpoint_route>
所以我正在尝试使用X-Hook-Signature
,但是端点需要Endpoint-Signature
,就像我们较早的错误一样可以明显看出。我还需要一些时间来找出标题值的“ SHA256 =”前缀。但是一切都很好,结束了: - )。
在这里,生成新游戏的功能代码,找出解决方案并将其存储在名为Games的DB集合中。
const ROWS = 6;
const COLS = 6;
// Generate a random number between min (included) & max (excluded)
const randomInt = (min, max) => {
return Math.floor(Math.random() * (max - min)) + min;
};
const getCoinsWithWalls = (start, end, count) => {
const coinColIndices = [];
while (coinColIndices.length < count) {
const index = randomInt(start, end);
if (!coinColIndices.includes(index)) {
coinColIndices.push(index);
}
}
return coinColIndices;
};
const addJob = (jobs, src, currJob) => {
jobs.push({
coins: JSON.parse(JSON.stringify(currJob.coins)),
src,
dst: currJob.dst,
pastMoves: JSON.parse(JSON.stringify(currJob.pastMoves)),
total: currJob.total,
});
};
const handleJob = (jobs, job) => {
const row = job.src[0];
const col = job.src[1];
const srcNode = job.coins[row][col];
srcNode.finished = true;
if (row === job.dst[0] && col === job.dst[1]) {
job.total += srcNode.value;
job.pastMoves.push(`${job.dst[0]}${job.dst[1]}`);
return true;
}
const neighbors = {
prevNode: col > 0 ? job.coins[row][col - 1] : null,
nextNode: col < COLS - 1 ? job.coins[row][col + 1] : null,
topNode: row > 0 ? job.coins[row - 1][col] : null,
bottomNode: row < ROWS - 1 ? job.coins[row + 1][col] : null,
};
job.total += srcNode.value;
job.pastMoves.push(srcNode.id);
for (const key in neighbors) {
const neighbor = neighbors[key];
if (neighbor && !neighbor.finished) {
if (key === 'prevNode' && neighbor.wall !== 2 && srcNode.wall !== 4) {
addJob(jobs, [row, col - 1], job);
}
if (key === 'nextNode' && neighbor.wall !== 4 && srcNode.wall !== 2) {
addJob(jobs, [row, col + 1], job);
}
if (key === 'topNode' && neighbor.wall !== 3 && srcNode.wall !== 1) {
addJob(jobs, [row - 1, col], job);
}
if (key === 'bottomNode' && neighbor.wall !== 1 && srcNode.wall !== 3) {
addJob(jobs, [row + 1, col], job);
}
}
}
return false;
};
const findBestRoute = (coins, start, end) => {
const src = [parseInt(start[0]), parseInt(start[1])];
const dst = [parseInt(end[0]), parseInt(end[1])];
const jobs = [{ coins, src, dst, pastMoves: [], total: 0 }];
const results = [];
while (jobs.length) {
const job = jobs.shift();
if (handleJob(jobs, job)) {
results.push({
total: job.total,
moves: job.pastMoves.length,
path: job.pastMoves,
});
}
}
if (results.length) {
results.sort((result1, result2) => {
return result2.total - result1.total;
});
return results[0];
} else {
console.log(`No valid path found`);
}
};
exports = async function (req) {
let reqBody = null;
if (req) {
if (req.body) {
console.log(`got a req body: ${req.body.text()}`);
reqBody = JSON.parse(req.body.text());
} else {
console.log(`got a req without req body: ${JSON.stringify(req)}`);
}
}
const coins = [];
for (let row = 0; row < ROWS; row++) {
coins.push([]);
const blockages = getCoinsWithWalls(0, COLS, 2);
for (let col = 0; col < COLS; col++) {
const coin = {
id: `${row}${col}`,
value: randomInt(1, 7),
wall: 0,
};
if (blockages.includes(col)) {
coin.wall = randomInt(1, 5);
}
coins[row].push(coin);
}
}
const start = `${randomInt(2, 4)}${randomInt(2, 4)}`;
let end = randomInt(1, 5);
if (end === 1) {
end = '00';
} else if (end === 2) {
end = `0${COLS - 1}`;
} else if (end === 3) {
end = `${ROWS - 1}0`;
} else {
end = `${ROWS - 1}${COLS - 1}`;
}
const date = new Date();
const gameEntry = {
coins,
start,
end,
active: false,
createdAt: date,
updatedAt: date,
};
const startTime = Date.now();
const bestMove = findBestRoute(JSON.parse(JSON.stringify(coins)), start, end);
console.log(
`Total time taken for finding bestRoute: ${Date.now() - startTime} ms`
);
if (bestMove) {
console.log(`best path: ${JSON.stringify(bestMove)}`);
gameEntry.maxScore = bestMove.total;
gameEntry.maxScoreMoves = bestMove.moves;
gameEntry.hints = bestMove.path;
const mongoDb = context.services.get('gcp-goldroad').db('goldroadDb');
const gamesCollection = mongoDb.collection('games');
const appCollection = mongoDb.collection('app');
const config = await appCollection.findOne({ type: 'config' });
console.log('fetch config data:', JSON.stringify(config));
console.log('lastPlayableGame:', config.lastPlayableGame);
if (config) {
if (config.lastPlayableGame) {
const lastPlayableDate = config.lastPlayableGame.playableAt;
lastPlayableDate.setUTCDate(lastPlayableDate.getDate() + 1);
gameEntry.playableAt = lastPlayableDate;
gameEntry.gameNo = config.lastPlayableGame.gameNo + 1;
if (reqBody) {
if (reqBody.active) {
gameEntry.active = true;
}
if (reqBody.current) {
gameEntry.current = true;
}
}
} else {
const playableDate = new Date();
playableDate.setUTCHours(0, 0, 0, 0);
gameEntry.playableAt = playableDate;
gameEntry.gameNo = 1;
gameEntry.current = true;
gameEntry.active = true;
}
}
let result = await gamesCollection.insertOne(gameEntry);
console.log(
`Successfully inserted game with _id: ${JSON.stringify(result)}`
);
result = await appCollection.updateOne(
{ type: 'config' },
{
$set: {
lastPlayableGame: {
playableAt: gameEntry.playableAt,
gameNo: gameEntry.gameNo,
_id: result.insertedId,
},
},
}
);
console.log('result of update operation: ', JSON.stringify(result));
}
return gameEntry;
};
我在本地编辑了上述代码(在后端文件夹的functions
文件夹中)。每当您在App Services UI上进行更改时,您都需要通过运行
将更改提交本地代码库
realm-cli pull
当我们在本地进行任何更改后,我们需要将更改推向遥控器
realm-cli push
在上述功能中,由于两个问题,我也挣扎了一段时间。
- 函数文档说它部分支持
crypto
模块。我正在使用crypto.randomInt
进行随机数。但是该功能未能提供无用的错误消息(TypeError: Value is not an object: undefined
)。经过大量反复试验,最终发现randomInt
方法可以在App Services Crypto中获得,因此创建了自己的功能来生成随机数。有一些有意义的错误消息真的很棒。 - 我正在使用
Set
跟踪生成的墙壁(blockages
)。但是在检查if (blockages.has(col))
时,我将获得false
的所有列,除0
外。同样,这需要进行一些测试,并弄清楚。因此,用列表替换了设置。不确定是否支持Set
(稍后再检查)。
数据访问规则
要使用Realm Web SDK获取数据(我添加到React应用程序中),我们需要为我们创建的每个集合配置数据访问规则。我们可以使用App Services UI(或本地设置中的JSON文件,但这需要一些时间熟悉)。
计划触发器
由于我们的难题应该每天都会改变,因此我们需要触发器才能自动这样做。 App Services提供了许多类型的触发器,出于我的目的,我需要一个简单的CRON时间表,该时间表每天在UTC上午12:00运行。使用UI的高级时间表类型,并将时间表设置为0 0 * * *
,然后调用另一个功能以更改游戏。
以下是该功能的代码,它使当前的游戏非流动性,并使下一个游戏排在线(由文档的下一个gameNo
字段识别)。
exports = async function () {
const mongoDb = context.services.get('gcp-goldroad').db('goldroadDb');
const gamesCollection = mongoDb.collection('games');
const currGame = await gamesCollection.findOne({ current: true });
if (currGame) {
console.log('got the current game: ', JSON.stringify(currGame));
const date = new Date();
const nextGameDate = new Date();
nextGameDate.setUTCHours(0, 0, 0, 0);
nextGameDate.setUTCDate(nextGameDate.getDate() + 1);
await gamesCollection.bulkWrite(
[
{
updateOne: {
filter: { gameNo: currGame.gameNo + 1 },
update: {
$set: {
current: true,
active: true,
updatedAt: date,
playedAt: date,
nextGameAt: nextGameDate,
},
},
},
},
{
updateOne: {
filter: { _id: currGame._id },
update: { $set: { current: false, updatedAt: date } },
},
},
],
{ ordered: true }
);
console.log('after the bulkWrite Op');
} else {
console.log('Error! No current game found.');
}
};
数据库触发器
由于我们每天都会自动消费存储的游戏文档,因此我们还需要找到一种以相同方式生成游戏的方法。这可以使用数据库触发器完成。每当我们将其中一个游戏标记为当前游戏时,我们都会收听数据库触发器,并通过调用我们的旧功能来生成新游戏。
选择的Operation Type as Update
。由于在游戏刷新2文档上进行了更新(一个是当前游戏,另一个游戏),因此使用匹配表达式(高级可选设置)仅触发下一个游戏文档的功能。我们还在游戏刷新上更新playedAt
字段,因此使用了。
{"updateDescription.updatedFields.playedAt":{"$exists":true}}
某种程度上与布尔场(current
)的表达式匹配表达不起作用。
{"updateDescription.updatedFields":{"current":true}}
没有尝试点表示法
{"updateDescription.updatedFields.current": true}
我们本可以在预定的触发器本身中创建一个新游戏(在这里刷新了每日难题),但是在任何功能运行时间内都有150 seconds
的上限。因此,数据库触发器更有意义,因为它将为我的游戏生成功能提供一些额外的缓冲时间。
匿名身份验证和验证触发器
我们还想存储用户的播放历史记录。因此从UI启用了相同的功能。还添加了一个auth触发器,以便我们可以将新创建的用户保存到DB。
。 auth触发函数代码,该函数代码在新的USR创建中被触发。
exports = async function (authEvent) {
const { user, time } = authEvent;
const mongoDb = context.services.get('gcp-goldroad').db('goldroadDb');
const usersCollection = mongoDb.collection('users');
const userData = { _id: user.id, ...user, createdAt: time, updatedAt: time };
userData.data = {
currStreak: 0,
longestStreak: 0,
isCurrLongestStreak: false,
solves: 0,
played: 0,
};
delete userData.id;
const res = await usersCollection.insertOne(userData);
console.log('result of user insert op: ', JSON.stringify(res));
};
没有使用任何其他类型的AUTH提供商,因为我不希望玩家担心此时登录。
Google云运行托管
我也想尝试托管应用程序服务,但显然这仅适用于付费帐户。 Google Cloud进行营救。使用Google Cloud运行来托管应用程序前端。
为此,需要在前端root文件夹中创建以下内容(无论frontend package.json ass)
FROM node:lts-alpine as react-build
WORKDIR /app
COPY . ./
RUN yarn
RUN yarn build
# server environment
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/configfile.template
COPY --from=react-build /app/build /usr/share/nginx/html
ENV PORT 8080
ENV HOST 0.0.0.0
EXPOSE 8080
CMD sh -c "envsubst '\$PORT' < /etc/nginx/conf.d/configfile.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"
还在前端根文件夹中创建了一个nginx.conf
文件
server {
listen $PORT;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri /index.html;
}
gzip on;
gzip_vary on;
gzip_min_length 10240;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
gzip_disable "MSIE [1-6]\.";
}
然后使用使用(请记住要创建一个Google Cloud Project,并启用Cloud Run API,Google Container Registry API&Cloud Build API API)
gcloud builds submit --tag gcr.io/<project_id>/app
构建成功后,我们可以通过运行
部署应用程序
gcloud run deploy --image gcr.io/<project_id>/app --platform managed
和瞧,我们可以通过控制台中提到的服务URL访问我们的前端。
进一步的增强
- 游戏玩法的前端代码需要一些重构
- 需要添加游戏统计数据和分析
- 当前用户的游戏历史记录被存储在数据库中(需要进一步测试),但在任何地方都没有显示。需要为同一 创建另一个应用程序路由
- 缓存尚未在任何地方使用。也许我们可以利用firebase托管进行应用程序缓存,以及缓存当前的游戏数据
- 用于用户游戏与DB相关的互动,需要使用更改流 /观看。< / li>
- 如果使用新游戏,则更新用户,如果他们使用应用程序
结论
总的来说,这是一个非常好的体验,它是通过MongoDB Atlas&App Services来建立游戏的。我们绝对可以改进文档,但是我很高兴这次使用并熟悉Atlas&App Services。
希望您喜欢阅读文章,并喜欢玩the game。