| """Fully-convolutional autoencoder over OpenFront tile states. |
| |
| Input encoding per tile: |
| - owner slot -> learned embedding (static slot per player for a whole game, |
| slot 0 = unowned, slots 1..MAX_SLOTS-1 assigned by smallID order at spawn) |
| - terrain scalars: land flag, normalized magnitude, fallout flag |
| |
| The encoder downsamples by 16x into a spatial latent grid (LATENT_C channels |
| per 16x16 tile region), so any map size divisible by 16 works. The decoder |
| reconstructs per-tile owner-slot logits. |
| """ |
|
|
| import torch |
| import torch.nn as nn |
|
|
| MAX_SLOTS = 128 |
| OWNER_EMB_DIM = 8 |
| TERRAIN_CHANNELS = 3 |
|
|
|
|
| def conv_block(c_in: int, c_out: int, stride: int) -> nn.Sequential: |
| return nn.Sequential( |
| nn.Conv2d(c_in, c_out, kernel_size=3, stride=stride, padding=1), |
| nn.GroupNorm(8, c_out), |
| nn.SiLU(), |
| ) |
|
|
|
|
| def deconv_block(c_in: int, c_out: int) -> nn.Sequential: |
| return nn.Sequential( |
| nn.ConvTranspose2d(c_in, c_out, kernel_size=4, stride=2, padding=1), |
| nn.GroupNorm(8, c_out), |
| nn.SiLU(), |
| ) |
|
|
|
|
| class TileAutoencoder(nn.Module): |
| def __init__(self, latent_c: int = 64): |
| super().__init__() |
| self.owner_emb = nn.Embedding(MAX_SLOTS, OWNER_EMB_DIM) |
| c_in = OWNER_EMB_DIM + TERRAIN_CHANNELS |
|
|
| self.encoder = nn.Sequential( |
| conv_block(c_in, 32, stride=1), |
| conv_block(32, 64, stride=2), |
| conv_block(64, 96, stride=2), |
| conv_block(96, 128, stride=2), |
| conv_block(128, 128, stride=2), |
| nn.Conv2d(128, latent_c, kernel_size=1), |
| ) |
|
|
| self.decoder = nn.Sequential( |
| conv_block(latent_c, 128, stride=1), |
| deconv_block(128, 128), |
| deconv_block(128, 96), |
| deconv_block(96, 64), |
| deconv_block(64, 32), |
| nn.Conv2d(32, MAX_SLOTS, kernel_size=1), |
| ) |
|
|
| def encode(self, owners: torch.Tensor, terrain: torch.Tensor) -> torch.Tensor: |
| """owners: (B, H, W) int64 slots; terrain: (B, 3, H, W) float.""" |
| emb = self.owner_emb(owners).permute(0, 3, 1, 2) |
| x = torch.cat([emb, terrain], dim=1) |
| return self.encoder(x) |
|
|
| def forward( |
| self, owners: torch.Tensor, terrain: torch.Tensor |
| ) -> tuple[torch.Tensor, torch.Tensor]: |
| z = self.encode(owners, terrain) |
| logits = self.decoder(z) |
| return logits, z |
|
|