GamePool / Game room added

This commit is contained in:
Michael Wain 2024-03-01 23:57:37 +03:00
parent ccc87d57f4
commit 094bebb7cb
24 changed files with 447 additions and 64 deletions

View File

@ -44,6 +44,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>

View File

@ -0,0 +1,24 @@
package com.alterdekim.game;
import com.alterdekim.game.component.game.GamePool;
import com.alterdekim.game.websocket.WebSocketHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.*;
@Configuration
@EnableWebSocket
@ComponentScan("com.alterdekim.game.component.game")
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private GamePool gamePool;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new WebSocketHandler(gamePool), "/websocket")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}

View File

@ -8,6 +8,7 @@ import com.alterdekim.game.component.result.LongPollResult;
import com.alterdekim.game.component.result.LongPollResultSingle;
import com.alterdekim.game.component.result.LongPollResultType;
import com.alterdekim.game.dto.*;
import com.alterdekim.game.entities.RoomPlayer;
import com.alterdekim.game.service.*;
import com.alterdekim.game.util.Hash;
import lombok.Getter;
@ -69,14 +70,20 @@ public class LongPoll {
});
getLongPollingQueue().forEach(longPollingSession -> {
try {
if( !map.containsKey(longPollingSession.getUserId())) map.put(longPollingSession.getUserId(), new LongPollConfig(0L,new ArrayList<>(), 0, Hash.rnd(), new ArrayList<>(), System.currentTimeMillis(), new ArrayList<>()));
if( !map.containsKey(longPollingSession.getUserId())) map.put(longPollingSession.getUserId(), new LongPollConfig(0L));
LongPollConfig config = map.get(longPollingSession.getUserId());
LongPollResult result = process(longPollingSession.getUserId(), config);
config.setSession_pass(config.getSession_pass()+1);
if( !result.getResultWithType(LongPollResultType.InviteResult, InviteResult.class).isEmpty() || !result.getResultWithType(LongPollResultType.FriendResult, FriendResult.class).isEmpty() || !result.getResultWithType(LongPollResultType.RoomResult, RoomResultV2.class).isEmpty() || !result.getResultWithType(LongPollResultType.ChatResult, ChatResult.class).isEmpty() || config.getSession_pass() >= iterations) {
if( !config.getGameRedirect().isEmpty() ||
!result.getResultWithType(LongPollResultType.InviteResult, InviteResult.class).isEmpty() ||
!result.getResultWithType(LongPollResultType.FriendResult, FriendResult.class).isEmpty() ||
!result.getResultWithType(LongPollResultType.RoomResult, RoomResultV2.class).isEmpty() ||
!result.getResultWithType(LongPollResultType.ChatResult, ChatResult.class).isEmpty() ||
config.getSession_pass() >= iterations) {
longPollingSession.getDeferredResult().setResult(result);
config.setSession_pass(0);
config.setInvites(new ArrayList<>());
config.setGameRedirect(new ArrayList<>());
}
map.put(longPollingSession.getUserId(), config);
} catch (Exception e) {
@ -89,8 +96,19 @@ public class LongPoll {
private LongPollResult process(Long userId, LongPollConfig config) {
List<LongPollResultSingle> result = new ArrayList<>();
result.add(new LongPollResultSingle<>(LongPollResultType.OnlineUsers, Arrays.asList(map.size())));
result.add(new LongPollResultSingle<>(LongPollResultType.Redirect, config.getGameRedirect()));
processors.forEach(p -> result.add(p.process(config, userId)));
result.add(new LongPollResultSingle<>(LongPollResultType.InviteResult, config.getInvites().stream().map(i -> new InviteResult(i.getRoomId(), i.getUserId(), i.getUsername())).collect(Collectors.toList())));
return new LongPollResult(result);
}
public void notifyPlayers(Long roomId, List<RoomPlayer> players) {
players.forEach(p -> {
LongPollConfig lc = map.get(p.getUserId());
List<GameRedirect> gr = lc.getGameRedirect();
gr.add(new GameRedirect(roomId));
lc.setGameRedirect(gr);
map.put(p.getUserId(), lc);
});
}
}

View File

@ -1,16 +1,18 @@
package com.alterdekim.game.component;
import com.alterdekim.game.dto.GameInvite;
import com.alterdekim.game.dto.GameRedirect;
import com.alterdekim.game.dto.RoomResult;
import com.alterdekim.game.dto.UserResult;
import com.alterdekim.game.util.Hash;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ -22,4 +24,16 @@ public class LongPollConfig {
private List<UserResult> friends_online;
private Long lastRequest;
private List<GameInvite> invites;
private List<GameRedirect> gameRedirect;
public LongPollConfig(Long last_chat_id) {
this.last_chat_id = last_chat_id;
this.rooms = new ArrayList<>();
this.session_pass = 0;
this.poll_token = Hash.rnd();
this.friends_online = new ArrayList<>();
this.lastRequest = System.currentTimeMillis();
this.invites = new ArrayList<>();
this.gameRedirect = new ArrayList<>();
}
}

View File

@ -0,0 +1,70 @@
package com.alterdekim.game.component.game;
import com.alterdekim.game.component.LongPoll;
import com.alterdekim.game.entities.RoomPlayer;
import com.alterdekim.game.entities.User;
import com.alterdekim.game.service.RoomPlayerServiceImpl;
import com.alterdekim.game.service.RoomServiceImpl;
import com.alterdekim.game.service.UserServiceImpl;
import com.alterdekim.game.util.Hash;
import com.alterdekim.game.websocket.message.BasicMessage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class GamePool {
@Autowired
private LongPoll longPoll;
@Autowired
private UserServiceImpl userService;
@Autowired
private RoomServiceImpl roomService;
@Autowired
private RoomPlayerServiceImpl roomPlayerService;
private ConcurrentHashMap<Long, GameRoom> games = new ConcurrentHashMap<>();
@Scheduled(fixedRate = 1000)
private void refreshRooms() {
roomService.getAllActive()
.forEach(r -> {
if( roomPlayerService.findByRoomId(r.getId()).size() != r.getPlayerCount().intValue() ) return;
List<RoomPlayer> players = roomPlayerService.findByRoomId(r.getId());
roomPlayerService.removeByRoomId(r.getId());
roomService.removeRoom(r.getId());
games.put(r.getId(), new GameRoom(players));
longPoll.notifyPlayers(r.getId(), players);
});
}
public void receiveMessage(String message, WebSocketSession session) {
try {
message = message.substring(message.indexOf("{"));
BasicMessage pm = new ObjectMapper().readValue(message, BasicMessage.class);
User u = userService.findById(pm.getUid());
if (u == null || !games.containsKey(pm.getRoomId())) return;
if (!Hash.sha256((u.getId() + u.getUsername() + u.getPassword() + pm.getRoomId()).getBytes()).equals(pm.getAccessToken()))
return;
games.get(pm.getRoomId()).receiveMessage(pm, session);
} catch (JsonProcessingException | NoSuchAlgorithmException e) {
log.error(e.getMessage(), e);
} catch (StringIndexOutOfBoundsException e) {
//log.error(e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,42 @@
package com.alterdekim.game.component.game;
import com.alterdekim.game.entities.RoomPlayer;
import com.alterdekim.game.websocket.message.BasicMessage;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class GameRoom {
@Getter
private List<RoomPlayer> players;
private ConcurrentHashMap<Long, WebSocketSession> socks;
public GameRoom(List<RoomPlayer> players) {
this.players = players;
this.socks = new ConcurrentHashMap<>();
}
public void receiveMessage(BasicMessage message, WebSocketSession session) {
if(players.stream().noneMatch(p -> p.getUserId().longValue() == message.getUid().longValue())) return;
socks.put(message.getUid(), session);
log.info("GOT MESSAGE " + message.getUid() + " " + message.getAccessToken() + " " + message.getBody());
this.sendMessage(message.getUid());
}
private void sendMessage(Long userId) {
try {
if (socks.get(userId).isOpen())
socks.get(userId).sendMessage(new TextMessage("HEY!"));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}

View File

@ -5,5 +5,6 @@ public enum LongPollResultType {
ChatResult,
RoomResult,
FriendResult,
InviteResult
InviteResult,
Redirect
}

View File

@ -207,7 +207,7 @@ public class APIController {
if( longPoll.getMap().containsKey(userId) ){
LongPollConfig c = longPoll.getMap().get(userId);
if( !c.getPoll_token().equals(poll_token) ) {
c = new LongPollConfig(last_chat_id, rooms, 0, poll_token, friends, System.currentTimeMillis(), new ArrayList<>());
c = new LongPollConfig(last_chat_id, rooms, 0, poll_token, friends, System.currentTimeMillis(), new ArrayList<>(), new ArrayList<>());
longPoll.getLongPollingQueue().removeIf(q -> q.getUserId().longValue() == userId.longValue());
}
c.setRooms(rooms);
@ -217,7 +217,7 @@ public class APIController {
c.setLastRequest(System.currentTimeMillis());
longPoll.getMap().put(userId, c);
} else {
longPoll.getMap().put(userId, new LongPollConfig(last_chat_id, rooms, 0, poll_token, friends, System.currentTimeMillis(), new ArrayList<>()));
longPoll.getMap().put(userId, new LongPollConfig(last_chat_id, rooms, 0, poll_token, friends, System.currentTimeMillis(), new ArrayList<>(), new ArrayList<>()));
}
longPoll.getLongPollingQueue().add(new LongPollingSession(userId, deferredResult));
return deferredResult;

View File

@ -0,0 +1,34 @@
package com.alterdekim.game.controller;
import com.alterdekim.game.dto.AuthApiObject;
import com.alterdekim.game.entities.User;
import com.alterdekim.game.service.UserServiceImpl;
import com.alterdekim.game.util.AuthenticationUtil;
import com.alterdekim.game.util.Hash;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Slf4j
@Controller
public class GameController {
@Autowired
private UserServiceImpl userService;
@GetMapping("/game")
public String gamePage(Model model, @RequestParam("id") Long roomId) {
model.addAttribute("roomId", roomId);
try {
User u = AuthenticationUtil.authProfile(model, userService);
String apiKey = Hash.sha256((u.getId() + u.getUsername() + u.getPassword() + roomId).getBytes());
model.addAttribute("auth_obj", new AuthApiObject(apiKey, u.getId(), Hash.rnd()));
} catch ( Exception e ) {
log.error(e.getMessage(), e);
}
return "game";
}
}

View File

@ -48,11 +48,6 @@ public class StaticController {
return "rules";
}
@GetMapping("/game")
public String gamePage(Model model) {
return "game";
}
@GetMapping("/games")
public String gamesPage(Model model) {
try {

View File

@ -0,0 +1,16 @@
package com.alterdekim.game.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class GameField {
private Long uid;
private Integer id;
private String cost;
private String img;
private String stars;
}

View File

@ -0,0 +1,10 @@
package com.alterdekim.game.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class GameRedirect {
private Long roomId;
}

View File

@ -22,6 +22,11 @@ public interface RoomPlayerRepository extends JpaRepository<RoomPlayer, Long> {
@Query(value = "DELETE FROM RoomPlayer r WHERE r.userId = :userId")
void deleteAllByUserId(@Param("userId") Long userId);
@Transactional
@Modifying
@Query(value = "DELETE FROM RoomPlayer r WHERE r.roomId = :roomId")
void removeByRoomId(@Param("roomId") Long roomId);
@Query(value = "SELECT r.roomId FROM RoomPlayer r WHERE r.userId = :userId ORDER BY r.roomId ASC LIMIT 1")
Long hasUserId(@Param("userId") Long userId);
}

View File

@ -42,6 +42,7 @@ public class SpringSecurity {
.requestMatchers("/friends").hasAnyAuthority("ROLE_ADMIN")
.requestMatchers("/followers").hasAnyAuthority("ROLE_ADMIN")
.requestMatchers("/settings").hasAnyAuthority("ROLE_ADMIN")
.requestMatchers("/websocket/**").hasAnyAuthority("ROLE_ADMIN")
.requestMatchers("/static/**").permitAll()
.requestMatchers("/access-denied").permitAll()
.requestMatchers("/signup").permitAll()

View File

@ -0,0 +1,19 @@
package com.alterdekim.game.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.security.authorization.AuthorityAuthorizationManager;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.annotation.web.socket.EnableWebSocketSecurity;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
@Configuration
@EnableWebSocketSecurity
public class WebSocketSecurityConfig {
@Bean
AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
return AuthorityAuthorizationManager.hasAnyAuthority("ROLE_ADMIN");
}
}

View File

@ -36,4 +36,8 @@ public class RoomPlayerServiceImpl implements RoomPlayerService{
public Long hasUserId(Long userId) {
return repository.hasUserId(userId);
}
public void removeByRoomId(Long roomId) {
repository.removeByRoomId(roomId);
}
}

View File

@ -35,4 +35,8 @@ public class RoomServiceImpl implements RoomService {
public void clearEmptyRooms() {
roomRepository.clearEmptyRooms();
}
public void removeRoom(Long roomId) {
roomRepository.deleteById(roomId);
}
}

View File

@ -1,12 +1,13 @@
package com.alterdekim.game.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class Hash {
public static String sha256( byte[] b ) throws Exception {
public static String sha256( byte[] b ) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return bytesToHex(digest.digest(b));
}

View File

@ -0,0 +1,32 @@
package com.alterdekim.game.websocket;
import com.alterdekim.game.component.game.GamePool;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
@Slf4j
@AllArgsConstructor
public class WebSocketHandler extends TextWebSocketHandler {
private GamePool gamePool;
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
String receivedMessage = (String) message.getPayload();
gamePool.receiveMessage(receivedMessage, session);
}
@Override
public void afterConnectionEstablished(WebSocketSession session) {
// Perform actions when a new WebSocket connection is established
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
// Perform actions when a WebSocket connection is closed
}
}

View File

@ -0,0 +1,17 @@
package com.alterdekim.game.websocket.message;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class BasicMessage {
private Long roomId;
private String accessToken;
private Long uid;
private String body;
}

View File

@ -0,0 +1,4 @@
package com.alterdekim.game.websocket.message;
public interface IWebSocketMessage {
}

View File

@ -0,0 +1,25 @@
const player_html = '<div class="player" th:data-pid="${p.id}" onClick="drop(this)"><p class="timeout"></p><p class="nickname" th:text="${p.name}"></p><p class="money" th:text="${p.money}"></p><div class="dropbox" style="display: none"></div> <!-- margin-top: -35px; --></div>';
var stompClient = null;
function disconnect() {
if (stompClient !== null) {
stompClient.disconnect();
}
console.log("Disconnected");
}
function sendMessage(message) {
stompClient.send("/", {}, JSON.stringify({'body': JSON.stringify(message), 'accessToken': $("api-tag").attr("data-access-token"), 'uid': $("api-tag").attr("data-uid"), 'roomId': $("api-tag").attr("data-room-id")}));
}
$(document).ready(function() {
var socket = new SockJS('/websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/', function (message) {
//showMessage(JSON.parse(message.body).content);
});
});
});

View File

@ -61,6 +61,12 @@ function takeInviteMessage(obj) {
});
}
function takeRedirect(reds) {
for( let i = 0; i < reds.length; i++ ) {
window.location.assign("/game?id=" + reds[i].roomId);
}
}
function successPolling(data) {
console.log(data);
data = data.result;
@ -89,6 +95,9 @@ function successPolling(data) {
case "InviteResult":
invites = res.array;
break;
case "Redirect":
takeRedirect(res.array);
break;
}
}

View File

@ -1,11 +1,14 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8"></meta>
<meta charset="utf-8"/>
<link rel="stylesheet" href="/static/css/game.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<title>Монополия онлайн</title>
<link rel="shortcut icon" type="image/png" href="../static/images/favicon.ico"/>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js" integrity="sha512-iKDtgDyTHjAitUDdLljGhenhPwrbBfqTKWO1mkhSFH3A7blITC9MhYon6SjnMhp4o0rADGw9yAC6EW4t5a4K3g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<title th:text="${title} ? ${title} : 'Nosedive'"></title>
<link rel="shortcut icon" type="image/x-icon" href="/static/images/favicon.ico">
<api-tag th:data-access-token="${auth_obj.accessToken}" th:data-uid="${auth_obj.uid}" th:data-room-id="${roomId}"></api-tag>
<div id="contract_field" style="display: none">
<div style="display: inline-flex;">
<div class="contract_image" style="margin-left: 10px"><img src="../static/images/mitsubishi.png" style="width: 55px;"></div>
@ -26,14 +29,12 @@
<div class="wrapper" style="width: 100%; height: 100%">
<div class="game">
<div class="players">
<th:block th:each="p: ${players}">
<div class="player" th:data-pid="${p.id}" onClick="drop(this)">
<p class="timeout"></p>
<p class="nickname" th:text="${p.name}"></p>
<p class="money" th:text="${p.money}"></p>
<div class="dropbox" style="display: none"></div> <!-- margin-top: -35px; -->
</div>
</th:block>
<!--<div class="player" th:data-pid="${p.id}" onClick="drop(this)">
<p class="timeout"></p>
<p class="nickname" th:text="${p.name}"></p>
<p class="money" th:text="${p.money}"></p>
<div class="dropbox" style="display: none"></div> margin-top: -35px;
</div>-->
</div>
<div class="table">
<div class="table_center">
@ -212,61 +213,90 @@
<div class="chip purple" data-trow=0 data-tcol=0></div>
<div class="chip orange" data-trow=0 data-tcol=0></div>
<!-- </div> -->
<div class="up"><div class="board_field corner"><img src="../static/images/start.png" style="width: 100%; height: 100%;" /></div></div>
<div class="up">
<div class="board_field corner">
<img src="../static/images/start.png" style="width: 100%; height: 100%;" />
</div>
</div>
<th:block th:each="f: ${fields_up}">
<div th:data-fid="${f.uid}" class="board_field" th:style="grid-column: @{${f.id}}"> <!-- 2 + -->
<div class="cost" th:text="${f.cost}"></div>
<!--<th:block th:each="ft: ${fields_top}">
<div th:data-fid="${ft.uid}" class="board_field" th:style="@{grid-column: {id}(id=${ft.id})}"> --> <!-- 2 + -->
<!-- <div class="cost" th:text="${ft.cost}"></div>
<div class="fh">
<div class="iconH">
<img th:src="${f.img}" class="vertImg">
<img th:src="${ft.img}" class="vertImg">
</div>
<div class="stars">
<span class="_star" th:text="${f.stars}"></span>
<span class="_star" th:text="${ft.stars}"></span>
</div>
</div>
</div>
</th:block>
</th:block>-->
<div class="up" style="grid-column: 11"><div class="board_field corner"><img src="../static/images/injail.png" style="width: 100%; height: 100%;" /></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/lacoste.png" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/mitsubishi.png" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/vw.png" class="horImg"></div></div><div class="stars hstars"><span class="_star _hstar"></span></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/mcdonalds.svg" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/apple.png" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/vw.png" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/macdonalds.png" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/nike.png" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 11"><div class="rcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/pepsi.png" class="horImg"></div><div class="stars hstars"><span class="_star _hstar"></span></div></div></div>
<div class="board_field corner" style="grid-column: 11"><img src="../static/images/parking.png" style="width:100%; height:100%;" /></div>
<div class="board_field" style="grid-column: 2; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/nike.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 3; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/mitsubishi.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 4; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/peugeot.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 5; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/vw.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 6; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/linux.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 7; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/apple.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 8; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/burger_king.svg" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 9; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/lacoste.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field" style="grid-column: 10; grid-row: 11;"><div class="fh"><div class="iconH"><img src="../static/images/pepsi.png" class="vertImg"></div><div class="stars"><span class="_star _vstar"></span></div></div><div class="cost">0</div></div>
<div class="board_field corner" style="grid-column: 1; grid-row: 11"><img src="../static/images/gotojail.png" style="width: 100%; height: 100%;" /></div>
<div class="board_field" style="grid-column: 1; grid-row: 10;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/peugeot.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 9;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/apple.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 8;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/linux.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 7;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/macdonalds.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 6;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/pepsi.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 5;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/burger_king.svg" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 4;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/mitsubishi.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 3;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/adidas.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="board_field" style="grid-column: 1; grid-row: 2;"><div class="lcost">0</div><div class="fv"><div class="iconV"><img src="../static/images/lacoste.png" class="horImg"></div><div class="stars hstars _hhstar"><span class="_star _hstar"></span></div></div></div>
<div class="up" style="grid-column: 11">
<div class="board_field corner">
<img src="../static/images/injail.png" style="width: 100%; height: 100%;" />
</div>
</div>
<!--
<th:block th:each="fr: ${fields_right}">
<div class="board_field" style="grid-column: 11">
<div class="rcost" th:text="${fr.cost}"></div>
<div class="fv">
<div class="iconV">
<img th:src="${fr.img}" class="horImg">
</div>
<div class="stars hstars">
<span class="_star _hstar" th:text="${fr.stars}"></span>
</div>
</div>
</div>
</th:block> -->
<div class="board_field corner" style="grid-column: 11">
<img src="../static/images/parking.png" style="width:100%; height:100%;" />
</div>
<!-- <th:block th:each="fb: ${fields_bottom}">
<div class="board_field" th:style="@{grid-row: 11; grid-column: {id}(id=${fb.id})}"> --> <!-- 2 + --> <!--
<div class="fh">
<div class="iconH">
<img class="vertImg" th:src="${fb.img}">
</div>
<div class="stars">
<span class="_star _vstar" th:text="${fb.stars}"></span>
</div>
</div>
<div class="cost" th:text="${fb.cost}"></div>
</div>
</th:block> -->
<div class="board_field corner" style="grid-column: 1; grid-row: 11">
<img src="../static/images/gotojail.png" style="width: 100%; height: 100%;" />
</div>
<!--
<th:block th:each="fl: ${fields_left}">
<div class="board_field" th:style="@{grid-column: 1; grid-row: {id}(id=${fl.id})}">--> <!-- start from 10, go to 2 --> <!--
<div class="lcost" th:text="${fl.cost}"></div>
<div class="fv">
<div class="iconV">
<img th:src="${fl.img}" class="horImg">
</div>
<div class="stars hstars _hhstar">
<span class="_star _hstar" th:text="${fl.stars}"></span>
</div>
</div>
</div>
</th:block> -->
</div>
</div>
</div>
</body>
<footer>
<script type="text/javascript" src="/games/cookie.js"></script>
<script type="text/javascript" src="/games/db.js"></script>
<script type="text/javascript" src="/games/methods.js"></script>
<script type="text/javascript" src="game.js"></script>
<script type="text/javascript" src="/static/javascript/game.js"></script>
<script type="text/javascript" src="/static/javascript/scale.js"></script>
</footer>
</html>