This commit is contained in:
Morten Olsen
2023-05-19 14:51:17 +02:00
commit b574c375af
89 changed files with 11574 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
import { Bottom } from '@/ui/base/bottom';
import { createMap } from "@shipped/engine";
import { Playground } from "@/ui/playground";
import { VesselInfo } from "@/ui/playground/utils/vessel";
# Example
This is an example bot which will select one of the five nearest port at random and sail to it.
Once it reached the port it will do a refuel, and if the goods there are cheaper than the ones it has on board it will buy them. Otherwise it will sell its goods.
<Playground
script={import('./script.ts?raw')}
utils={(
<>
<VesselInfo />
</>
)}
/>
<Bottom
links={[
{ title: 'Play', href: '/game' },
{ title: 'Ideas', href: '/features' },
]}
/>

View File

@@ -0,0 +1,92 @@
import { MapTile, PortMapTile, CaptainAI, CaptainCommand, calculatePrice } from "@shipped/engine";
import PF from 'pathfinding';
// We start by defining a function that will be called every turn
const captain: CaptainAI = ({ vessel, map, currentPort }) => {
// Then we create an array to hold any commands we want to send to the vessel
const commands: CaptainCommand[] = [];
// If the vessel doesn't have a plan, give it one
if (!vessel.plan || vessel.plan.length === 0) {
// We arrived at a port, so we should fuel up
commands.push({
type: 'fuel-up',
});
if (currentPort) {
// Get the current price of goods at the port
const goodsPrice = calculatePrice(currentPort);
// And the amount we've paid for goods
const paidPrice = parseFloat(vessel.data.paid || '0');
// If the price is higher than what we paid, and we have goods, sell them
if (goodsPrice > paidPrice && vessel.goods > 0) {
commands.push({
type: 'sell',
amount: vessel.goods,
});
// If the price is lower than what we paid, and we have space, buy them
} else {
commands.push({
type: 'buy',
amount: 10,
});
commands.push({
type: 'record',
name: 'paid',
data: goodsPrice.toString(),
});
}
}
// We get our list of port tiles
const portTiles = findPorts(map);
// And calculate the distance to each one
const withDistance = portTiles.map((port) => ({
...port,
distance: Math.sqrt(
Math.pow(port.x - vessel.position.x, 2) +
Math.pow(port.y - vessel.position.y, 2)
),
}));
// We select the 5 closest
const closestPort = withDistance.sort((a, b) => a.distance - b.distance).slice(0, 5);
// And pick a random one
const randomPort = closestPort[Math.floor(Math.random() * closestPort.length)];
const grid = new PF.Grid(
map.map((row) => row.map((tile) => tile.type !== 'land' ? 0 : 1))
);
const finder = new PF.AStarFinder();
// We then use A* to find a path to it
const path = PF.Util.compressPath(finder.findPath(
Math.round(vessel.position.x),
Math.round(vessel.position.y),
randomPort.x,
randomPort.y,
grid,
));
commands.push({
type: 'update-plan',
// And then update the plan to navigate to our random port
plan: path.map(([x, y]) => ({ x, y })),
});
}
// Hand of the wheel
return commands;
}
// Since we only have a tile map, we want to convert it into a list
// of all the port tiles
const findPorts = (tiles: MapTile[][]) => {
const portTiles = tiles.map((row, y) => row.map((tile, x) => ({
x,
y,
tile: tile as PortMapTile,
}))).flat().filter(({ tile }) => tile.type === 'port');
return portTiles;
};
export default captain;

View File

@@ -0,0 +1,20 @@
import { Bottom } from '@/ui/base/bottom';
# Introduction
Congratulations, you just completed you Maritime 101 and is now ready to start your new **ocean empire**!
The first thing you do is spend your life savings buying a (_very_) used vessel. You then spend the next few months fixing it up and getting it ready for your first voyage. You are now ready to set sail!
But the ship and the repair means that you are already out of money, so you can not afford to hire on a crew.
**Not to worry**; you once fixed you VCR with only minor electrical burns, so setting up a ship for remote control should be a piece of cake.
But remote controlling it requires a connection, and maritime connectivity is expensive. You have to find a way to control the ship from on board the ship it self, you need a **bot**!
Luckily you have extensive knowladge in Excel macros, so you are just the right person for the job - **Good luck!**
<Bottom
links={[
{ title: 'Next: The game', href: '/articles/rules/intro' },
]}
/>

View File

@@ -0,0 +1,50 @@
import { Bottom } from '@/ui/base/bottom';
import { createMap } from "@shipped/engine";
import { Playground } from "@/ui/playground";
import { VesselInfo } from "@/ui/playground/utils/vessel";
export const map = createMap(30, 10, {
ports: {
'25,3': {},
'7,5': {},
'5,7': {},
}
});
# Fuel
A ship needs fuel to move. Your ship will start with a full tank of fuel, which will depleted as you move around the map.
Once your fuel is depleted, you will be unable to move, and the ship will be stuck.
To refuel you need to visit a port. Port are black dots on the map. Once you are in a port, you can issue a `{ type: 'fuel-up', amount?: number }` command to refuel your ship.
If you do not specify an amount, your ship will be refueled to full.
If you do not have enough cash to refuel the requested amount, the command will be ignored.
You can calulate the price of fuel as `desiredAmount * currentPort.fuelPrice`.
Different ports can have different fuel prices, and you can use the `ports` property to look at prices on different ports.
To find the location of a port, you can use the `map` property. which contains all the tiles of the map. here the tile will have a `type` of `port` and an `id` of the port.
### Visibility
when you are at a port the `ports` property will contain all ports on the map, and their fuel prices.
When you are not at a port, the `ports` property will only contain the ports that are within your ships visibility range (`vessel.stats.visibility`).
<Playground
map={map}
script={import('./script.ts?raw')}
utils={(
<>
<VesselInfo />
</>
)}
/>
<Bottom
links={[
{ title: 'Next: Example', href: '/articles/full' },
]}
/>

View File

@@ -0,0 +1,44 @@
import { MapTile, PortMapTile, CaptainAI, CaptainCommand } from "@shipped/engine";
// We start by defining a function that will be called every turn
const captain: CaptainAI = ({ vessel, map, currentPort }) => {
// Then we create an array to hold any commands we want to send to the vessel
const commands: CaptainCommand[] = [];
// If the vessel doesn't have a plan, give it one
if (!vessel.plan || vessel.plan.length === 0) {
// If we're at a port, we want to fuel up
if (currentPort) {
commands.push({
type: 'fuel-up',
});
}
// We get our list of port tiles
const portTiles = findPorts(map);
// And pick a random one
const randomPort = portTiles[Math.floor(Math.random() * portTiles.length)];
commands.push({
type: 'update-plan',
// And then update the plan to navigate to it
plan: [randomPort],
});
}
// Hand of the wheel
return commands;
}
// Since we only have a tile map, we want to convert it into a list
// of all the port tiles
const findPorts = (tiles: MapTile[][]) => {
const portTiles = tiles.map((row, y) => row.map((tile, x) => ({
x,
y,
tile: tile as PortMapTile,
}))).flat().filter(({ tile }) => tile.type === 'port');
return portTiles;
};
export default captain;

View File

@@ -0,0 +1,30 @@
import { Bottom } from '@/ui/base/bottom';
# The game
_Shipped_ is a game where you build bots, which controls vessels on the sea, battling for supremacy. You win by making the bot which can earn the most money, in the shortest amount of time, emitting the least CO2.
## The rules
The game is played in a succession of rounds. In each round your bot is giving information about the vessel, and the world around it. Based on this information, your bot must decide what to do next.
Bots can perform a multitude of actions, for instance:
### Anywhere
- Create a planned route for the vessel to follow
- Change the speed of the vessel
### At ports
- Fuel the vessel
- Buy goods
- Sell goods
## Scoring
The exact score calulation is TBD but will be a combination of profit and CO2 emissions.
<Bottom
links={[
{ title: 'Next: Planning a route', href: '/articles/rules/move' },
]}
/>

View File

@@ -0,0 +1,27 @@
import { Bottom } from '@/ui/base/bottom';
import { createMap } from "@shipped/engine";
import { Playground } from "@/ui/playground";
export const map = {
size: { width: 10, height: 10 },
}
# Land
Unfortunatly the world is not only water. There are also land tiles. Land tiles are not passable by ships.
You can use the `map` property to check which tiles are land.
It is generally a good idea to avoid sailing unto land as your ship would get stuck.
The bot below find connecting square tiles, check if they are water tiles and sails to a random one.
<Playground
map={map}
script={import('./script.ts?raw')}
/>
<Bottom
links={[
{ title: 'Next: Fuel', href: '/articles/rules/fuel' },
]}
/>

View File

@@ -0,0 +1,18 @@
import { CaptainAI } from "@shipped/engine";
const captain: CaptainAI = ({ vessel, map }) => {
if (!vessel.plan || vessel.plan.length === 0) {
const connectedPassableTiles = [[0, -1], [1, 0], [0, 1], [-1, 0]].map(([ x, y ]) => ({
x: vessel.position.x + x,
y: vessel.position.y + y,
})).filter(({ x, y }) => map[y]?.[x] && map[y][x].type !== 'land')
const randomTile = connectedPassableTiles[Math.floor(Math.random() * connectedPassableTiles.length)];
return [{
type: 'update-plan',
plan: [randomTile],
}];
}
}
export default captain;

View File

@@ -0,0 +1,31 @@
import { Bottom } from '@/ui/base/bottom';
import { createMap } from "@shipped/engine";
import { Playground } from "@/ui/playground";
export const map = createMap(10, 3);
export const vessel = {
position: { x: 1, y: 1 },
}
# Planning a route
So the first thing that you bot needs to learn is how to move the vessel around.
Everything the bot does, is done by returning a set of commands from its AI function.
There are different commands, but they all take the form of <code>{`{ type: string, [prop: string]: any }`}</code>.
To move the ship the Captain needs to provide a plan in the form of a series of waypoints, which the ship will follow.
The captain can at anytime give a new plan, and the ship will then start to follow the updated plan
<Playground
map={map}
vessel={vessel}
script={import('./script.ts?raw')}
/>
<Bottom
links={[
{ title: 'Next: Land', href: '/articles/rules/land' },
]}
/>

View File

@@ -0,0 +1,18 @@
import { CaptainAI } from "@shipped/engine";
// We start by defining a function that will be called every turn
const captain: CaptainAI = ({ vessel }) => {
// If the vessel already have a plan we don't want to do anything
if (vessel.plan && vessel.plan.length > 0) {
return;
}
// If the vessel doesn't have a plan, give it one
return [{
type: 'update-plan',
// This is a plan to move to the tile at (8, 1)
plan: [{ x: 8, y: 1 }],
}];
}
export default captain;